阅读本文之前,你应该对 Corda 核心概念 - Transactions 比较熟悉了。
Transaction 生命周期
从它被创建到最终被添加到账本中,每个 transaction 会大体占用 3种状态中的一种:
TransactionBuilder
。这个是 transaction 的初始状态。这也是 transaction 唯一可以被修改的一个状态,所以在进行下一步之前我们必须要确保添加了所有必须的组件。SignedTransaction
。现在的 transaction 已经有了一个或者更多的数字签名,并且已经是不可修改了。这个会是在不同的节点间传递来获得更多签名的 transaction 类型,也是会最终被记录到账本中的 transaction。LedgerTransaction
。这个 transaction 已经被“解决”掉了。比如它的 inputs 已经从引用被转换为实际的 states 了 - 允许 transaction 被彻底地检查。
我们可以用下图来表示 transactions 在三个状态中的转换:
Transaction 组件
一个 transaction 包括六种类型的组件:
- 1+ states:
- 0+ input states
- 0+ output states
- 1+ commands
- 0+ attachments
- 0 or 1 time-window
- 带有 time-window 的 transaction 还必须要有一个 notary
每个组件都对应于 Corda API 中的一个指定的类。下边的部分描述了每个组件的类,和他们是如何被创建的。
Input states
Input states 是以 StateAndRef
实例的形式添加进 transaction 的,它包括:
ContractState
本身- 一个
StateRef
用来识别作为一个指定的 transaction 的 output 的该ContractState
val ourStateAndRef: StateAndRef<DummyState> = serviceHub.toStateAndRef<DummyState>(ourStateRef)
一个 StateRef
唯一地识别了一个 input state,允许 notary 可以将它标记为一个历史记录。它由下边的元素组成:
- 产生该 state 的 transaction 的哈希值
- 该 state 在这个 transaction 中的 outputs 列表中的索引值(index)
val ourStateRef: StateRef = StateRef(SecureHash.sha256("DummyTransactionHash"), 0)
StateRef
将一个 input 连接回来产生它的那次 transaction。这就意味着那个 transaction 形成了一个“链条”,这个链条将每个 input 都同产生它的原始 transaction 链接在了一起。这就允许了节点可以回溯整条链来确认一个新的 transaction 并且确保了每个 input 都是通过一个有效的并且有序的 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
中,它将下边两个元素整合在了一起:
ContractState
代表了 output state- 一个
String
用来识别决定该 state 的 contract
val ourOutput: StateAndContract = StateAndContract(ourOutputState, DummyContract.PROGRAM_ID)
Commands
一个 command 是做为 Command
实例被添加到一个 transaction 中的。Command 包含:
- 一个
CommandData
实例,它代表了 command 的类型 - 一个
List<PublicKey>
代表了 command 所要求的签名者的列表
val commandData: DummyContract.Commands.Create = DummyContract.Commands.Create()
val ourPubKey: PublicKey = serviceHub.myInfo.legalIdentitiesAndCerts.first().owningKey
val counterpartyPubKey: PublicKey = counterparty.owningKey
val requiredSigners: List<PublicKey> = listOf(ourPubKey, counterpartyPubKey)
val ourCommand: Command<DummyContract.Commands.Create> = Command(commandData, requiredSigners)
Attachments
附件是通过他们的哈希值来识别的:
val ourAttachment: SecureHash = SecureHash.sha256("DummyAttachment")
具有相应的哈希值的附件必须要提前通过节点的 RPC 接口上传到 ledger 中。
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 中添加内容:
StateAndRef
对象是作为 input states 被添加TransactionState
和StateAndContract
对象是作为 output states 被添加TransactionState
和StateAndContract
会被 wrapper 成一个ContractState
output,这就将 output 和一个指定的 contract 链接到了一起
Command
对象是作为 commands 被添加SecureHash
对象是作为附件被添加的- 如果 transaction 中已经存在
TimeWindow
的话,那么这里的TimeWindow
对象会替换掉那个已经存在的TimeWindow
传入任何其他类型的对象将会造成一个 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 上的签名列表
@CordaSerializable
data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
override val sigs: List<TransactionSignature>
) : TransactionWithSignatures {
当提供我们的签名之前,我们会既要确认 transaction 的内容,也有确认 transaction 的签名。
确认 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 的内容。我们首先需要解决的是将 StateRef
和 SecureHash
实例化为真正的 ContractState
和 Attachment
的实例,然后我们就可以检查了。
我们通过使用 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 来处理的。