使用Oracle验证外部数据

我在Corda Slack频道闲逛了很多,并尽可能地回答问题。我试图回答的合理数量的问题与Oracles有关。更具体地说,何时使用一个。我觉得我可以回答这个问题,“当你需要验证可以经常更改的外部数据时使用Oracle。”我可能在某些时候写了类似的答案。我不能做的就是告诉别人如何实施。因此,为了纠正这个问题,我写了这篇文章,学习如何自己实现一个,并与你和我未来的自我分享这些知识。

何时使用Oracle

让我们从扩展何时使用Oracle开始。正如我在一分钟前提到的那样,当您需要验证可能经常更改的外部数据时,您应该使用Oracle。这可能是汇率,股票价格等数据,甚至我的博客目前是上涨还是下跌(尽管我还没有看到它已经下跌了!)。我认为这里经常是重要的部分。如果数据很少发生变化,那么根据包含Oracle将自己检索的相同类型值的附件来验证某些数据可能是可行的。这就是为什么在我看来,验证汇率等数据只能由Oracle来完成。所有这一切,它真的取决于你的特定用例。

如何使用Oracle

Oracle如何进行此验证?嗯,这取决于你。但是,它可能会遵循以下步骤:

  • 从节点接收数据

  • 检索外部数据

  • 根据外部数据验证接收的数据

  • 为交易提供签名

这些是我认为大多数Oracle实现将包含的步骤。可以添加更多步骤,并且完成的验证可以与用例需求一样复杂或简单。虽然可以添加更多步骤,但我真的怀疑Oracle中有多少用法可以排除上面显示的任何步骤。

上面显示的所有步骤仅显示Oracle侧面的过程。还有一些进展,所以我认为一个好的图表将帮助我们在这里。它还将继续介绍我将用于此帖子的示例。

使用Oracle验证外部数据显示与Oracle交互过程的序列图

这些步骤中的相当一部分是通用的,将在您放在一起的任何Flow中进行。在本节中,我将展开并展示实现图中所示流程所涉及的代码。因此,值得合理地看待它。我也花了很多时间让它看起来很漂亮,所以请看一下!

哦,在我继续之前的另一点。我想强调将序列图放在一起模拟Corda Flows是多么有用。它真正突出了谁参与,需要制作多少网络跃点以及每个参与者的工作量。此外,它们还可以很好地解释对您正在构建和/或实施的更高级别流程感兴趣的人员所发生的事情。

客户/不是Oracle方面

正如我之前提到的,这里的一些代码是您可能会在您编写的任何Flow中添加的通用代码。我已经展示了所有内容,因此对于正在发生的事情没有任何歧义,但我只会扩展需要突出显示的点,因为它们包含特定于与Oracle交互的代码。

@InitiatingFlow
@StartableByRPC
class  GiveAwayStockFlow(
私有 val  符号:String,
私人 VAL  量:龙,
private  val  recipient:String
):
FlowLogic < SignedTransaction >(){
 
@Suspendable
覆盖 好玩 调用():SignedTransaction {
val  recipientParty  =  party()
val  oracle  =  oracle()
val  transaction  =
collectRecipientSignature(
verifyAndSign(transaction(recipientParty,oracle)),
recipientParty
val  allSignedTransaction  =  collectOracleSignature(transaction,oracle)
返回 子流(FinalityFlow(allSignedTransaction))
}
 
私人 乐趣 派对():派对 =
serviceHub。networkMapCache。getPeerByLegalName(CordaX500Name。解析(收件人))
?:抛出 IllegalArgumentException(“Party不存在”)
 
private  fun  oracle():Party  =  serviceHub。networkMapCache。getPeerByLegalName(
CordaX500Name(
“甲骨文”,
“伦敦”,
“国标”
?:throw  IllegalArgumentException(“Oracle不存在”)
 
@Suspendable
私人 乐趣 collectRecipientSignature(
transaction:SignedTransaction,
派对:派对
):SignedTransaction {
val  signature  =  subFlow(
CollectSignatureFlow(
交易,
initiateFlow(派对),
派对。owningKey
)。单()
退货 交易。withAdditionalSignature(签名)
}
 
private  fun  VerifyAndSign(transaction:TransactionBuilder):SignedTransaction {
交易。验证(serviceHub)
返回 serviceHub。signInitialTransaction(事务)
}
 
私人 有趣的 交易(recipientParty:Party,oracle:Party):TransactionBuilder  =
TransactionBuilder(notary())。申请 {
val  priceOfStock  =  priceOfStock()
addOutputState(状态(recipientParty,priceOfStock),StockContract。CONTRACT_ID)
addCommand(
GiveAway(symbol,priceOfStock),
listOf(recipientParty,oracle)。地图(Party :: owningKey)
}
 
私人 乐趣 priceOfStock():双 =
serviceHub。cordaService(StockRetriever :: 类。java的)。getCurrent(符号)。价钱
 
私人 娱乐 状态(派对:派对,priceOfStock:Double):StockGiftState  =
StockGiftState(
符号 =  符号,
金额 =  金额,
价格 =  priceOfStock  *  金额,
收件人 =  派对
 
private  fun  notary():Party  =  serviceHub。networkMapCache。公证身份。第一个()
 
@Suspendable
私人 乐趣 collectOracleSignature(
transaction:SignedTransaction,
oracle:派对
):SignedTransaction {
val  filtered  =  filteredTransaction(transaction,oracle)
val  signature  =  subFlow(CollectOracleStockPriceSignatureFlow(oracle,filtered))
退货 交易。withAdditionalSignature(签名)
}
 
私人 乐趣 过滤交易(
transaction:SignedTransaction,
oracle:派对
):FilteredTransaction  =
交易。buildFilteredTransaction(Predicate {
何时(它){
是 Command <*>  - >  oracle。owningKey  于 它。签名者 &&  它。价值 是 GiveAway
否则 - >  假
}
})
}
 
@InitiatedBy(GiveAwayStockFlow :: class)
class  SendMessageResponder(val  session:FlowSession):FlowLogic < Unit >(){
@Suspendable
覆盖 有趣的 call(){
subFlow(object:SignTransactionFlow(session){
覆盖 乐趣 checkTransaction(STX:SignedTransaction){}
})
}
}

 

首先,让我们看一下如何构建事务:

私人 有趣的 交易(recipientParty:Party,oracle:Party):TransactionBuilder  =
TransactionBuilder(notary())。申请 {
val  priceOfStock  =  priceOfStock()
addOutputState(状态(recipientParty,priceOfStock),StockContract。CONTRACT_ID)
addCommand(
GiveAway(symbol,priceOfStock),
listOf(recipientParty,oracle)。地图(Party :: owningKey)
}
 
私人 乐趣 priceOfStock():双 =
serviceHub。cordaService(StockRetriever :: 类。java的)。getCurrent(符号)。价钱

 

这里与创建不涉及Oracle的事务的方式没有太大的不同。唯一的两个区别是从外部源(隐藏在StockRetriever服务中)检索股票价格,并在命令中包含Oracle的签名。这些代码添加与使用Oracle的原因排列在一起。外部数据包含在事务中,Oracle需要验证它是否正确。为了证明Oracle认为交易有效,我们需要签名。

我们将仔细研究分别检索外部数据。

接下来是收集收件人签名:

@Suspendable
私人 乐趣 collectRecipientSignature(
transaction:SignedTransaction,
派对:派对
):SignedTransaction {
val  signature  =  subFlow(
CollectSignatureFlow(
交易,
initiateFlow(派对),
派对。owningKey
)。单()
退货 交易。withAdditionalSignature(签名)
}

 

收集交易对手的签名并不是Flow的一个不常见的步骤,但这里做的不同的是使用CollectSignatureFlow而不是CollectSignaturesFlow,通常使用的(注意中间缺少“s”)。这是因为要求Oracle在交易中签名。调用CollectSignaturesFlow遗嘱将从所有必需的签名者(包括Oracle)中检索签名。这将Oracle视为“正常”参与者。这不是我们想要的。相反,我们需要单独手动获取收件人和Oracle的签名。手册部分是使用transaction.withAdditionalSignature

现在收件人已签署了该事务,Oracle需要对其进行签名:

@Suspendable
私人 乐趣 collectOracleSignature(
transaction:SignedTransaction,
oracle:派对
):SignedTransaction {
val  filtered  =  filteredTransaction(transaction,oracle)
val  signature  =  subFlow(CollectOracleStockPriceSignatureFlow(oracle,filtered))
退货 交易。withAdditionalSignature(签名)
}
 
私人 乐趣 过滤交易(
transaction:SignedTransaction,
oracle:派对
):FilteredTransaction  =
交易。buildFilteredTransaction(Predicate {
何时(它){
是 Command <*>  - >  oracle。owningKey  于 它。签名者 &&  它。价值 是 GiveAway
否则 - >  假
}
})

 

在将事务发送到Oracle之前,建议对其进行过滤以删除Oracle不需要的任何信息。这可以防止Oracle看不到共享的信息。请记住,Oracle很可能是由另一个组织控制的节点,并不是您尝试与其共享状态和事务的参与者。

SignedTransaction提供buildFilteredTransaction仅包含与传入的谓词匹配的对象的函数。在上面的示例中,它过滤掉除GiveAway(我创建的命令)命令之外的所有命令,该命令还必须将Oracle作为签名者。

这输出一个FilteredTransaction,传递给CollectOracleStockPriceSignatureFlow

@InitiatingFlow
class  CollectOracleStockPriceSignatureFlow(
私人 val  oracle:派对,
private  val  filtered:FilteredTransaction
):FlowLogic < TransactionSignature >(){
 
@Suspendable
覆盖 有趣的 call():TransactionSignature {
val  session  =  initiateFlow(oracle)
回归 会议。sendAndReceive < TransactionSignature >(已过滤)。展开 { it }
}
}

 

所有这些代码都是发送FilteredTransaction给Oracle并等待其签名。这里的代码可以放入主Flow中,但是在我们可以的时候将代码拆分出来是非常好的。

最后,TransactionSignature从Oracle返回的内容与之前添加收件人签名的方式相同。此时,事务已准备就绪,因为所有必需的签名者都已完成了自己的工作。

Oracle Side

现在我们已经介绍了代码的客户端,我们需要了解Oracle如何验证事务。以下是Oracle代码的内容:

@InitiatedBy(CollectOracleStockPriceSignatureFlow :: class)
class  OracleStockPriceSignatureResponder(private  val  session:FlowSession):FlowLogic < Unit >(){
 
@Suspendable
覆盖 有趣的 call(){
val  transaction  =  session。接收< FilteredTransaction >()。展开 { it }
 
val  key  =  key()
 
val  isValid  =  transaction。checkWithFun { element:Any  - >
当 {
element  是 Command <*>  &&  element。值 是 GiveAway  - > {
val  command  =  element。作为GiveAway的价值  
(关键 在 元素。签名者)。还 {
validateStockPrice(
命令。符号,
命令。价钱
}
}
否则 - >  假
}
}
 
if(isValid){
会议。发送(serviceHub。createSignature(交易,键))
} else {
抛出 InvalidStockPriceFlowException(“Transaction:$ {transaction.id}无效”)
}
}
 
private  fun  key():PublicKey  =  serviceHub。myInfo。法律身份。第一个()。owningKey
 
私人 有趣的 validateStockPrice(符号:字符串,价格:双倍)=  try {
serviceHub。cordaService(StockPriceValidator :: 类。java的)。验证(符号,价格)
} catch(e:IllegalArgumentException){
扔 InvalidStockPriceFlowException(Ë。消息)
}
}

 

应该在这里的一些代码隐藏在其中StockPriceValidator,它检索外部股票价格并将其与传递给Oracle的股票价格进行比较。它没有太多的代码,它的验证是基本的,所以我不会详细说明。由于它很短,我现在可以展示它:

@CordaService
类 StockPriceValidator(私人 VAL  serviceHub:AppServiceHub):
SingletonSerializeAsToken(){
 
有趣的 验证(符号:字符串,价格:双倍)=
serviceHub。cordaService(StockRetriever :: 类。java的)。getCurrent(符号)。让 {
要求(价格 ==  它。价格){ “$符号的价格是$ {it.price},而不是$ price” }
}
}

 

回到OracleStockPriceSignatureResponder。首先,receive调用以获取FilteredTransaction客户端发送的内容。然后使用其checkWithFun功能进行检查。这是一个方便的函数,它查看每个对象并期望得到Boolean回报。使用此方法,如果事务包含的是GiveAwayOracle作为签名者的命令,并且最重要的是检查命令中包含的外部数据是否正确,则该事务被视为有效。如果您回忆起之前的代码,则会传入正确的命令和签名者。唯一剩下的验证是在外部数据上。如果一切正常,那么Oracle将接受该事务并将其签名发送回请求它的客户端。

我选择通过抛出异常(以及错误消息)来完成验证,然后将异常传播到请求方。我认为这样可以更容易地理解出现了什么问题,因此可以正确处理,而不仅仅是直接的“验证失败”消息。如果Oracle执行的验证很复杂,则这些错误消息会变得更有价值。

检索外部数据

您应该已经看到StockRetriever该类现在弹出两次。它在请求方和Oracle中都使用过。我已在两种类型的节点(普通节点和Oracles)之间共享此代码,但这可能不适合您自己的用例。此外,您如何选择检索外部数据取决于您,我只是提供了一个可能的解决方案。

代码可以在下面找到:

@CordaService
类 StockRetriever(serviceHub:AppServiceHub):
SingletonSerializeAsToken(){
 
private  val  client  =  OkHttpClient()
private  val  mapper  =  ObjectMapper()
 
fun  getCurrent(symbol:String):Stock {
val  response  =  client。newCall(请求(符号))。执行()
返回 响应。身体()?。让 {
val  json  =  它。string()
require(json  !=  “Unknown symbol”){ “带符号的股票:$符号不存在” }
val  tree  =  mapper。readTree(json)
股票(
符号 =  符号,
name  =  tree [ “companyName” ]。asText(),
primaryExchange  =  tree [ “primaryExchange” ]。asText(),
price  =  tree [ “latestPrice” ]。asDouble()
} ?:throw  IllegalArgumentException(“No response”)
}
 
私人 有趣的 请求(符号:字符串)=
请求。Builder()。url(“https://api.iextrading.com/1.0/stock/$symbol/quote”)。构建()
}

 

StockRetriever是一个很好的小服务,它使用OkHttpClient(OkHttp)向API发出HTTP请求(由IEX Trading使用他们的Java库提供),当提供股票代码时返回股票信息。您可以使用您想要发出HTTP请求的任何客户端。我在一个CorDapp示例中看到了这个,并且已经把它拿走了。就个人而言,我已经习惯了春天,所以除了他们之外,我真的不认识任何客户RestTemplate

返回响应后,它将转换为Stock对象并传递回函数的调用者。这就是所有人。

结论

总之,当CorDapp需要经常更改需要在提交事务之前验证的外部数据时,您应该使用Oracle。就像状态中保存的数据一样,外部数据非常重要,可能是最重要的,因为它可能决定事务的主要内容。因此,所有参与者都必须感到舒适,因为数据是正确的并且不仅仅是凭空而来。为此,Oracle还将检索外部数据并根据事务所说的数据进行验证。此时,Oracle将签署事务或抛出异常并认为它无效。由于没有太多步骤需要采取,因此实施方面相当简单。检索数据,发送一个FilteredTransaction到包含要验证的数据的Oracle。是的,正如你已经阅读过这篇文章,你会知道还有更多内容。但是,对于一个基本的流程,这就是它。正如我在开始附近所说的那样,Oracle如何进行验证可以根据需要简单或复杂。虽然,我认为大多数人会遵循此处所示的相同过程。

现在,为了得出主要结论,你现在有了回答关于神谕的Slack频道的问题的知识,或者知道如果你不能发送它们的地方!