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 = INITEXPR;
[invariant](https://docs.reach.sh/ref-programs-consensus.html#%28reach.%28%28invariant%29%29%29)(INVARIANTEXPR);
[while](https://docs.reach.sh/ref-programs-consensus.html#%28reach.%28%28while%29%29%29)( 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 = UPDATEEXPR;
[continue](https://docs.reach.sh/ref-programs-consensus.html#%28reach.%28%28continue%29%29%29);
其中,由 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(INITEXPR)
.[invariant](https://docs.reach.sh/ref-programs-consensus.html#%28reach.%28%28invariant%29%29%29)(INVARIANTEXPR)
.[while](https://docs.reach.sh/ref-programs-consensus.html#%28reach.%28%28while%29%29%29)(CONDEXPR)
.[case](https://docs.reach.sh/ref-programs-compute.html#%28reach.%28%28case%29%29%29)(PARTEXPR,
PUBLISH_EXPR,
PAY_EXPR,
CONSENSUS_EXPR)
.[timeout](https://docs.reach.sh/ref-programs-step.html#%28reach.%28%28timeout%29%29%29)(DELAYEXPR, () [=>](https://docs.reach.sh/ref-programs-compute.html#%28reach.%28%28~3d._~3e%29%29%29)
TIMEOUT_BLOCK);
其中:
- LHS 和 NITEXPR 就像 [while](https://docs.reach.sh/ref-programs-consensus.html#%28reach.%28%28while%29%29%29) 循环的初始化组件;
- .invariant 和 .while 组件是类似于 while 循环的常量和条件;
- 而 .case 和 .timeout 组件类似于 fork 语句中相应的组件。
当 PARTEXPRs 的计算结果都是唯一参与者时, .[case](https://docs.reach.sh/ref-programs-compute.html#%28reach.%28%28case%29%29%29) 组件可以重复很多次,就像 fork 语句中一样。
.timeRemaining
在 parallelReduce 中处理绝对期限时,TIMEOUTBLOCK 中有一种通用模式,即让参与者[竞争](https://docs.reach.sh/ref-programs-step.html#%28reach.%28%28race%29%29%29)发布并返回累加器。对于以下情况可以使用简写 .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 = INITEXPR;
[invariant](https://docs.reach.sh/ref-programs-consensus.html#%28reach.%28%28invariant%29%29%29)(INVARIANTEXPR)
[while](https://docs.reach.sh/ref-programs-consensus.html#%28reach.%28%28while%29%29%29)(CONDEXPR) {
[fork](https://docs.reach.sh/ref-programs-step.html#%28reach.%28%28fork%29%29%29)()
.case(PARTEXPR,
PUBLISH_EXPR,
PAY_EXPR,
() [=>](https://docs.reach.sh/ref-programs-compute.html#%28reach.%28%28~3d.~3e%29%29%29) {
LHS = CONSENSUS_EXPR;
[continue](https://docs.reach.sh/ref-programs-consensus.html#%28reach.%28%28continue%29%29%29); })
.timeout(DELAYEXPR, () [=>](https://docs.reach.sh/ref-programs-compute.html#%28reach.%28%28~3d._~3e%29%29%29)
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(AMOUNTEXPR,TOKEN_EXPR).to(PART)
其中 TOKEN_EXPR 是 [Token](https://docs.reach.sh/ref-programs-compute.html#%28reach.%28%28.Token%29%29%29) 类型,用于指定转移哪一种非网络代币。
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 就是远程函数。
远程函数可以通过使用合适的参数,然后返回指定输出的方式来调用。此外,远程函数可以通过以下操作之一进行扩展:
REMOTEFUN.[pay](https://docs.reach.sh/ref-programs-step.html#%28reach.%28%28pay%29%29%29)(AMT)—返回一个远程函数,当该函数被调用时,从调用方接收 AMT 个网络代币。
REMOTE_FUN.bill(AMT)—返回一个远程函数,当该函数返回时,向调用方提供 AMT 个网络代币。
REMOTEFUN.withBill()—返回一个远程函数,该函数返回时,向调用方提供一定数量的网络代币。确切的数量会以元组打包原始结果的方式返回,其中第一个元素是数量,第二个元素是原始结果。例如:
[const](https://docs.reach.sh/ref-programs-compute.html#%28reach.%28%28const%29%29%29) [ returned, randomValue ] =
randomOracle.getRandom.pay(stipend).withBill()();
这算是一种与随机预言机通信的方法,该预言机获得实际成本的保守近似值(即实际应支付的值),并返回未被使用部分。这种返回未使用部分的情形无法通过 REMOTEFUN.[bill](https://docs.reach.sh/ref-programs-consensus.html#%28reach.%28%28bill%29%29%29) 获得。
远程对象目前还不支持非网络代币的交互。
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[ADDREXPR]=VALUE_EXPR 来对 ADDR_EXPR 设定 VALUE_EXPR(类型为 TYPE_EXPR ),或通过 delete [map](https://docs.reach.sh/ref-programs-compute.html#%28reach.%28%28map%29%29%29)[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 。这类修改只能存在于共识步骤中。
s.member(ADDRESS) 会返回一个 Bool 值,代表该地址是否存在于集合中。