Статьи

Проверка внешних данных с помощью Oracle

Я довольно много общаюсь на канале Corda Slack и стараюсь отвечать на вопросы, когда могу. Разумное количество вопросов, на которые я пытался ответить, связано с оракулами. Более конкретно, когда использовать один. Я чувствую, что могу ответить на это: «Используйте Oracle, когда вам нужно проверять внешние данные, которые могут часто меняться». Я вероятно написал ответ, подобный тому в некоторый момент. Что я не мог сделать, хотя … Был кто-то сказать, как реализовать один. Поэтому исправить это. Я написал этот пост, чтобы узнать, как реализовать его самому и поделиться этими знаниями с вами и моим будущим я.

Когда использовать Oracle

Давайте начнем с того, когда рассмотрим, когда использовать Oracle. Как я упомянул минуту назад, вы должны использовать Oracle, когда вам нужно проверять внешние данные, которые могут часто меняться. Это могут быть данные, такие как курсы валют, цены на акции или даже то, работает ли мой блог в настоящее время или нет (хотя я еще не видел его вниз!). Я думаю, что часто важная часть здесь важна. Если данные редко изменяются, может оказаться целесообразным проверить некоторые данные по вложению, содержащему значения такого же типа, которые Oracle будет извлекать самостоятельно. Вот почему проверка данных, таких как обменные курсы, по моему мнению, должна выполняться только Oracle. Все это, как говорится, действительно сводится к вашему конкретному варианту использования.

Как использовать Oracle

Как Oracle выполняет эту проверку? Ну, это зависит от вас. Но это, вероятно, будет следовать этим шагам:

  • Получать данные из узла
  • Получить внешние данные
  • Проверять полученные данные на соответствие внешним данным
  • Предоставить подпись для транзакции

Это те шаги, которые, я думаю, будут включать большинство реализаций Oracle. Можно добавить больше шагов, и выполненная проверка может быть такой же сложной или простой, как того требует сценарий использования. Хотя можно было бы добавить больше шагов, я действительно сомневаюсь, что Oracle будет очень полезен, если исключить какие-либо из шагов, показанных выше.

Все шаги, показанные выше, показывают процесс только со стороны Oracle. Происходит немного больше, поэтому я думаю, что хорошая диаграмма поможет нам здесь. Также будет представлен пример, который я буду использовать для этого поста.

Диаграмма последовательности, показывающая процесс взаимодействия с Oracle

Многие из этих шагов являются общими, которые будут выполнены в любом потоке, который вы создадите вместе. В этом разделе я расширю и покажу код, задействованный для реализации потока, показанного на диаграмме. Поэтому стоит взглянуть на это разумно… Я также потратил много времени, чтобы это выглядело хорошо, поэтому, пожалуйста, посмотрите на это!

О, еще один момент, прежде чем я продолжу. Я хочу подчеркнуть, насколько полезно составление диаграмм последовательности для модели Corda Flows. Это действительно подчеркивает, кто вовлечен, сколько сетевых прыжков необходимо сделать и сколько работы выполняет каждый участник. Кроме того, они дают хороший способ объяснить, что происходит людям, которые заинтересованы только в процессах более высокого уровня, которые вы разрабатываете и / или внедряете.

Клиент / Не сторона Oracle

Как я упоминал ранее, часть кода здесь является общим кодом, который вы, вероятно, вставите в любой написанный вами поток. Я показал все это, чтобы не было двусмысленности в отношении того, что происходит, но я остановлюсь лишь на тех моментах, которые необходимо выделить, поскольку они содержат код, специфичный для взаимодействия с Oracle.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
@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) {}
    })
  }
}

Во-первых, давайте посмотрим, как строится транзакция:

01
02
03
04
05
06
07
08
09
10
11
12
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 должен проверить ее правильность. Чтобы доказать, что Oracle признал транзакцию действительной, нам нужна ее подпись.

Мы рассмотрим подробнее получение внешних данных отдельно.

Далее идет сбор подписи получателей:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Suspendable
private fun collectRecipientSignature(
  transaction: SignedTransaction,
  party: Party
): SignedTransaction {
  val signature = subFlow(
    CollectSignatureFlow(
      transaction,
      initiateFlow(party),
      party.owningKey
    )
  ).single()
  return transaction.withAdditionalSignature(signature)
}

Сбор подписи контрагента на самом деле не является необычным шагом потока, но здесь делается иначе — это использование CollectSignatureFlow а не CollectSignaturesFlow который обычно используется (заметил, что «s» отсутствует в середине). Это связано с требованием подписи Oracle в транзакции. Вызов CollectSignaturesFlow сработает для получения подписей от всех необходимых подписантов, включая Oracle. Это относится к Oracle как к «нормальному» участнику. Это не то, что мы хотим. Вместо этого нам нужно получить подпись получателя и Oracle по отдельности и несколько вручную. Ручная часть — это использование transaction.withAdditionalSignature .

Теперь, когда получатель подписал транзакцию, Oracle должен подписать ее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
@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 :

01
02
03
04
05
06
07
08
09
10
11
12
@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 и ожидает его подписи. Код здесь можно было бы поместить в основной поток, но было бы неплохо разделить код, когда мы сможем.

Наконец, TransactionSignature возвращаемая из Oracle, добавляется в транзакцию так же, как ранее была добавлена ​​подпись получателя. На этом этапе транзакция готова к принятию, так как все необходимые подписчики сделали свое дело.

Сторона Oracle

Теперь, когда мы рассмотрели клиентскую часть кода, нам нужно посмотреть, как Oracle проверяет транзакцию. Ниже приведено содержание кода Oracle:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@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. В нем не так много кода, и его проверка является базовой, поэтому я не буду подробно останавливаться на этом. Поскольку это коротко, я мог бы также показать это теперь:

1
2
3
4
5
6
7
8
9
@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 функции checkWithFun . Это удобная функция, которая просматривает каждый объект и ожидает Boolean в ответ. Используя это, транзакция считается действительной, если все, что она содержит, это команды GiveAway где Oracle является подписывающим лицом и, что наиболее важно, проверяет правильность внешних данных, содержащихся в команде. Если вы помните код из ранее, правильная команда и подписчики были переданы. Единственная оставшаяся проверка — на внешних данных. Если все в порядке, то Oracle примет транзакцию и отправит свою подпись обратно клиенту, который ее запросил.

Я решил завершить проверку с помощью исключения (вместе с сообщениями об ошибках), которые затем передаются запрашивающей стороне. Я думаю, что это облегчает понимание того, что пошло не так, так что это может быть обработано должным образом, а не просто сообщение «проваленная проверка». Если проверка, которую выполняет Oracle, является сложной, эти сообщения об ошибках становятся еще более ценными.

Получение внешних данных

Вы бы уже видели, как класс StockRetriever дважды. Он использовался как в запрашивающей стороне, так и в Oracle. Я поделился этим кодом между обоими типами узлов (обычными узлами и оракулами), но он может не подойти для вашего собственного варианта использования. Кроме того, как вы решите получить ваши внешние данные, зависит только от вас, я просто предлагаю возможное решение.

Код можно найти ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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 ) для отправки HTTP-запроса к API (предоставленному IEX Trading с использованием их библиотеки Java ), который возвращает информацию об OkHttpClient когда предоставляется символ акции. Вы можете использовать любой клиент, который вы хотите сделать HTTP-запрос. Я видел это в примере CorDapp и взял его для себя. Лично я слишком привык к Spring, поэтому на самом деле не знал никаких клиентов, кроме их RestTemplate .

Как только ответ возвращается, он преобразуется в объект Stock и передается обратно вызывающей функции. Это все, ребята.

Вывод

В заключение вам следует использовать Oracle, когда вашему CorDapp требуется часто меняющиеся внешние данные, которые необходимо проверить, прежде чем транзакция может быть зафиксирована. Как и данные, хранящиеся в состояниях, внешние данные чрезвычайно важны, возможно, наиболее важны, так как они могут определять основное содержание транзакции. Поэтому все участники должны чувствовать себя комфортно, чтобы данные были правильными и не просто появились из воздуха. Чтобы достичь этого, Oracle также извлечет внешние данные и проверит их на соответствие транзакции, согласно которой эти данные должны быть. В этот момент Oracle либо подпишет транзакцию, либо сгенерирует исключение и сочтет его недействительным. Реализация этого подхода достаточно проста, так как не нужно предпринимать много шагов. Получите данные, отправьте FilteredTransaction в Oracle, содержащий данные, где они будут проверены. Да, прочитав этот пост, вы узнаете, что в нем есть что-то еще. Но для базового потока это в значительной степени так. Как я уже сказал где-то в начале, то, как Oracle выполняет свою проверку, может быть настолько простым или сложным, насколько это необходимо. Хотя, я думаю, что большинство будет следовать тому же процессу, показанному здесь.

Теперь о главном выводе … В заключение, у вас теперь есть знания, чтобы отвечать на вопросы в медленном канале о оракулах или знать, куда их отправлять, если вы не можете!

Код, используемый в этом посте, можно найти на моем GitHub .

Если вы нашли этот пост полезным, вы можете подписаться на меня в Twitter на @LankyDanDev, чтобы не отставать от моих новых сообщений.

Опубликовано на Java Code Geeks с разрешения Дэна Ньютона, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Проверка внешних данных с помощью Oracle

Мнения, высказанные участниками Java Code Geeks, являются их собственными.