在上一节中,我们的“剪刀石头布”程序弥补了一个安全漏洞!这显然是让游戏可以好好玩下去的重大一步。在这节里,我们将重点讨论另一个去中心化应用独有的重要问题:不参与。
这里的不参与是指一方玩家不再继续游戏的行为。
在传统的客户端-服务器程序(如 Web 服务器)中,这个情况可能是客户机不再向服务器发送请求,或者服务器停止向客户机发送响应。此时,不参与通常会导致客户机出现错误消息,这最多只会导致服务器出现日志条目。出现这种情况时,传统程序可能需要回收如网络端口等资源,但如果程序以正常方式结束时,它们也需要这样做。换句话说,对于传统的客户端—服务器程序,程序员没有必要仔细考虑不参与的后果。
然而,去中心化应用程序必须仔细的考虑不参与的情况。以我们的剪刀石头布的游戏为例,如果Alice下了她的赌注后,Bob一直不接受,应用程序不再继续,会发生什么呢 ? 在这种时后,Alice的网络代币将被锁定在合约里。再比如,如果Bob接受并支付了他的赌注后,Alice就停止了参与,不提交她出的手势,那么两人的钱就都会被锁住。如果出现这些情况,双方都会遭受损失,他们对这样的结果的担忧会给交易带来额外的代价,这会降低游戏带给他们的利益。也许在这样的游戏里,这不太重要,但请记住,这个剪刀石头布是去中心化应用的缩影。
从技术上来说,在第一种情况下,当 Bob 启动应用程序失败时, Alice 的资金还没有锁定:因为 Bob 的身份直到他发出第一条消息后才被固定,所以Alice可以作为 Bob的 角色再进入游戏,然后赢得所有的资金,只是必须付出共识网络的交易成本。但在第二种情况下,任何一方对锁定的资金都没有追索权。
在本节余下部分,我们将讨论 Reach 如何解决不参与问题。若想了解更详细的讨论,请参阅关于不参与的指南一章。
—
在 Reach 中,不参与是通过一种“超时”机制来处理的,在这种机制下,如果共识转移的发起者未能在特定时间之前发布所需的内容,则每个共识转移都可以与所有参与者的一个步骤配对。我们把这种机制集成到我们的石头剪刀布中,并有意地在 JavaScript 测试程序中插入非参与程序,以观察结果。
首先,我们将修改参与者交互界面,让前端知道发生了超时。
tut-5/index.rsh
.. // ...
20 const Player =
21 { ...hasRandom,
22 getHand: Fun([], UInt),
23 seeOutcome: Fun([UInt], Null),
24 informTimeout: Fun([], Null) };
.. // ...
- 第 24 行引入了一个新方法informTimeout来告知超时 ,它不接收任何参数,也不返回任何信息。当超时发生时,我们将调用这个函数。
我们将对 JavaScript 前端做一些细微的调整,使其能够接收此消息并在控制台上显示它。
tut-5/index.mjs
.. // ...
20 const Player = (Who) => ({
21 ...stdlib.hasRandom,
22 getHand: () => {
23 const hand = Math.floor(Math.random() * 3);
24 console.log(`${Who} played ${HAND[hand]}`);
25 return hand;
26 },
27 seeOutcome: (outcome) => {
28 console.log(`${Who} saw outcome ${OUTCOME[outcome]}`);
29 },
30 informTimeout: () => {
31 console.log(`${Who} observed a timeout`);
32 },
33 });
.. // ...
在 Reach 程序中,我们将在程序的顶部定义一个标识符,以便在整个程序中使用该deadline。
tut-5/index.rsh
.. // ...
32 const DEADLINE = 10;
33 export const main =
.. // ...
- 第 32 行将 deadline 定义为十个时间单位,这是对共识网络中基本的时间概念的抽象。在许多网络中,比如以太坊,这个数字是一个块的数量。
接下来,在 Reach 应用程序的开始,我们将定义一个辅助函数,通过调用这个新方法来通知每个参与者超时。
tut-5/index.rsh
.. // ...
37 (A, B) => {
38 const informTimeout = () => {
39 each([A, B], () => {
40 interact.informTimeout(); }); };
41
42 A.only(() => {
.. // ...
- 第 38 行将函数定义为箭头表达式。
- 第 39 行让每个参与者执行一个本地步骤。
- 第 40 行让他们调用新的通知超时的方法。
我们不会更改 Alice 的第一条消息,因为她的不参与对这没有任何影响:如果她不开始比赛,那么没有对任何人不利。
tut-5/index.rsh
.. // ...
46 A.publish(wager, commitA)
47 .pay(wager);
.. // ...
不过,我们会对 Bob 的第一条消息进行调整,因为如果他未能参与,那么 Alice 会损失她的初始赌注。
tut-5/index.rsh
.. // ...
54 B.publish(handB)
55 .pay(wager)
56 .timeout(DEADLINE, () => closeTo(A, informTimeout));
.. // ...
- 第 56 行在 Bob 的程序里添加了超时处理程序。
超时处理程序指定:如果 Bob 没有在 DEADLINE 的时间内完成操作,则应用程序会调用箭头表达式给出的步骤。在这种情况下,超时代码是对 closeTo 的调用,这是一个 Reach 标准库函数,它向Alice发送消息并将合约中的所有资金转移给她自己,然后再调用给定的函数。这意味着,如果Bob未能公布他的牌,然后Alice将拿回她的代币。
我们将为 Alice 的第二条消息添加一个类似的超时处理程序。
tut-5/index.rsh
.. // ...
61 A.publish(saltA, handA)
62 .timeout(DEADLINE, () => closeTo(B, informTimeout));
.. // ...
但在这种情况下,如果Alice不参加,Bob将能够获得所有的资金。你可能觉得应该把 Alice 的资金还给 Alice ,把 Bob 的资金给 Bob ,这样就“公平”了。但是,如果我们以这种方式实现它,当Alice要输的话时后,她可以一直故意超时 ,因为她知道自己和Bob的手势。
为了完美应对不参与情况,以上就是我们需要对 Reach 程序代码所做的修改:只要七行!
—
接下来我们修改 JavaScript 前端,以便在轮到Bob 接受赌注时故意造成超时。
tut-5/index.mjs
.. // ...
35 await Promise.all([
36 backend.Alice(ctcAlice, {
37 ...Player('Alice'),
38 wager: stdlib.parseCurrency(5),
39 }),
40 backend.Bob(ctcBob, {
41 ...Player('Bob'),
42 acceptWager: async (amt) => { // <-- async now
43 if ( Math.random() <= 0.5 ) {
44 for ( let i = 0; i < 10; i++ ) {
45 console.log(` Bob takes his sweet time...`);
46 await stdlib.wait(1); }
47 } else {
48 console.log(`Bob accepts the wager of ${fmt(amt)}.`);
49 }
50 },
51 }),
.. // ...
- 第 42 行到第 50 行将 Bob 的 下注 方法重新定义为一个异步函数,其中一半的时间需要等待 10 个时间单位才能通过,在以太网上至少需要 10 个块。我们知道 10 是DEADLINE 的值,所以这会导致超时。
—
让我们运行该程序,看看会发生什么:
reach 运行 |
---|
Alice 出石头 |
Bob下5 注。 |
Bob出布 |
Bob看到了结果,Bob赢了 |
Alice 看到了 Bob 获胜的结果 |
Alice从10变成4.9999 |
Bob从10变成了14.9999。 |
resch 运行 |
Alice 出剪刀 |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob出剪刀 |
Bob观察到超时了 |
Alice观察到超时了 |
Alice从10变成了9.9999。 |
Bob从10变成了9.9999秒 |
reach运行 |
Alice出布 |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
Bob在故意消耗时间… |
.. |
Bob出剪刀 |
Bob观察到超时了 |
Alice观察超时了 |
Alice从10变成了9.9999。 |
Bob从10变成了9.9999。 |
当然,在你运行的时候,三次中可能有两次都不会以超时结束。
如果你的版本不能正常运行,看看完整的版本: tut - 5 / index . rsh 和 tut . 5 / Index . mj ,并确保你复制下来的一切正确!
现在我们的石头剪子布游戏的实现对于任何一个游戏参与者都是可行的。在下一步中,我们将扩展应用程序以禁止平局,并让 Alice 和 Bob 再次进行比赛,直到出现胜者。
您知道了吗?:在一个去中心化应用中,当一个参与者拒绝执行程序的下一步,例如,如果 Alice 拒绝和 Bob 一起玩剪刀石头布的游戏时,会发生什么?
- 这是不可能的,因为区块链保证每一方都执行特定的一组动作;
- 程序永远挂着等待 Alice 提供值;
- Alice 受到惩罚,项目继续进行,默认 Bob 是赢家;
- 这取决于程序的编写方式;如果开发人员使用 Reach ,则默认为( 2 ),但是开发人员可以包含一个超时块来实现( 3 )行为。 答案是:
答案是: 4;Reach 授权程序员使用他们想要的业务逻辑来设计应用程序。