5.4.5 共识步骤
Reach 的共识步骤存在于共识转移语句的延拓部分。它代表着应用中共识网络合约执行的动作。
5.4.5.1 语句
任何对计算有效的语句都对共识步骤有效。但也允许一些额外的语句。
5.4.5.1.1 commit
commit();
commit 语句格式为:commit();。commit 将语句的延拓部分作为 DApp 计算的下一步提交。换句话说,它结束当前的共识步骤,并允许更多的本地步骤。
5.4.5.1.2 only 和 each
共识步骤中允许 only 和 each ,当后端观察到共识步骤完成时会执行它们(即:在相关的 commit 语句之后)。
5.4.5.1.3 Participant.set 和 .set
Participant.set(PART, ADDR); PART.set(ADDR);
在执行之后,给定的参与者被固化到给定的地址。尝试 .set 一个参与者类是无效的。如果此参与者的后端正在运行,而其地址与给定的地址不匹配,则它会中止。这只可能在共识步骤中发生。
研讨:中继帐户是一个很好的介绍性项目,它演示了如何使用 Reach 的这一特性。
5.4.5.1.4 while
var [ heap1, heap2 ] = [ 21, 21 ]; invariant(balance() == 2 * wagerAmount); while ( heap1 + heap2 > 0 ) { .... [ heap1, heap2 ] = [ heap1 - 1, heap2 ]; continue; }
while 语句存在于共识步骤中,其格式为: var LHS = INIT_EXPR; invariant(INVARIANT_EXPR); while( COND_EXPR ) BLOCK
其中:
- LHS 是标识符定义中的有效左侧,表达式 INIT_EXPR 位于右侧;
- INVARIANT_EXPR 是一个表达式,称之为循环常量。该常量在每次执行 BLOCK 块之前和之后都必须为 true;
- 若 COND_EXPR 为 true 则执行块,否则循环终止,并控制转移到 while 语句的延拓部分;
- 由 LHS 绑定的标识符在 INVARIANT_EXPR 、 COND_EXPR 、 BLOCK 和 while 语句的尾部被绑定。
阅读 Reach 指南中有关寻找循环常量的内容。
5.4.5.1.5 continue
[ heap1, heap2 ] = [ heap1 - 1, heap2 ]; continue;
continue 语句存在于 while 语句的块中,其格式为: LHS = UPDATE_EXPR; continue;
其中,由 LHS 绑定的标识符是由最近的封闭 while 语句绑定的变量的子集,UPDATE_EXPR 是可由 LHS 绑定的表达式。
continue 语句是终止语句,因此必须有一个空尾。
continue 语句可以不带标识符更新地书写,相当于写成: [] = []; continue;
continue 语句必须受共识转移支配,这意味着 while 语句的主体在调用 continue; 之前必须始终 commit(); 。在将来版本的 Reach 中将会取消此限制,届时 Reach 将执行终止检查。
5.4.5.1.6 parallelReduce
const [ keepGoing, as, bs ] = parallelReduce([ true, 0, 0 ]) .invariant(balance() == 2 * wager) .while(keepGoing) .case(Alice, (() => ({ when: declassify(interact.keepGoing()) })), () => { each([Alice, Bob], () => { interact.roundWinnerWas(true); }); return [ true, 1 + as, bs ]; }) .case(Bob, (() => ({ when: declassify(interact.keepGoing()) })), () => { each([Alice, Bob], () => { interact.roundWinnerWas(false); }); return [ true, as, 1 + bs ]; }) .timeout(deadline, () => { showOutcome(TIMEOUT)(); race(Alice, Bob).publish(); return [ false, as, bs ]; });
parallelReduce 语句格式为: const LHS = parallelReduce(INIT_EXPR) .invariant(INVARIANT_EXPR) .while(COND_EXPR) .case(PART_EXPR, PUBLISH_EXPR, PAY_EXPR, CONSENSUS_EXPR) .timeout(DELAY_EXPR, () => TIMEOUT_BLOCK); 其中:
- LHS 和 NIT_EXPR 就像 while 循环的初始化组件;
- .invariant 和 .while 组件是类似于 while 循环的常量和条件;
- 而 .case 和 .timeout 组件类似于 fork 语句中相应的组件。
当 PART_EXPRs 的计算结果都是唯一参与者时, .case 组件可以重复很多次,就像 fork 语句中一样。
.timeRemaining
在 parallelReduce 中处理绝对期限时,TIMEOUT_BLOCK 中有一种通用模式,即让参与者竞争发布并返回累加器。对于以下情况可以使用简写 .timeRemaining : const [ timeRemaining, keepGoing ] = makeDeadline(deadline); const [ x, y, z ] = parallelReduce([ 1, 2, 3 ]) .while(keepGoing()) ... .timeRemaining(timeRemaining())
可以扩展为: .timeout(timeRemaining(), () => { race(...Participants).publish(); return [ x, y, z ]; })
.throwTimeout .throwTimeout 是一种简写,当发生超时时它将作为异常抛出累加器。因此,使用此分支的 parallelReduce 必须被包含在 try 语句中。例如: try { const [ x, y, z ] = parallelReduce([ 1, 2, 3 ]) ... .throwTimeout(deadline) } catch (e) { ... }
其中 throwTimeout 可以扩展为: .timeout(deadline, () => { throw [ x, y, z ]; })
— parallelReduce 语句本质上是while循环与你自己编写的 fork 语句相结合的一种缩写模式。这在去中心化应用程序中极为常见。 该理念是:有一些值( LHS )初始化后,会反复由每个竞争参赛者来进行唯一更新,直到条件不成立。 var LHS = INIT_EXPR; invariant(INVARIANT_EXPR) while(COND_EXPR) { fork() .case(PART_EXPR, PUBLISH_EXPR, PAY_EXPR, () => { LHS = CONSENSUS_EXPR; continue; }) .timeout(DELAY_EXPR, () => TIMEOUT_BLOCK); }
5.4.5.2 表达式
任何对计算有效的语句都对共识步骤有效。但也允许一些额外表达式。
5.4.5.2.1 this
在共识步骤中,this 指执行共识转移的参与者的地址。当共识转移是由 race 表达式发起时,这种方式会很有用。
5.4.5.2.2 transfer
transfer(10).to(Alice); transfer(2, gil).to(Alice);
transfer 表达式的格式为: transfer(AMOUNT_EXPR).to(PART) 其中:
- AMOUNT_EXPR 是一个用于计算出一个无符号整数的表达式
- PART 是参与者标识符,执行从合约转移网络代币到到被命名的参与者。
- AMOUNT_EXPR 计算出的值必须小于或等于合约账户中网络代币的余额。
transfer 表达式也可以写成: transfer(AMOUNT_EXPR,TOKEN_EXPR).to(PART) 其中 TOKEN_EXPR 是 Token 类型,用于指定转移哪一种非网络代币。
transfer 表达式只能存在于共识步骤中。
5.4.5.2.4 checkCommitment
checkCommitment( commitment, salt, x )
制作一个要求,要求 commitment 是 salt 和 x 的摘要。该表达式用于 makeCommitment 已经在本地步骤中被使用过后的共识步骤中。
5.4.5.2.5 Remote 对象
const randomOracle = remote( randomOracleAddr, { getRandom: Fun([], UInt), }); const randomVal = randomOracle.getRandom.pay(randomFee)();
远程对象(即 Remote 对象)是 Reach 应用程序中外来合约的表现形式。在共识步骤期间,Reach 计算可以通过规定的接口一致地与该对象通信。
远程对象是通过调用带有地址和接口的 remote 函数来构造的,该接口是一个对象,其中每个键都绑定一个函数类型。例如:
const randomOracle = remote( randomOracleAddr, { getRandom: Fun([], UInt), }); const token = remote( tokenAddr, { balanceOf: Fun([Address], UInt), transferTo: Fun([UInt, Addres], Null), });
一旦构建,远程对象的字段就代表了那些远程合约交互,称之为远程函数。例如示例中的 randomOracle.getRandom 、token.balanceOf 和 token.transferTo 就是远程函数。
远程函数可以通过使用合适的参数,然后返回指定输出的方式来调用。此外,远程函数可以通过以下操作之一进行扩展: REMOTE_FUN.pay(AMT)—返回一个远程函数,当该函数被调用时,从调用方接收 AMT 个网络代币。
REMOTE_FUN.bill(AMT)—返回一个远程函数,当该函数返回时,向调用方提供 AMT 个网络代币。
REMOTE_FUN.withBill()—返回一个远程函数,该函数返回时,向调用方提供一定数量的网络代币。确切的数量会以元组打包原始结果的方式返回,其中第一个元素是数量,第二个元素是原始结果。例如: const [ returned, randomValue ] = randomOracle.getRandom.pay(stipend).withBill()(); 这算是一种与随机预言机通信的方法,该预言机获得实际成本的保守近似值(即实际应支付的值),并返回未被使用部分。这种返回未使用部分的情形无法通过 REMOTE_FUN.bill 获得。
远程对象目前还不支持非网络代币的交互。
5.4.5.2.6 映射:创建与修改
const bidsM = new Map(UInt); bidsM[this] = 17; delete bidsM[this];
在共识步骤中可以通过 new Map(TYPE_EXPR) 的方式创建新的线形状态映射,其中 TYPE_EXPR 是某种类型。
创建映射的表达式会返回一个值,该值可通过 map[ADDR_EXPR] 对特定映射进行间接引用,其中 ADDR_EXPR 是一个地址。这样间接引用返回的值类型可能为 (TYPE_EXPR) ,因为映射可能不包含 ADDR_EXPR 的值。
可以通过 map[ADDR_EXPR]=VALUE_EXPR 来对 ADDR_EXPR 设定 VALUE_EXPR(类型为 TYPE_EXPR ),或通过 delete map[ADDR_EXPR] 来删除已添加的映射。这类修改只可存在于共识步骤中。
5.4.5.2.7 集合:创建与修改
const bidders = new Set(); bidders.insert(Alice); bidders.remove(Alice); bidders.member(Alice); // false
Set (即集合)是线形状态的另一种容器。它仅仅是 Map(Null) 的别名;并且它仅在追踪地址时有用。由于 Set 本质是 Map,因此它也只能存在于共识步骤中。
Set 可以通过 s.insert(ADDRESS) 在集合 s 内部设置 ADDRESS 的方式进行修改。或者通过 s.remove(ADDRESS) 从集合中移除 ADDRESS 。这类修改只能存在于共识步骤中。