Corda API: 合约约束

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

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

合约约束 Contract constraints

Corda 将对于 states 的校验从他们的定义中分出来。你可能会希望 ContractState 接口会定义一个 verify 方法,或者在构造函数(constructor)中进行校验的逻辑,但是真正的交易却是在 Contract 类中的一个方法里进行的。这是因为我们真正要校验的是 transaction 的有效性,不仅仅是要看每一个 states 内部是不是有效的。如果对于整个 application 的规则没有遵守的话,那么在两个有效的 states 之间进行的 transaction 不一定是有效的。比如两个现金的 states $100 和 $200 可能在每个 state 中都是内部有效的,但是使用第二个来替换第一个是不被允许的,除非你是现金的发行者,否则的话你就可以免费地打印钱了。

对于一笔有效的交易,每个 state 相关的 verify 方法必须要成功执行。但是,为了能让这些变得安全,仅仅依靠名字来指定 verify 方法是不足够的,因为可能会存在多个具有相同名字的方法签名但他们是不同的实现。这个通常会在应用进行改进的过程中发生,但是这个也可能会是一个恶意的操作。

合约约束(contract constraints)通过允许一个合约开发者来约束在所有实现的全集中哪个 verify 方法会被使用(比如针对于能够匹配某个签名的所有实现的全集,合约约束能够限定到它的某个子集)。约束通过附件(JARs 文件)来满足的。因为不可以重复的规则,你是不被允许同时附加两个都定义了相同的 application 的 JARs 文件的。这个规则指定了两个附件 JARs 文件不可以提供相同的文件路径。如果他们提供了,那么这个 transaction 会被认为是无效的。因为每一个 state 都指明了一个 CorDapp 附件的约束和一个使用的 Contract class,这个指定的 class 必须只能在一个附件中出现。

那么谁来选择应该使用哪一个 CorDapp JAR 呢?这是由 transaction 的创建者来选择的,这需要满足 input 约束。Transaction 的创建者也会选择被任何的 output states 所使用的约束,但是合约逻辑本身可能也会有一些关于这些约束都是什么的选项 - 一个典型的合约会要求约束是要被传递的,也就是说,合约并不仅仅会要求下一次使用一个 state 的 transaction 是有效的,而且还需要要求之前相关的成功的 transactions 也必须是有效的。这个约束的机制在数据的创建者和更新着之间建立了一种权力的平衡,这个在管理 Corda applications 升级的时候非常有用。

通常有两种处理 Corda 中的智能合约升级的方式:

这篇文章主要讨论第一种方式。

使用约束提前授权升级更新的优势是你不需要走一个非常繁琐的流程来为账本中的每个 state 创建一个升级的 transaction。缺点是你将更多的信任交给了合约开发的第三方,他们可能会按照你不期望的或者不同意的方式来改变这个 application。使用显式更新的好处是你可以不用去考虑他们的约束而去升级 states,也包括你不想参与一次升级的情况。但是这个流程需要每个人为其提供签名,需要每个人手动地为一次升级授权,消费 notary 和 账本资源,这仅仅是让事情变得更加复杂。

约束是如何工作的

从 Corda 3 开始,有两种约束可以来使用:hash 和 zone whitelist。在将来的版本会提供第三种类型,signature 约束。

Hash 约束。公链系统(比如比特币和以太坊)提供的行为是一旦数据被记录到账本中,控制这个数据的程序是固定的并且不允许变动的。它是完全不允许升级的。这个实现了一种“代码即是法律(code is law)”的形式,这里会假设你完全相信那个 blockchain 社区(community)不会发行该平台的新的版本而造成你的程序变得无效或者改变了原来的意图。

这个在 Corda 里是通过使用 hash 约束来实现的。这个明确地指明了任何消费 transaction 允许使用的包含合约的 CorDapp JAR 的 hash 值。当这样的一个 state 被创建的时候,其他节点只有在它使用完全一致的 JAR 文件作为附件的时候才会接受这个 transaction。这也就意味着,任何在合约代码或者 state 定义中的 bugs 是不可以被解决的,除非使用 ContractUpgradeFlow 进行一次显式地合约升级流程。

注意:Corda 不支持任何一种方式来创建从来不可以被升级的 states,但是可以使用一个 hash 约束来实现这样的效果,然后只需简单地拒绝任何的显式的升级即可。Hash 约束通过对于任何的升级必须要求一个明确的同意来使你变得可控。

Zone Whitelist 约束。通常来讲,一个 hash 约束有些太过严格。你肯定想要升级你的 app,你也不会介意因为其他的业务原因而在一个 transaction 发生的时候进行升级。这就指明了对于一个 compatibility zone 的 network parameters 是期望包含一个关于合约 class 名和被允许提供该 class 的 JARs 文件的 hash 的一个 map。升级 app 的流程就会需要请求 zone operator 将你的心的 JAR 的 hash 添加到 network parameter 文件,然后触发 network parameter 升级流程。这个需要每个节点的 operator 运行一个 shell 命令来接受新的 parameters 文件并重启节点。如果节点的 owner 没有及时地重启节点,那么该节点就会停止成为该网络中的成员。

Signature 约束。这个在当前版本中还未提供,但是如果之后被实现的话,它会允许一个 state 要求一个由指定的 identity 通过使用常规的 Java jarsigner 工具来提供签名。这个会是最灵活的一种约束类型并且是最平稳的一种部署:不需要重启或者合约升级的 transactions。

默认。当 transaction 被构建的时候,如果 network parameters 包含了一个合约 class 的一个记录的话,那默认的约束类型是 zone whitelist 约束,如果没有的话就是一个 hash 约束。

一个 TransactionState 含有一个 constraint 字段,它代表了 state 的 附件(attachment)约束。当一方构造了一个 TransactionState 或者使用 TransactionBuilder.addOutput(ContractState) 添加了一个 state 并且没有指明约束参数的时候,一个默认的值(AutomaticHashConstraint)会被使用。这个约束的默认值对于一个特定的 HashAttachmentConstraint 或者 WhitelistedByZoneAttachmentConstraint 能够被自动的满足。这个自动化的解决方案会在当一个 TransactionBuilder 被转换为一个 WireTransaction 的时候发生。

最终,一个 AlwaysAcceptAttachmentConstraint 可以被用来接收任何改动,尽管这个只是为了测试的目的。

请注意 AttachmentConstraint 接口被标注了 @DoNotImplement 注解。你是不被允许定义新的约束类型的。只有开发平台可能会实现这个接口。如果你试着创建自己的约束的话,其他的节点是不会明白你的约束类型并且你的 transaction 将不会被 verify。

注意:一个 AlwaysAccept 约束就像完全地关闭了安全验证。没有人能阻挡你在生产环境使用这种约束,但是这个会将 Corda 降级为一个常规的分布式消息系统,它仅仅是有一些可选的合约逻辑仅仅可以被用来捕获错误,而无法捕获可能的恶意操作。如果你正在部署一个恶意操作并没有在你所处理的模式中的一个应用的话,使用一个 AlwaysAccept 约束可能会在操作上将事情变得简单化。

下边的例子演示了在一个 flow 中如何通过一个显式指定的哈希值约束来构造一个 TransactionState

// Constructing a transaction with a custom hash constraint on a state
TransactionBuilder tx = new TransactionBuilder();

Party notaryParty = ... // a notary party
DummyState contractState = new DummyState();

SecureHash myAttachmentHash = SecureHash.parse("2b4042aed7e0e39d312c4c477dca1d96ec5a878ddcfd5583251a8367edbd4a5f");
TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, new AttachmentHashConstraint(myAttachmentHash));

tx.addOutputState(transactionState);
WireTransaction wtx = tx.toWireTransaction(serviceHub);  // This is where an automatic constraint would be resolved.
LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub);
ltx.verify(); // Verifies both the attachment constraints and contracts

将 hash 硬编码到你的 app 中是一种非常笨拙的方式,所以 API 也提供了 AutomaticHashConstraint。这个并不是一个将会出现在一个 transaction 中的真正的约束:它作为 TransactionBuilder 的一个标记,说明了你要求节点安装的 CorDapp 的 hash,这个 CorDapp 提供了可以使用的指定的合约。通常,当使用 hash 约束的时候,你基本上会总是想“无论当前的 code 是什么”,并且不想要一个硬编码的 hash。所以这个自动的约束是很有用的。

CorDapps 作为附件

当节点启动的时候,安装在节点上并包含实现了 Contract 接口的类的 CorDapps JARs 文件(什么是 CorDapp?)会被自动加载到 AttachmentStorage

当 CorDapps 被加载到 attachment store 后,节点会创建一个在 contract 类和 attachment 之间创建一个连接。这个使找到任何指定 contract 的 attachment 成为可能。这个就是附件的自动解决方案是如何通过使用 TransactionBuilder 来完成的,并且当确认约束及 contract 的时候,附件是如何被关联至他们对应的 contracts 上的。

注意:一种民概念的编写一个 CorDapp 的方式是将所有的 states,contracts,flows 和支持的代码都放在同一个 Java module 中。这个可以工作但是它也会将你整个 app 发布到账本上去。这会有两个问题:(1) 它不是有效率的,并且(2) 它意味着对于 flows 或者 app 其他部分的改动会在账本中作为一个“新 app”被看到,这个可能会以要求一个没有必要的升级流程而终止。将你的 app 分别放到多个 modules 中是一个更好的方式:一个 module 仅仅包含 states,contracts 和核心的数据类型。另一个 module 包含 app 剩下的部分。

测试

因为所有涉及 transactions 的测试现在都要求要有附件,也要求必须在测试中加载正确的附件。JVM 生态系统中的单元测试环境趋向于使用类目录(class directories)而不是 JARs,所以 CorDapp JARs 通常不会被创建来做测试。这些要求将会给构建 Corda 和 CorDapp 带来巨大的难度,所以测试套件有一套方便的方法来从包名字(package names)或者当测试中引入的 CorDapp(s) 已经存在的时候,通过指定 JAR URLs 来生成 CorDapps。你也可以在你的测试中使用 AlwaysAcceptAttachmentConstraint 来关闭约束机制。

MockNetwork/MockNode

确保一个 MockNode 实例生成正确的 CorDapp 的方式是在创建 MockNetwork 的时候使用 cordappPackages 构造函数参数(Kotlin)或者 MockNetworkParameterssetCordappPackages 方法(Java)。这些调用会使 AbstractNode 使用这个指定的包来作为 CorDapps 的 source。这个包里的所有文件会被 zip 成一个 JAR 并被添加到附件中然后被 CordappLoader 作为 CorDapps 被加载。下边是一个实例:

class SomeTestClass {
     MockNetwork network = null;

     @Before
     void setup() {
         network = new MockNetwork(new MockNetworkParameters().setCordappPackages(Arrays.asList("com.domain.cordapp")))
     }

     ... // Your tests go here
}

MockServices

如果你直接使用了 MockServices,你可以使用一个带有一个包列表的构造器来实例化它,它会使用 cordappPackages 这个参数作为 CorDapps 来使用。

MockServices mockServices = new MockServices(Arrays.asList("com.domain.cordapp"))

然而这有一种更简单的方式!如果你的 unit test 跟合约代码本身是在相同的包中的时候,那么你可以使用 MockServices 的 no-args 构造函数。CorDapp 要被扫描的包将会和构建成对象的类的包一致。

Driver

Driver 会带有一个叫做 extraCordappPackagesToScan 的参数,这个参数是一个可以作为 CorDapps 来使用的包列表。

driver(new DriverParameters().setExtraCordappPackagesToScan(Arrays.asList("com.domain.cordapp"))) ...

Full Nodes

当测试全节点的时候,只需要简单地把你的 CorDapp 放置到节点的 cordapps 路径下即可。

Debugging

如果一个附件的约束无法满足的话,一个 MissingContractAttachments 的异常会被抛出。以下是两个常见的 MissingContractAttachments 异常的 source: