我经常在Corda Slack频道中闲逛,并尽可能回答问题。 我尝试回答的合理数量的问题与Oracle有关。 更具体地说,何时使用。 我觉得我可以回答,“当您需要验证可能经常更改的外部数据时使用Oracle”。 我可能在某个时候写了一个类似的答案。 我没办法做的...告诉某人如何实施。 因此,要纠正这一点。 我写这篇文章的目的是学习如何实现自己,并与您和我未来的自我分享这些知识。
何时使用Oracle
让我们从扩展何时使用Oracle开始。 就像我刚才提到的那样,当您需要验证可能经常更改的外部数据时,应该使用Oracle。 这可能是诸如汇率,股票价格之类的数据,甚至是我的博客当前处于上升还是下降状态(尽管我还没有看到它下降过!)。 我认为经常性部分在这里很重要。 如果数据很少更改,则针对包含与Oracle自己检索的相同类型的值的附件验证某些数据可能是可行的。 因此,我认为,仅由Oracle才能验证汇率等数据。 话虽如此,它实际上取决于您的特定用例。
如何使用Oracle
Oracle如何进行此验证? 好吧,这取决于您。 但是,它可能会遵循以下步骤:
- 从节点接收数据
- 检索外部数据
- 根据外部数据验证接收到的数据
- 提供交易签名
这些是我认为大多数Oracle实现将包含的步骤。 可以添加更多步骤,并且完成的验证可以与用例需求一样复杂或简单。 尽管可以增加更多的步骤,但我真的怀疑,排除上面显示的任何步骤的Oracle是否会有很多用途。
上面显示的所有步骤仅从Oracle的角度显示了该过程。 还有更多事情要做,所以我认为一个好的图表可以帮助我们。 它还将继续介绍我将在本文中使用的示例。
这些步骤中有很多是通用步骤,无论您将其放在何处,都将执行这些步骤。 在本节中,我将扩展并显示实现图中所示流程所涉及的代码。 因此值得一看。。。我也花了很多时间使它看起来不错,所以请看一下!!
哦,在我继续之前还有一点。 我想强调一下将时序图组合起来对Corda Flows建模有多大帮助。 它确实突出显示了涉及的人员,需要进行多少次网络跳跃以及每个参与者进行了多少工作。 此外,它们是向只对您正在设计和/或实现的更高层次的流程感兴趣的人解释发生了什么的好方法。
客户端/不是Oracle端
如前所述,这里的某些代码是通用代码,您很可能会将其放入编写的任何Flow中。 我已经展示了所有内容,因此对于正在发生的事情没有任何歧义,但是我将仅针对需要突出显示的点进行扩展,因为它们包含特定于与Oracle交互的代码。
@InitiatingFlow
@StartableByRPC
class GiveAwayStockFlow(
private val symbol: String,
private val amount: Long,
private val recipient: String
) :
FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val recipientParty = party()
val oracle = oracle()
val transaction =
collectRecipientSignature(
verifyAndSign(transaction(recipientParty, oracle)),
recipientParty
)
val allSignedTransaction = collectOracleSignature(transaction, oracle)
return subFlow(FinalityFlow(allSignedTransaction))
}
private fun party(): Party =
serviceHub.networkMapCache.getPeerByLegalName(CordaX500Name.parse(recipient))
?: throw IllegalArgumentException("Party does not exist")
private fun oracle(): Party = serviceHub.networkMapCache.getPeerByLegalName(
CordaX500Name(
"Oracle",
"London",
"GB"
)
)
?: throw IllegalArgumentException("Oracle does not exist")
@Suspendable
private fun collectRecipientSignature(
transaction: SignedTransaction,
party: Party
): SignedTransaction {
val signature = subFlow(
CollectSignatureFlow(
transaction,
initiateFlow(party),
party.owningKey
)
).single()
return transaction.withAdditionalSignature(signature)
}
private fun verifyAndSign(transaction: TransactionBuilder): SignedTransaction {
transaction.verify(serviceHub)
return serviceHub.signInitialTransaction(transaction)
}
private fun transaction(recipientParty: Party, oracle: Party): TransactionBuilder =
TransactionBuilder(notary()).apply {
val priceOfStock = priceOfStock()
addOutputState(state(recipientParty, priceOfStock), StockContract.CONTRACT_ID)
addCommand(
GiveAway(symbol, priceOfStock),
listOf(recipientParty, oracle).map(Party::owningKey)
)
}
private fun priceOfStock(): Double =
serviceHub.cordaService(StockRetriever::class.java).getCurrent(symbol).price
private fun state(party: Party, priceOfStock: Double): StockGiftState =
StockGiftState(
symbol = symbol,
amount = amount,
price = priceOfStock * amount,
recipient = party
)
private fun notary(): Party = serviceHub.networkMapCache.notaryIdentities.first()
@Suspendable
private fun collectOracleSignature(
transaction: SignedTransaction,
oracle: Party
): SignedTransaction {
val filtered = filteredTransaction(transaction, oracle)
val signature = subFlow(CollectOracleStockPriceSignatureFlow(oracle, filtered))
return transaction.withAdditionalSignature(signature)
}
private fun filteredTransaction(
transaction: SignedTransaction,
oracle: Party
): FilteredTransaction =
transaction.buildFilteredTransaction(Predicate {
when (it) {
is Command<*> -> oracle.owningKey in it.signers && it.value is GiveAway
else -> false
}
})
}
@InitiatedBy(GiveAwayStockFlow::class)
class SendMessageResponder(val session: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
subFlow(object : SignTransactionFlow(session) {
override fun checkTransaction(stx: SignedTransaction) {}
})
}
}
首先,让我们看一下如何构建事务:
private fun transaction(recipientParty: Party, oracle: Party): TransactionBuilder =
TransactionBuilder(notary()).apply {
val priceOfStock = priceOfStock()
addOutputState(state(recipientParty, priceOfStock), StockContract.CONTRACT_ID)
addCommand(
GiveAway(symbol, priceOfStock),
listOf(recipientParty, oracle).map(Party::owningKey)
)
}
private fun priceOfStock(): Double =
serviceHub.cordaService(StockRetriever::class.java).getCurrent(symbol).price
这与我创建不涉及Oracle的事务的方式没有太大不同。 仅有的两个区别是,从外部来源(隐藏在StockRetriever
服务内部)检索股票价格,并在命令中包括Oracle的签名。 这些代码添加内容与使用Oracle的原因相吻合。 外部数据包含在事务中,并且Oracle需要验证它是否正确。 为了证明甲骨文认为交易有效,我们需要其签名。
我们将仔细研究分别获取外部数据的方法。
接下来是收集收件人签名:
@Suspendable
private fun collectRecipientSignature(
transaction: SignedTransaction,
party: Party
): SignedTransaction {
val signature = subFlow(
CollectSignatureFlow(
transaction,
initiateFlow(party),
party.owningKey
)
).single()
return transaction.withAdditionalSignature(signature)
}
收集交易对手的签名并不是Flow真正不平凡的一步,但CollectSignatureFlow
不同的是,使用CollectSignatureFlow
而不是通常使用的CollectSignaturesFlow
(注意中间缺少“ s”)。 这是由于在事务中需要Oracle的签名。 调用CollectSignaturesFlow
将会从所有必需的签名者(包括Oracle)中检索签名。 这将Oracle视为“正常”参与者。 这不是我们想要的。 取而代之的是,我们需要单独地或手动地获得收件人和Oracle的签名。 手动部分是使用transaction.withAdditionalSignature
。
现在,接收者已经签署了交易,Oracle需要签署它:
@Suspendable
private fun collectOracleSignature(
transaction: SignedTransaction,
oracle: Party
): SignedTransaction {
val filtered = filteredTransaction(transaction, oracle)
val signature = subFlow(CollectOracleStockPriceSignatureFlow(oracle, filtered))
return transaction.withAdditionalSignature(signature)
}
private fun filteredTransaction(
transaction: SignedTransaction,
oracle: Party
): FilteredTransaction =
transaction.buildFilteredTransaction(Predicate {
when (it) {
is Command<*> -> oracle.owningKey in it.signers && it.value is GiveAway
else -> false
}
})
在将事务发送给Oracle之前,建议对其进行过滤,以删除Oracle不需要的任何信息。 这可以防止Oracle看到不应共享的信息。 请记住,Oracle可能是另一个组织控制的节点,而不是您试图与其共享状态和事务的参与者。
SignedTransaction
提供了buildFilteredTransaction
函数,该函数仅包含与传入的谓词匹配的对象。在上面的示例中,它过滤掉了GiveAway
(我创建的命令)命令之外的所有命令,该命令还必须具有Oracle作为签名者。
这将输出一个FilteredTransaction
,并传递给CollectOracleStockPriceSignatureFlow
:
@InitiatingFlow
class CollectOracleStockPriceSignatureFlow(
private val oracle: Party,
private val filtered: FilteredTransaction
) : FlowLogic<TransactionSignature>() {
@Suspendable
override fun call(): TransactionSignature {
val session = initiateFlow(oracle)
return session.sendAndReceive<TransactionSignature>(filtered).unwrap { it }
}
}
这些代码所做的全部工作就是将FilteredTransaction
发送到Oracle并等待其签名。 此处的代码可以放入主流程中,但是在可以的情况下将代码拆分出来是非常好的。
最后,从Oracle返回的TransactionSignature
以与接收者签名之前添加方式相同的方式添加到事务中。 此时,由于所有必需的签名者都已做好了自己的准备,因此可以准备提交交易。
甲骨文方面
既然我们已经覆盖了代码的客户端,我们就需要研究一下Oracle如何验证事务。 以下是Oracle代码的内容:
@InitiatedBy(CollectOracleStockPriceSignatureFlow::class)
class OracleStockPriceSignatureResponder(private val session: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val transaction = session.receive<FilteredTransaction>().unwrap { it }
val key = key()
val isValid = transaction.checkWithFun { element: Any ->
when {
element is Command<*> && element.value is GiveAway -> {
val command = element.value as GiveAway
(key in element.signers).also {
validateStockPrice(
command.symbol,
command.price
)
}
}
else -> false
}
}
if (isValid) {
session.send(serviceHub.createSignature(transaction, key))
} else {
throw InvalidStockPriceFlowException("Transaction: ${transaction.id} is invalid")
}
}
private fun key(): PublicKey = serviceHub.myInfo.legalIdentities.first().owningKey
private fun validateStockPrice(symbol: String, price: Double) = try {
serviceHub.cordaService(StockPriceValidator::class.java).validate(symbol, price)
} catch (e: IllegalArgumentException) {
throw InvalidStockPriceFlowException(e.message)
}
}
StockPriceValidator
隐藏了一些应在此处的代码,该代码检索外部股票价格并将其与传递给Oracle的价格进行比较。 它没有太多的代码,并且其验证是基本的,因此我将不对其进行详细说明。 简短地说,我现在不妨展示一下:
@CordaService
class StockPriceValidator(private val serviceHub: AppServiceHub) :
SingletonSerializeAsToken() {
fun validate(symbol: String, price: Double) =
serviceHub.cordaService(StockRetriever::class.java).getCurrent(symbol).let {
require(price == it.price) { "The price of $symbol is ${it.price}, not $price" }
}
}
返回到OracleStockPriceSignatureResponder
。 首先,调用receive
来获取客户端发送的FilteredTransaction
。 然后使用其checkWithFun
函数进行检查。 这是一个方便的函数,它查看每个对象并期望返回Boolean
。 使用此方法,如果事务中包含的所有内容都是GiveAway
命令(Oracle是签名者),并且最重要的是检查该命令中包含的外部数据是否正确,则该事务被视为有效。 如果您回想起以前的代码,则会传入正确的命令和签名者。唯一剩下的验证是在外部数据上。 如果一切正常,那么Oracle将接受该事务并将其签名发送回请求该请求的客户端。
我选择通过引发异常(连同错误消息)来完成验证,然后将异常传播到请求方。 我认为,这使您更容易了解出了什么问题,以便可以正确地进行处理,而不仅仅是直接的“失败验证”消息。 如果Oracle正在执行的验证很复杂,则这些错误消息将变得更加有价值。
检索外部数据
您应该已经看到StockRetriever
类现在弹出两次。 请求方和Oracle中都使用了它。 我已经在两种类型的节点(普通节点和Oracle)之间共享了此代码,但这可能不适用于您自己的用例。 此外,您如何选择检索外部数据取决于您,我只是提供一种可能的解决方案。
该代码可以在下面找到:
@CordaService
class StockRetriever(serviceHub: AppServiceHub) :
SingletonSerializeAsToken() {
private val client = OkHttpClient()
private val mapper = ObjectMapper()
fun getCurrent(symbol: String): Stock {
val response = client.newCall(request(symbol)).execute()
return response.body()?.let {
val json = it.string()
require(json != "Unknown symbol") { "Stock with symbol: $symbol does not exist" }
val tree = mapper.readTree(json)
Stock(
symbol = symbol,
name = tree["companyName"].asText(),
primaryExchange = tree["primaryExchange"].asText(),
price = tree["latestPrice"].asDouble()
)
} ?: throw IllegalArgumentException("No response")
}
private fun request(symbol: String) =
Request.Builder().url("https://api.iextrading.com/1.0/stock/$symbol/quote").build()
}
StockRetriever
是一个很好的小服务,它使用OkHttpClient
( OkHttp )向API(由IEX Trading使用其Java库提供)发出HTTP请求,该API在提供股票代号时返回股票信息。 您可以使用任何想要发出HTTP请求的客户端。 我在一个示例CorDapp中看到了这个,并且我自己考虑了它。 就我个人而言,我也已经习惯了Spring,所以除了RestTemplate
之外,我真的不认识其他任何客户。
返回响应后,它将转换为Stock
对象,并传递回函数的调用者。 那是所有人。
结论
总之,当您的CorDapp需要频繁更改的外部数据且需要先进行验证才能提交事务时,应使用Oracle。 就像状态中保存的数据一样,外部数据非常重要,可能是最重要的,因为它很可能确定事务的主要内容。 因此,所有参与者都必须对数据的正确性感到放心,而不仅仅是凭空提出。 为此,Oracle还将检索外部数据,并根据事务表明数据应具有的内容对其进行验证。 此时,Oracle将签署交易或引发异常并将其视为无效。 由于不需要采取许多步骤,因此实现方面相当简单。 检索数据,将FilteredTransaction
发送到包含将在其中进行验证的数据的Oracle。 是的,阅读本文后,您将了解更多内容。 但是,对于基本流程而言,差不多就可以了。 就像我刚开始说的那样,Oracle如何进行验证可以根据需要简单或复杂。 虽然,我认为大多数将遵循此处所示的相同过程。
现在得出主要结论……总之,您现在已经掌握了在闲聊的渠道中回答有关Oracle的问题的知识,或者知道了在不可行的情况下将其发送到哪里!
这篇文章中使用的代码可以在我的GitHub上找到 。
如果您认为这篇文章有帮助,可以在Twitter上@LankyDanDev关注我,以跟上我的新文章。
翻译自: https://www.javacodegeeks.com/2019/01/validating-external-data-oracle.html