Статьи

Проверка договора с данными CSV

Вложения в Corda могут быть не просто PDF-файлами, отправленными вместе с транзакцией. Они могут фактически использоваться программно при запуске потока или даже внутри функции verify контракта. Зачем тебе это делать? ? Ответ имеет большой смысл, когда вы думаете об этом. Давайте возьмем в качестве примера вложение, содержащее данные CSV. Собственно, это то, о чем этот пост. Так или иначе. Вложение может содержать все действительные идентификаторы (или любые другие), которые может иметь состояние. Теперь, это может быть сделано в вашем коде, но это не практично для системы, которая должна меняться со временем. Возможно, есть новые идентификаторы, которые необходимо добавить. Если они живут внутри кода, обновленный CorDapp необходимо компилировать и распространять всякий раз, когда разрешены новые значения. Не совсем практично. Но загрузка новой версии файла, содержащего данные, позволяет проверке изменяться со временем без необходимости перекомпиляции. Это идея, которая имеет смысл ?.

Теперь, когда мы согласны с тем, что проверка содержимого транзакции по данным вложения — это хорошая идея. Следующий вопрос — где лучше всего поставить эту проверку. Как упоминалось ранее, есть два варианта. Внутри потока или в договоре. Внедрение этого в контракт имеет смысл, поскольку все стороны, получающие транзакцию, должны выполнить проверку вложения. Гарантируя, что все они достигнут консенсуса относительно действительности сделки ?.

Знание того, что вы можете использовать вложения для проверки контракта — это здорово, и все, но есть еще какой-то код, который нужно написать, прежде чем он станет реальностью. Не беспокойся, я понял тебя.

Построение транзакции с приложением

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
private fun transaction(): TransactionBuilder =
  TransactionBuilder(notary()).apply {
    val attachmentId = attachment()
    addOutputState(message)
    addCommand(Command(Send(attachmentId), message.participants.map(Party::owningKey)))
    addAttachment(attachmentId)
  }
 
private fun attachment(): AttachmentId {
  return serviceHub.attachments.queryAttachments(
    AttachmentQueryCriteria.AttachmentsQueryCriteria(
      filenameCondition = Builder.equal(
        attachmentName
      )
    )
  ).first()
}

Добавление вложения в транзакцию не включает какой-либо причудливый код. Извлечение вложения немного сложнее, но собрать его тоже не сложно. В этом примере вложение запрашивается по имени, а возвращенный AttachmentId затем передается в addAttachment (он принимает AttachmentId не само вложение). Использование имени вложения — это мое предпочтение, но также возможна передача в AttachmentId , если вы знаете это заранее.

Есть еще один хитрый кусок кода, который вы, возможно, не уловили. Я передал AttachmentId в команду « Send . Это позволяет узнать хэш вложения и значительно упростит его извлечение из транзакции. Я быстро покажу вам, как я это сделал:

01
02
03
04
05
06
07
08
09
10
11
class MessageContract : Contract {
 
  interface Commands : CommandData {
    class Send(attachmentId: AttachmentId) : CommandWithAttachmentId(attachmentId), Commands
  }
 
  abstract class CommandWithAttachmentId(val attachmentId: AttachmentId) : CommandData {
    override fun equals(other: Any?) = other?.javaClass == javaClass
    override fun hashCode() = javaClass.name.hashCode()
  }
}

Проверка договора

Вот где происходит магия. Используя вложение, ранее добавленное к транзакции, данные внутри него могут быть извлечены и использованы для проверки состояний в транзакции. Ниже приведена функция verify контракта:

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
override fun verify(tx: LedgerTransaction) {
  val command = tx.commands.requireSingleCommand<Commands>()
  when (command.value) {
    is Commands.Send -> requireThat {
      "No inputs should be consumed when sending a message." using (tx.inputs.isEmpty())
      "Only one output state should be created when sending a message." using (tx.outputs.size == 1)
    }
    is Commands.Reply -> requireThat {
      "One input should be consumed when replying to a message." using (tx.inputs.size == 1)
      "Only one output state should be created when replying to a message." using (tx.outputs.size == 1)
    }
  }
  require(isMessageInCsv(tx)) {
    "The output message must be contained within the csv of valid messages. " +
            "See attachment with hash = ${tx.attachments.first().id} for its contents"
  }
}
 
private fun isMessageInCsv(tx: LedgerTransaction): Boolean {
  val message = tx.outputsOfType<MessageState>().first()
  val attachmentId = tx.commandsOfType<CommandWithAttachmentId>().single().value.attachmentId
  return tx.getAttachment(attachmentId).openAsJAR().use { zipInputStream: JarInputStream ->
    zipInputStream.nextJarEntry.name
    val csv = CSVFormat.DEFAULT.withHeader("valid_messages")
      .withFirstRecordAsHeader()
      .parse(InputStreamReader(zipInputStream))
    csv.records.any { row -> row.get("valid messages") == message.contents }
  }
}

Интересный контент в этом примере находится внутри функции isMessageInCsv . Это может выглядеть немного устрашающе, но, возможно, мне просто нужно немного привести в порядок ?… Забудь, что я сказал это ?. Это просто нуждается в небольшом объяснении (или, возможно, некоторые комментарии кода code).

Вложения могут быть извлечены из транзакции через свойство ее commands или через функцию getAttachment (занимая позицию или AttachmentId / SecureHash ). В этом примере вложение, содержащее данные CSV, извлекается с использованием getAttachment вместе с идентификатором, который ранее был передан в команду.

Вложение было получено этим пунктом. Теперь данные CSV внутри него должны быть проанализированы и сравнены с состояниями транзакции. Чтобы сделать это намного проще, я использовал Apache Commons CSV . Вложение открывается с использованием openAsJAR (вложение сохраняется в виде zip- openAsJAR ), а библиотека используется для чтения данных внутри него. Когда каждая строка читается, она проверяет, соответствует ли contents MessageState текущей строке. Если кто-нибудь сделает, то отлично. Государство проходит проверку, и договор считается действительным. Если ничего не найдено, выдается следующая ошибка, и вам нужно будет повторить попытку ?.

1
2
3
net.corda.core.contracts.TransactionVerificationException$ContractRejection: Contract verification failed:
The output message must be contained within the csv of valid messages. See attachment with hash = 3E3031BA98F3F01843E8FD0A1B34E21C599C9C8F09765C2F820E45D6E8770948 for its contents,
contract: com.lankydanblog.tutorial.contracts.MessageContract, transaction: 5C8963497E493684A78C0A95A30E4C30029E6C36A792DE8A1B1733DE5358BB15

Этот код предполагает, что содержимое CSV соответствует некоторому формату. Другими словами, "valid messages" жестко кодируются как заголовок в файле. Если его там нет, проверка становится немного бессмысленной. Это не удастся, если это произойдет, что на самом деле хорошо, но если вы вспомните то, что я сказал во введении. Это вводит более жесткое кодирование и снижает гибкость контракта. Если вы хотите улучшить это, вы можете передать имя строки, которую вы хотите использовать, в команду так же, как я добавил к ней AttachmentId ранее.

Завершение

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

Для суммирования шагов необходимо проверить транзакцию с некоторыми данными CSV:

  • Загрузить CSV на узел
  • Внутри потока найдите его и добавьте в транзакцию
  • Внутри договора возьмите CSV из транзакции и, наконец, сравните с ней содержание транзакции.

Это позволит со временем изменить вашу проверку без необходимости перекомпиляции вашего CorDapp. Это звучит здорово, верно? Я так думаю. Если вы этого не сделаете, то вы должны, потому что это определенно здорово ?.

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

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

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