Attachmentを用いたFlowとContract作成
1. AttachmentをUploadする
uploadBlacklist タスクを実行して共有したいNodeにAttachmentを共有
具体的には以下のコードをgradle タスクから実行することでAttachmentをrpc経由で全てのnodeにアップロードする
code:build.gradle
task uploadBlacklist(type: JavaExec, dependsOn: compileTestKotlin) {
classpath = sourceSets.test.runtimeClasspath
main = 'net.corda.examples.attachments.tests.client.ClientKt'
args 'localhost:10007', 'localhost:10010', 'localhost:10013'
}
code:Client.kt
package net.corda.examples.attachments.tests.client
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.crypto.SecureHash
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.NetworkHostAndPort.Companion.parse
import net.corda.core.utilities.loggerFor
import net.corda.examples.attachments.ATTACHMENT_EXPECTED_CONTENTS
import net.corda.examples.attachments.ATTACHMENT_FILE_NAME
import net.corda.examples.attachments.BLACKLIST_JAR_PATH
import org.slf4j.Logger
import java.io.File
import java.util.jar.JarInputStream
/**
* Uploads the jar of blacklisted counterparties with whom agreements cannot be struck to the node.
*/
fun main(args: Array<String>) {
UploadBlacklistClient().main(args)
}
private class UploadBlacklistClient {
companion object {
val logger: Logger = loggerFor<UploadBlacklistClient>()
}
fun main(args: Array<String>) {
require(args.isNotEmpty()) { "Usage: uploadBlacklist <node address 1> <node address 2> ..." }
args.forEach { arg ->
val nodeAddress = parse(arg)
val rpcConnection = CordaRPCClient(nodeAddress).start("user1", "test")
val proxy = rpcConnection.proxy
val attachmentHash = uploadAttachment(proxy, BLACKLIST_JAR_PATH)
logger.info("Blacklist uploaded to node at $nodeAddress")
val attachmentJar = downloadAttachment(proxy, attachmentHash)
logger.info("Blacklist downloaded from node at $nodeAddress")
checkAttachment(attachmentJar, ATTACHMENT_FILE_NAME, ATTACHMENT_EXPECTED_CONTENTS)
logger.info("Attachment contents checked on node at $nodeAddress")
rpcConnection.notifyServerAndClose()
}
}
}
/**
*/
private fun uploadAttachment(proxy: CordaRPCOps, attachmentPath: String): SecureHash {
val attachmentUploadInputStream = File(attachmentPath).inputStream()
return proxy.uploadAttachment(attachmentUploadInputStream)
}
/**
*/
private fun downloadAttachment(proxy: CordaRPCOps, attachmentHash: SecureHash): JarInputStream {
val attachmentDownloadInputStream = proxy.openAttachment(attachmentHash)
return JarInputStream(attachmentDownloadInputStream)
}
/**
*/
private fun checkAttachment(attachmentJar: JarInputStream, expectedFileName: String, expectedContents: List<String>) {
var name = attachmentJar.nextEntry.name
while (name != expectedFileName) {
name = attachmentJar.nextEntry.name
}
val contents = attachmentJar.bufferedReader().readLines()
if (contents != expectedContents) {
throw IllegalArgumentException("Downloaded JAR did not have the expected contents.")
}
}
code:Constraints.kt
package net.corda.examples.attachments
const val BLACKLIST_JAR_PATH = "src/main/resources/blacklist.jar"
const val ATTACHMENT_FILE_NAME = "blacklist.txt"
val ATTACHMENT_EXPECTED_CONTENTS = listOf(
"Crossland Savings",
"TCF National Bank Wisconsin",
"George State Bank",
"The James Polk Stone Community Bank",
"Tifton Banking Company")
このサンプル実装では、Attachmentとしてuploadするblacklist.jarのpath指定をベタ書きしているが、本来はなんらかの方法で指定して渡したいところ
etaro.icon > TXとは関係ないところでjarファイル自体は渡してるのがポイント
このjarのSecureHashのみTXにAttachする
2. Flow でTXにAttachmentを付与
FlowでAttachmentのHashを引数にとり、それをTXにaddAttachmentしてTX送信
TransactionBuilderの.addAttachment()メソッドで 1でuploadしたjarファイルのsecureHashをTXのattachmentに入れる
具体的には以下のようなflowの実装を行う
code:ReachAgreementFlow.kt
@InitiatingFlow
@StartableByRPC
class ProposeFlow(private val agreementTxt: String,
private val untrustedPartiesAttachment: SecureHash,
private val counterparty: Party) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryIdentities.first()
val agreementState = AgreementState(ourIdentity, counterparty, agreementTxt)
val agreeCmd = AgreementContract.Commands.Agree()
val agreeCmdRequiredSigners = listOf(ourIdentity.owningKey, counterparty.owningKey)
val txBuilder = TransactionBuilder(notary)
.addOutputState(agreementState, AGREEMENT_CONTRACT_ID)
.addCommand(agreeCmd, agreeCmdRequiredSigners)
.addAttachment(untrustedPartiesAttachment)
val partSignedTx = serviceHub.signInitialTransaction(txBuilder)
val counterpartySession = initiateFlow(counterparty)
val signedTx = subFlow(CollectSignaturesFlow(partSignedTx, listOf(counterpartySession)))
return subFlow(FinalityFlow(signedTx, listOf(counterpartySession)))
}
}
3. Contract の中でTXのattachmentを取り出してvalidation する
val nonContractAttachments = tx.attachments.filter { it !is ContractAttachment }で Contract.jar以外のAttachmentのSecureHashを取得
contract.jarのsecureHashはデフォルトでTXにattachされる(?)
val attachment = nonContractAttachments.single()
val attachmentJar = attachment.openAsJAR()で中身のdataを取得
attachmentJar.nextEntry.name 、 attachmentJar.bufferedReader().readline()のような形で使用する
具体的なContract実装は以下の通り
code:Contract.kt
package net.corda.examples.attachments.contract
import net.corda.core.contracts.Contract
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.TypeOnlyCommandData
import net.corda.core.contracts.requireThat
import net.corda.core.crypto.SecureHash
import net.corda.core.transactions.LedgerTransaction
import net.corda.examples.attachments.state.AgreementState
open class AgreementContract : Contract {
companion object {
const val AGREEMENT_CONTRACT_ID = "net.corda.examples.attachments.contract.AgreementContract"
val BLACKLIST_JAR_HASH = SecureHash.parse("4CEC607599723D7E0393EB5F05F24562732CD1B217DEAEDEABD4C25AFE5B333A")
}
override fun verify(tx: LedgerTransaction) = requireThat {
// Constraints on the inputs, outputs and commands.
"The transaction should have no inputs" using (tx.inputs.isEmpty())
"The transaction should have an AgreementState output" using (tx.outputsOfType<AgreementState>().size == 1)
"The transaction should have no other outputs" using (tx.outputs.size == 1)
"The transaction should have an Agree command" using (tx.commandsOfType<Commands.Agree>().size == 1)
"The transaction should have no other commands" using (tx.commands.size == 1)
// Constraints on the included attachments.
val nonContractAttachments = tx.attachments.filter { it !is ContractAttachment }
"The transaction should have a single non-contract attachment" using (nonContractAttachments.size == 1)
val attachment = nonContractAttachments.single()
// TODO: Switch to constraint on the jar's signer.
// In the future, Corda will support the signing of jars. We will then be able to restrict
// the attachments used to just those signed by party X.
"The jar's hash should be correct" using (attachment.id == BLACKLIST_JAR_HASH)
// We extract the blacklisted company names from the JAR.
val attachmentJar = attachment.openAsJAR()
while (attachmentJar.nextEntry.name != "blacklist.txt") {
// Calling attachmentJar.nextEntry causes us to scroll through the JAR.
}
val blacklistedCompanies = mutableListOf<String>()
val bufferedReader = attachmentJar.bufferedReader()
var company = bufferedReader.readLine()
while (company != null) {
blacklistedCompanies.add(company)
company = bufferedReader.readLine()
}
// Constraints on the blacklisted parties.
val agreement = tx.outputsOfType<AgreementState>().single()
val participants = agreement.participants
val participantsOrgs = participants.map { it.name.organisation }
val overlap = blacklistedCompanies.toSet().intersect(participantsOrgs)
"The agreement involved blacklisted parties: $overlap" using (overlap.isEmpty())
// Constraints on the signers.
val command = tx.commands.single()
val participantKeys = participants.map { it.owningKey }
"All the parties to the agreement are signers" using (command.signers.containsAll(participantKeys))
}
interface Commands {
class Agree : TypeOnlyCommandData(), Commands
}
}
blacklist cordappを実行してみる
1. ./gradlew uploadBlacklistでblacklist.jarをupload
2. tx実行
Monogram Bankからstart ProposeFlow agreementTxt: "A and B agree Y", counterparty: "Hiseville Deposit Bank", untrustedPartiesAttachment: "4CEC607599723D7E0393EB5F05F24562732CD1B217DEAEDEABD4C25AFE5B333A" を実行
このTXは双方がblacklistに含まれていないため以下のようにOK
https://gyazo.com/762d30373a7fa0c6a525705c19dc1904
Monogram Bankからstart ProposeFlow agreementTxt: "A and B agree Y", counterparty: "George State Bank", untrustedPartiesAttachment: "4CEC607599723D7E0393EB5F05F24562732CD1B217DEAEDEABD4C25AFE5B333A"
こちらのTX は"George State Bank"がblacklist.jarに含まれているため 以下のようにContract validationで弾かれる
https://gyazo.com/0e2ecc48f1f4eee554ab540a7ad82231
George State Bankからstart ProposeFlow agreementTxt: "A and B agree Y", counterparty: "Hiseville Deposit Bank", untrustedPartiesAttachment: "4CEC607599723D7E0393EB5F05F24562732CD1B217DEAEDEABD4C25AFE5B333A" を実行
こちらのTX も"George State Bank"がblacklist.jarに含まれているため 以下のようにContract validationで弾かれる
https://gyazo.com/ef7293f431bcb92e8403af46cc5b0f11