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);
其中:

当 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)
其中:

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 值,代表该地址是否存在于集合中。