API: Contracts

阅读本文之前,你应该对 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)
}

Verify()

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

LedgerTransaction 实例被传入 verify 方法,并具有以下属性:

@CordaSerializable
data class LedgerTransaction(
        /** 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
) : FullTransaction() {

其中:

  • inputs 是类型为 List<StateAndRef<ContractState>> 的 transaction inputs
  • outputs 是类型为 List<TransactionState<ContractState>> 的 transaction outputs
  • commands 是类型为 List<CommandWithParties<CommandData>> 的 transaction commands 和相关的签名
  • attachments 是类型为 List<Attachment> 的 transaction attachments
  • notary 是 transaction 的 notary。这个必须要同所有的 inputs 拥有相同的 notary
  • timeWindow 定义了一笔交易在什么样的时间窗(timewindow)内才会被公正(notarised)

LedgerTransaction 暴漏了很多 utility 方法来访问交易的内容:

  • inputStatesStateAndRef 列表中获得 input ContractState
  • 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 签名人名字的列表,如果知道的话
  • value 是被签名的对象(在这里指的是这个 command)

在 commands 中使用 verify

总体来说,我们希望基于交易的 commands 来定义不同的约束条件。为了实现这个,我们可以通过提取这个 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.
    }
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注