权利

语法概述

在 PDC 中,合同权利的具象体现是⼀段基于合同内容的可执行代码,可用于修改合同状态、创建新的合同等。当某个参与⽅能够行使某项权利时,即意味着该参与方拥有执行这段代码的权限。所有权利的定义均需要放在与合同模板相关联的impl代码块中,同时使用#[liquid(rights)]属性对该代码块进行标注,例如在下列代码中,定义了一项名为add的权利:

  1. #[liquid(contract)]
  2. pub struct Ballot {
  3. #[liquid(signers)]
  4. government: address,
  5. #[liquid(signers = "$[..](?@.voted).addr")]
  6. voters: Vec<Voter>,
  7. proposal: Proposal,
  8. }
  9. #[liquid(rights)]
  10. impl Ballot {
  11. #[liquid(belongs_to = "government")]
  12. pub fn add(mut self, voter_addr: address) -> ContractId<Ballot> {
  13. ...
  14. }
  15. }

#[liquid(rights)]属性标注的代码块中,每一项代表权利的函数的可见性必须为pub,即所有权利都需对外可见。若需要编写无需公开的辅助函数,则可以将辅助函数的定义放置于另一个普通的impl代码块中,或直接放置于impl代码块之外,例如:

  1. #[liquid(rights)]
  2. impl Ballot {
  3. ...
  4. }
  5. impl Ballot {
  6. fn helper_1(&self) {
  7. ...
  8. }
  9. }
  10. fn helper_2() {
  11. ...
  12. }

每项权利必须要使用#[liquid(belongs_to)]属性标注此项权利属于哪些参与方。权利的所属所属方的账户地址必须包含在合同内容中,例如在上述示例中,名为add的权利属于合同中的government成员。所属方后可跟随一个由花体括号{}括起的选择器,其语法与上节中的对象选择器函数选择器相同。例如,若需要将add权利分配给所有的投票者,即voters中所有投票者的账户地址,则上述示例可以改写为:

  1. #[liquid(belongs_to = "voters{ $[..].addr }")]
  2. pub fn add(mut self, voter_addr: address) -> ContractId<Ballot> {
  3. ...
  4. }

由于所有的权利都需要基于具体的合同执行,因此权利的第一项参数必须是接收器(Receiver),用于和具体的合同进行绑定。接收器可以为下列四种之一:

  • self,以只读的方式访问当前合同,不能修改合同中的内容,并且在权利行使完毕后,作废当前合同;
  • mut self,以可写的方式访问当前合同,可以修改合同中的内容,并且在权利行使完毕后,作废当前合同;
  • &self,以只读的方式访问当前合同,不能修改合同中的内容。权利行使完毕后,不会作废当前合同;
  • &mut self,以可写的方式访问当前合同,可以修改合同中的内容。权利行使完毕后,不会作废当前合同。

作废合同意味着之后不能再基于该合同继续行使权利,但是与该合同相关的数据并不会从链上删除,其contractId也不会被废弃,可以继续用于查询合同中的内容。在某些领域,作废机制也被称作“归档”,因为虽然不能再继续行权,但是合同内容仍会作为存证保留在区块链中,可供日后取证所用。作废机制在由旧合同生成新合同时比较有用,能够用于避免产生重复的新合同。例如,在完整的投票协作中,government被授予根据已投票的提案中产生出一项新决议的权利,如下列代码所示,用于执行此功能的decide权利的接收器是self,因此在行权完毕后,原始的提案将会被作废,从而避免产生重复的决议(代码中的sign!宏及ContractId类型将在下一节中进行详细解释。):

  1. #[liquid(rights)]
  2. impl Ballot {
  3. ...
  4. #[liquid(belongs_to = "government")]
  5. pub fn decide(self) -> ContractId<Decision> {
  6. require(
  7. self.voters.iter().all(|voter| voter.voted),
  8. "all voters must vote",
  9. );
  10. let yays = self.voters.iter().filter(|v| v.choice).count();
  11. let nays = self.voters.iter().filter(|v| !v.choice).count();
  12. require(yays != nays, "cannot decide on tie");
  13. let accept = yays > nays;
  14. let voters = self.voters.iter().map(|voter| voter.addr).collect();
  15. sign! { Decision =>
  16. accept,
  17. government: self.government,
  18. proposal: self.proposal,
  19. voters,
  20. }
  21. }
  22. }

当多项权利的所属方相同时,若为每项权利标注同样的#[liquid(belongs_to)]属性会导致代码稍显冗余,因此 Liquid 提供了另一种简便的属性#[liquid(rights_belong_to)]。该属性用于标注impl代码块,但是与#[liquid(belongs_to)]属性类似,需要被赋予一个用于指定权利所属方的字符串参数,用于表示该impl代码块中定义的所有权利均归属于这些所属方。属性参数中同样也可以使用选择器语法。当使用#[liquid(rights_belong_to)]属性后,impl代码块内部的函数均不允许再被标注#[liquid(belongs_to)]属性。在下列示例代码中,government同时拥有adddecide权利:

  1. #[liquid(rights_belong_to = "government")]
  2. impl Ballot {
  3. pub fn add(mut self, voter_addr: address) -> ContractId<Ballot> {
  4. ...
  5. }
  6. pub fn decide(self) -> ContractId<Decision> {
  7. ...
  8. }
  9. }

表示权利所属方的字符串也可以是空字符串,此时表示任何实体均可以行使该权利,例如:

  1. #[liquid(belongs_to = "")]
  2. pub fn vote(&mut self, choice: bool) {
  3. ...
  4. }

行权

可以通过 Node.js CLI 工具的exercise命令行使合同中的权利,exercise命令的使用方式如下所示,在使用时需要传递合同模板名称、合同 ID、权利名称以及行使权利时所需要参数:

  1. cli.js exec exercise <contract> <rightName> [parameters..]
  2. Exercise an right of a contract
  3. Positionals:
  4. contract The name and ID(split by `#`) of the exercised contract
  5. [string] [required]
  6. rightName The name of the exercised right [string] [required]
  7. parameters The parameters(split by space) of the contract
  8. [array] [default: []]
  9. Options:
  10. --version Show version number [boolean]
  11. --who, -w Who will do this operation [string]
  12. -h, --help Show help [boolean]

假设government的账户地址是 Alice(0x144d5ca47de35194b019b6f11a56028b964585c9),Alice 可以首选签署一份投票者列表为空的提案合同:

  1. node ./cli.js exec sign Ballot 0x144d5ca47de35194b019b6f11a56028b964585c9 [] '{\"proposer\":\"0x144d5ca47de35194b019b6f11a56028b96458\",\"content\":\"Playing\"}' --who alice

返回结果如下所示:

  1. {
  2. "status": "0x0",
  3. "contractId": 0,
  4. "transactionHash": "0x13c11b0d2e4907962d2dde5e09d8c1632fcb414c5a71f3195a86125f258f137e"
  5. }

随后,Alice 通过行使add权利将 Bob(0x3b1b0b74801e104543ef05ed88cc215eb4e51d72)及 Charlie(0x1653641673a6f5eaebfcea9137b91407e7c86c35)添加至投票列表中:

  1. node ./cli.js exec exercise Ballot#0 add 0x3b1b0b74801e104543ef05ed88cc215eb4e51d72 --who alice
  2. node ./cli.js exec exercise Ballot#1 add 0x1653641673a6f5eaebfcea9137b91407e7c86c35 --who alice

注意命令中Ballot的合约 ID 在不断自增,这是由于根据add权利的定义,其会作废当前提案合同并生成一份新的提案合同,然后返回新提案合同的合同 ID。如果在 ID 为 0 的提案合同作废后继续在其上行使权利,则会导致如下所示的报错:

  1. {
  2. "status": "0x16",
  3. "message": "the contract `Ballot` with id `0` had been abolished already",
  4. "transactionHash": "0xf0b8dfbe2d0bba0f40280d3b502d572787a0580d861070c5ce1916e7b009f57c"
  5. }

但是在 ID 为 0 的提案合同作废后仍然可以查询其合同内容:

  1. node ./cli.js exec fetch Ballot#0

返回结果如下所示:

  1. {
  2. "status": "0x0",
  3. "Ballot": {
  4. "government": "0x144d5ca47de35194b019b6f11a56028b964585c9",
  5. "voters": [],
  6. "proposal": {
  7. "proposer": "0x000144d5ca47de35194b019b6f11a56028b96458",
  8. "content": "Playing"
  9. }
  10. },
  11. "transactionHash": "0xdf7418f3c5bb6a569d1c1cb9f1e522865ab179927a6eb14fe202ed6303786e5b"
  12. }

可以看出截至作废时,投票者列表仍然为空,因此新的投票者已经被加入至 ID 为 1 的提案合同中。

在 Bob 及 Charlie 投赞成票之后,Alice 可以行使decide权利以产生新的决议合同:

  1. node ./cli.js exec exercise Ballot#2 decide --who alice

根据decide权利的定义,行权完毕后应当返回Decision合同的 ID:

  1. {
  2. "status": "0x0",
  3. "outputs": [1],
  4. "transactionHash": "0xb0cb7c048afc09083841bc49eb918648a91742fd1f4dffe1876144b8d38e2ca9"
  5. }
  1. node ./cli.js exec fetch Decision#1

返回结果如下所示,包含了合同 ID 为 1 的决议合同中的内容:

  1. {
  2. "status": "0x0",
  3. "Decision": {
  4. "government": "0x144d5ca47de35194b019b6f11a56028b964585c9",
  5. "proposal": {
  6. "proposer": "0x000144d5ca47de35194b019b6f11a56028b96458",
  7. "content": "Playing"
  8. },
  9. "voters": [
  10. "0x3b1b0b74801e104543ef05ed88cc215eb4e51d72",
  11. "0x1653641673a6f5eaebfcea9137b91407e7c86c35"
  12. ],
  13. "accept": true
  14. },
  15. "transactionHash": "0xe10cdc052fa4121c2fc52f5a135a5fb7821303b67bee0a814f4f8a7f37731384"
  16. }

Liquid 在行权时会自动校验行权的发起方是否拥有行权的资格,假如在上述最后一步中,Bob 试图代替 Alice 行使decide权利:

  1. node ./cli.js exec exercise Ballot#2 decide --who bob

则会导致执行失败,并报出权限校验不通过错误:

  1. {
  2. "status": "0x16",
  3. "message": "exercising right `decide` of contract `Ballot` is not permitted",
  4. "transactionHash": "0x40c167383c748c3d2bbc86bbe4186a6051294815fa3d1a02d5e03e7df6d44a36"
  5. }