注意:阅读本文之前,你应该对 Corda 核心概念 - Contract 比较熟悉了。
Contract
Contracts 都是实现了 Contract
接口的类。Contract
接口定义如下:
/**
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for
* every [net.corda.core.transactions.LedgerTransaction] they see on the network, for every input and output state. All
* contracts must accept the transaction for it to be accepted: failure of any aborts the entire thing. The time is taken
* from a trusted time-window attached to the transaction itself i.e. it is NOT necessarily the current time.
*
* TODO: Contract serialization is likely to change, so the annotation is likely temporary.
*/
@CordaSerializable
interface Contract {
/**
* Takes an object that represents a state transition, and ensures the inputs/outputs/commands make sense.
* Must throw an exception if there's a problem that should prevent state transition. Takes a single object
* rather than an argument so that additional data can be added without breaking binary compatibility with
* existing contract code.
*/
@Throws(IllegalArgumentException::class)
fun verify(tx: LedgerTransaction)
}
Contract
只有一个 verify
方法,它会有一个 LedgerTransaction
作为 input 参数并且不会返回任何内容。这个方法被用来检验一个交易的提议是否有效,包括下边的验证:
- 我们会搜集这个交易的 input 和 output states 的 contract code
- 我们会调用每个 contract code 的
verify
方法,将 transaction 作为 input 传进去 - 这个更新账本的提议仅仅在所有的
verify
方法都没有返回 exception 的情况下才算是有效的
verify
是在一个 sandbox 中执行的:
- 它没有权限访问内部的内容
- 针对于它可用的类库被放入白名单来不允许:网络访问像硬盘或数据库 I/O
这意味着 verify
仅仅能够在决定一个交易是否有效的时候才能够访问 LedgerTransaction
中定义的属性。
最简单的两个 verify
方法:
- 一个是接受所有可能的 transactions:
override fun verify(tx: TransactionForContract) {
// Always accepts!
}
- 一个是拒绝所有的 transactions:
override fun verify(tx: TransactionForContract) {
throw IllegalArgumentException("Always rejects!")
}
LedgerTransaction
被传入 verify 方法中的 LedgerTransaction
对象具有以下属性:
@CordaSerializable
data class LedgerTransaction @JvmOverloads constructor(
/** The resolved input states which will be consumed/invalidated by the execution of this transaction. */
override val inputs: List<StateAndRef<ContractState>>,
override val outputs: List<TransactionState<ContractState>>,
/** Arbitrary data passed to the program of each input state. */
val commands: List<CommandWithParties<CommandData>>,
/** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */
val attachments: List<Attachment>,
/** The hash of the original serialised WireTransaction. */
override val id: SecureHash,
override val notary: Party?,
val timeWindow: TimeWindow?,
val privacySalt: PrivacySalt,
private val networkParameters: NetworkParameters? = null
) : FullTransaction() {
其中:
inputs
是类型为List<StateAndRef<ContractState>>
的 transaction 的 inputsoutputs
是类型为List<TransactionState<ContractState>>
的 transaction 的 outputscommands
是类型为List<CommandWithParties<CommandData>>
的 transaction 的 commands 和相关的签名者attachments
是类型为List<Attachment>
的 transaction 的 attachmentsnotary
是 transaction 的 notary。这个必须要同所有的 inputs 拥有相同的 notarytimeWindow
定义了一笔交易在什么样的时间窗(timewindow)内才会被公正(notarised)
LedgerTransaction
暴漏了很多 utility 方法来访问交易的内容:
inputStates
从StateAndRef
列表中获得 inputContractState
getInput
/getOutput
/getCommand
/getAttachment
通过索引(index)来获得某个组件getAttachment
通过 ID 获得一个附件inputsOfType
/inRefsOfType
/outputsOfType
/outRefsOfType
/commandsOfType
基于他们的类型获得相关组件filterInputs
/filterInRefs
/filterOutputs
/filterOutRefs
/filterCommands
基于一个条件获得相关组件findInput
/findInRef
/findOutput
/findOutRef
/findCommand
获得满足一定条件的单一组件,或者当有多个满足条件的组件的时候抛出异常
requireThat
verify
能够针对每一个约束手动地抛出异常:
override fun verify(tx: LedgerTransaction) {
if (tx.inputs.size > 0)
throw IllegalArgumentException("No inputs should be consumed when issuing an X.")
if (tx.outputs.size != 1)
throw IllegalArgumentException("Only one output state should be created.")
}
但是这个定义有些繁琐。我们可以使用 requireThat
来定义一系列的约束:
requireThat {
"No inputs should be consumed when issuing an X." using (tx.inputs.isEmpty())
"Only one output state should be created." using (tx.outputs.size == 1)
val out = tx.outputs.single() as XState
"The sender and the recipient cannot be the same entity." using (out.sender != out.recipient)
"All of the participants must be signers." using (command.signers.containsAll(out.participants))
"The X's value must be non-negative." using (out.x.value > 0)
}
对于 requireThat
中的每一个 <String
, Boolean
> 对来说,如果 boolean 条件返回的是 false,一个 IllegalArgumentException
会被抛出,包含对应的错误信息。所以这个错误会造成 transaction 被拒绝。
Commands
LedgerTransaction
包含了作为 CommandWithParties
实例列表的 commands。CommandWithParties
将一个 CommandData
和一个所需的签名者列表匹配起来:
/** A [Command] where the signing parties have been looked up if they have a well known/recognised institutional key. */
@CordaSerializable
data class CommandWithParties<out T : CommandData>(
val signers: List<PublicKey>,
/** If any public keys were recognised, the looked up institutions are available here */
val signingParties: List<Party>,
val value: T
)
其中:
signers
是每个签名者的PublicKey
的一个列表signingParties
签名者 identities 的列表,如果知道的话value
是被签名的对象(在这里指的是这个 command)
使用 commands 来处理 verify 分支
总的来说,我们希望基于交易的 commands 来定义不同的约束条件。比如我们想要为一个现金发行的 transaction 和 一个现金交换的 transaction 定义不同的合约。
我们可以通过提取这个 command 并在 verify
里使用标准的分支逻辑来实现这个功能。这里我们提取了交易中的类型为 XContract.Commands
的单独的 command,并且相应地对 verify
进行了分支逻辑判断:
class XContract : Contract {
interface Commands : CommandData {
class Issue : TypeOnlyCommandData(), Commands
class Transfer : TypeOnlyCommandData(), Commands
}
override fun verify(tx: LedgerTransaction) {
val command = tx.findCommand<Commands> { true }
when (command) {
is Commands.Issue -> {
// Issuance verification logic.
}
is Commands.Transfer -> {
// Transfer verification logic.
}
}
}
}
Legal prose
一个 Contract 类可以使用 @LegalProseReference 的 annotation。这个 annotation 能够将这个 contract code 同以法律条款的形式定义的文档关联起来,这个文档中定义的是跟 verify 里所定义的约束内容相同的内容。这个并不是必须的,但是当出现意见不统一的情况下,法律条款文件效力会优先于软件中的实现。
@LegalProseReference 仅仅有一个 uri 的输入参数,作为同 contract code 相关联的法律条款文档的标识:
@LegalProseReference(uri = "foo.bar.com/my-legal-doc.html")
class MyContract : Contract {
override fun verify(tx: LedgerTransaction) {
// Contract logic.
}
}