Corda API: Transactions

原文地址:https://docs.corda.net/api-transactions.html

阅读本文之前,你应该对 Corda 核心概念 - Transactions 比较熟悉了。

Transaction 生命周期

从它被创建到最终被添加到账本中,每个 transaction 会大体占用 3种状态中的一种:

我们可以用下图来表示 transactions 在三个状态中的转换: Transaction Lifecycle

Transaction 组件

一个 transaction 包括六种类型的组件:

每个组件都对应于 Corda API 中的一个指定的类。下边的部分描述了每个组件的类,和他们是如何被创建的。

Input states

Input states 是以 StateAndRef 实例的形式添加进 transaction 的,它包括:

Output states

因为一个 transaction 的 output states 在 transaction 被最终提交前是不存在的,所以他们不能够被之前的 transaction 进行引用。相反,我们通过创建 ContractState 实例的方式创建想要的 output states,并直接把他们添加到 transaction 中:

val ourOutputState: DummyState = DummyState()

当一个 output 会作为一个 input 的更新版本的时候,我们可能会希望基于原始的这个 input state 来创建一个新的 output state:

val ourOtherOutputState: DummyState = ourOutputState.copy(magicNumber = 77)

当我们的 output state 在能够被添加到一个 transaction 之前,我们需要将它同一个 contract 关联起来。我们可以通过将这个 output state 放入一个 StateAndContract 中,它将下边两个元素整合在了一起:

Commands

一个 command 是做为 Command 实例被添加到一个 transaction 中的。Command 包含:

Time-windows

Time windows 代表了一个时间区间,transaction 必须要在这个时间区间内被公正。它可以有一个起始和终止时间,或者是一个开放的区间:

val ourTimeWindow: TimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX)
val ourAfter: TimeWindow = TimeWindow.fromOnly(Instant.MIN)
val ourBefore: TimeWindow = TimeWindow.untilOnly(Instant.MAX)

我们也可以定义一个包含一个 Instant 和正/负时间差的 time window(比如加/减 30 秒钟):

val ourTimeWindow2: TimeWindow = TimeWindow.withTolerance(serviceHub.clock.instant(), 30.seconds)

或者包含一个起始时间加上一个时间段:

val ourTimeWindow3: TimeWindow = TimeWindow.fromStartAndDuration(serviceHub.clock.instant(), 30.seconds)

TransactionBuilder

创建一个 builder

创建一个 transaction proposal 的第一步是实例化一个 TransactionBuilder

如果一个 transaction 包含 input states 或者一个 time-window 的话,我们需要实例化这个 builder 并且需要有一个关于 notary 的引用,这个 notary 会对 inputs 进行公正并且验证这个 time-window:

val txBuilder: TransactionBuilder = TransactionBuilder(specificNotary)

如果一个 transaction 没有任何的 input states 或者 time-window 的话,那就不需要指定 notary 来实例化了:

val txBuilderNoNotary: TransactionBuilder = TransactionBuilder()

添加 items

下一步就是通过添加期望的组件来构建 transaction。

我们可以使用 TransactionBuilder.withItems 方法来向 builder 中增加组件:

    /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
    fun withItems(vararg items: Any): TransactionBuilder {
        for (t in items) {
            when (t) {
                is StateAndRef<*> -> addInputState(t)
                is SecureHash -> addAttachment(t)
                is TransactionState<*> -> addOutputState(t)
                is StateAndContract -> addOutputState(t.state, t.contract)
                is ContractState -> throw UnsupportedOperationException("Removed as of V1: please use a StateAndContract instead")
                is Command<*> -> addCommand(t)
                is CommandData -> throw IllegalArgumentException("You passed an instance of CommandData, but that lacks the pubkey. You need to wrap it in a Command object first.")
                is TimeWindow -> setTimeWindow(t)
                is PrivacySalt -> setPrivacySalt(t)
                else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
            }
        }
        return this
    }

withItems 使用了一个由对象构成的 vararg,并根据他们的类型向 builder 中添加内容:

传入任何其他类型的对象将会造成一个 IllegalArgumentException 被抛出。

下边是一个如何使用 TransactionBuilder.withItems 的实例代码:

txBuilder.withItems(
        // Inputs, as ``StateAndRef``s that reference the outputs of previous transactions
        ourStateAndRef,
        // Outputs, as ``StateAndContract``s
        ourOutput,
        // Commands, as ``Command``s
        ourCommand,
        // Attachments, as ``SecureHash``es
        ourAttachment,
        // A time-window, as ``TimeWindow``
        ourTimeWindow
)

这里也有独立的方法来添加不同的组件。

添加 inputs 和 附件的方法:

txBuilder.addInputState(ourStateAndRef)
txBuilder.addAttachment(ourAttachment)

一个 output state 可以作为 ContractState,contract 类名和 notary 来添加:

txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary)

我们也可以将 notary 字段留空,那么 transaction 的默认 notary 就会被使用了:

txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID)

或者我们可以将一个 output state 作为 TransactionState 来添加,它已经指定了 output 的 contract 和 notary:

val txState: TransactionState<DummyState> = TransactionState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary)

Commands 可以作为 Command 被添加:

txBuilder.addCommand(ourCommand)

或者作为 CommandData 和一个 vararg PublicKey

txBuilder.addCommand(commandData, ourPubKey, counterpartyPubKey)

对于 time-window,我们可以直接设定 time-window:

txBuilder.setTimeWindow(ourTimeWindow)

或者将 time-window 定义为一个时间加上一个时间差(比如 45 秒钟):

txBuilder.setTimeWindow(serviceHub.clock.instant(), 45.seconds)

为 builder 签名

一旦 builder 准备好了,我们就可以通过签名的方式将它变为一个 SignedTransaction

我们可以使用我们的 legal identity key 来签名:

val onceSignedTx: SignedTransaction = serviceHub.signInitialTransaction(txBuilder)

或者也可以选择使用我们的另一个公钥(public key)来签名:

val otherIdentity: PartyAndCertificate = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
val onceSignedTx2: SignedTransaction = serviceHub.signInitialTransaction(txBuilder, otherIdentity.owningKey)

任何的方式,这个流程的输出都会是创建了一个带有我们签名的无法修改的 SignedTransaction

SignedTransaction

一个 SignedTransaction 是下边内容的组合:

确认 transaction 的内容

如果一个 transaction 含有 inputs 的话,在能够确认 transaction 的内容之前,我们需要取回这个 transaction 依赖的 transaction 链中的所有 states。这是因为只有当依赖链(transaction chain)是有效的时候,这个 transaction 才会被认为是有效的。我们可以通过向发起 transaction 的一方来请求任何在当前结点的本地存储中没有 states 来最终验证整个 transaction 依赖链。这个流程是由一个内置的名为 ReceiveTransactionFlow 的方法来处理的。

我们现在就可以验证 transaction 的内容来确保它的 input 和 output states 中的 contract code 中定义的约束都能满足:

twiceSignedTx.verify(serviceHub)

检查 transaction 满足合约约束(contract constraints)只是验证 transaction 内容的一部分。通常我们也会在提供签名前,希望进行我们自己指定的额外的验证,来确保 transaction proposal 是我们真正想加入的一个协议。

但是,SignedTransaction 将它的 inputs 以 StateRef 实例的形式保留,并且它的附件是作为 SecureHash 的实例,这并不能提供足够的信息来很好地验证 transaction 的内容。我们首先需要解决的是将 StateRefSecureHash 实例化为真正的 ContractStateAttachment 的实例,然后我们就可以检查了。

我们通过使用 ServiceHub 来将 SignedTransaction 转换为一个 LedgerTransaction

val ledgerTx: LedgerTransaction = twiceSignedTx.toLedgerTransaction(serviceHub)

我们现在就可以进行额外的验证了,下边是示例代码:

val outputState: DummyState = ledgerTx.outputsOfType<DummyState>().single()
if (outputState.magicNumber == 777) {
    // ``FlowException`` is a special exception type. It will be
    // propagated back to any counterparty flows waiting for a
    // message from this flow, notifying them that the flow has
    // failed.
    throw FlowException("We expected a magic number of 777.")
}

确认 transaction 的签名

除了确认 transaction 的内容是有效的,我们也要检查签名是有效的。一个建立在 transaction 的哈希值的基础上有效的签名能够防止记录被篡改。

我们可以验证该 transaction 需要的所有的签名都已经被提供了:

fullySignedTx.verifyRequiredSignatures()

然而,在所有的签名被搜集到之前,我们通常也会希望先确认 transaction 里已经有的签名。我们可以使用 SignedTransaction.verifySignaturesExcept,它带有一个公钥(public keys)的 vararg 传入参数,它会允许该公钥不需要提供签名:

onceSignedTx.verifySignaturesExcept(counterpartyPubKey)

这里还有一个对于 SignedTransaction.verifySignaturesExcept 的重载,它可以传入一个允许不提供签名的公钥(public keys)的集合:

onceSignedTx.verifySignaturesExcept(listOf(counterpartyPubKey))

如果一个 transaction 没有传入对应的公钥而造成缺少任何的签名的话,一个 SignaturesMissingException 会被抛出。

我们也可以选择只是简单地确认一下签名是否提供了:

twiceSignedTx.checkSignaturesAreValid()

但是要小心,这个方法既不能保证被展示出来的签名是必须要有的,也不能查出是否缺少了任何的签名。

为 transaction 提供签名

一旦我们同意了 transaction 的内容以及 transaction 上已经存在的这些签名,我们就可以将自己的签名附加在这个 SignedTransaction 上来说明我们同意了这个 transaction。

我们可以使用我们的 legal identity key 来签名:

val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx)

或者可以使用我们的其他的公钥来签名:

val twiceSignedTx2: SignedTransaction = serviceHub.addSignature(onceSignedTx, otherIdentity2.owningKey)

我们也可以通过 transaction 生成一个签名但是不直接地把它添加到 transaction 中。

我们可以使用我们的 legal identity key 来实现这个:

val sig: TransactionSignature = serviceHub.createSignature(onceSignedTx)

或者使用我们的另外的公钥:

val sig2: TransactionSignature = serviceHub.createSignature(onceSignedTx, otherIdentity2.owningKey)

公正(Notarising)和记录(recording)

公正和记录一个 transaction 是由一个内建的名为 FinalityFlow 的 flow 来处理的。