2.6 超时问题的处理

·  阅读 405

原文链接


在上一节中,我们的“石头剪刀布”程序弥补了一个安全漏洞!这显然是让游戏可以顺利进行的重大一步。在这节里,我们将重点讨论另一个去中心化应用独有的重要问题:不参与。
这里的不参与是指一方玩家不再继续游戏的行为。

在传统的客户端-服务器程序(如 Web 服务器)中,这个情况可能是客户机不再向服务器发送请求,或者服务器停止向客户机发送响应。此时,不参与通常会导致客户机出现错误消息,这最多只会导致服务器出现日志条目。出现这种情况时,传统程序可能需要回收如网络端口等资源,但如果程序以正常方式结束时,它们也需要这样做。换句话说,对于传统的客户端--服务器程序,程序员没有必要仔细考虑不参与的后果。
然而,去中心化应用程序必须仔细的考虑不参与的情况。以我们的石头剪刀布的游戏为例,如果 Alice 下了她的赌注后, Bob 一直不接受,应用程序不再继续,会发生什么呢 ? 在这种时后, Alice 的网络代币将被锁定在合约里。再比如,如果 Bob 接受并支付了他的赌注后, Alice 就停止了参与,不提交她出的手势,那么两人的钱就都会被锁住。如果出现这些情况,双方都会遭受损失,他们对这样的结果的担忧会给交易带来额外的代价,从而降低游戏带给他们的利益。也许在这样的游戏里,这不太重要,但请记住,这个石头剪刀布是去中心化应用的缩影。

从技术上来说,在第一种情况下,当 Bob 启动应用程序失败时, Alice 的资金还没有锁定:因为 Bob 的身份直到他发出第一条消息后才被固定,所以 Alice 可以作为 Bob 的 角色再进入游戏,然后赢得所有的资金,只是必须付出共识网络的交易成本。但在第二种情况下,任何一方对锁定的资金都没有索要权。

在本节余下部分,我们将讨论 Reach 如何解决不参与问题。若想了解更详细的讨论,请参阅关于不参与的指南一章。

在 Reach 中,不参与是通过一种“超时”机制来处理的,在这种机制下,如果共识转移的发起者未能在特定时间之前发布所需的内容,则“超时”机制会使每个参与者执行某个指定的步骤。我们把这种机制集成到我们的石头剪刀布中,并有意地在 JavaScript 测试程序中插入不参与程序,以观察结果。

首先,我们将修改参与者交互界面,让前端知道发生了超时。
tut-6/index.rsh

..    // ...
20    const Player =
21          { ...hasRandom,
22            getHand: Fun([], UInt),
23            seeOutcome: Fun([UInt], Null),
24            informTimeout: Fun([], Null) };
..    // ...
复制代码
  • 第 24 行引入了一个新方法 informTimeout 来告知超时 ,它不接收任何参数,也不返回任何信息。当超时发生时,我们会调用这个函数。

我们稍微调整一下 JavaScript 前端,使其在接收到此消息时在控制台上显示谁超时了(第 30~31 行) :
tut-6/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-6/index.rsh

..    // ...
32    const DEADLINE = 10;
33    export const main =
..    // ...
复制代码
  • 第 32 行将 deadline 定义为十个时间单位,这是对共识网络中基本的时间概念的抽象。在许多网络中,比如以太坊,这个数字是一个块的数量。

接下来,在 Reach 应用程序的开始,我们将定义一个辅助函数,通过调用这个新方法来通知每个参与者超时的发生。
tut-6/index.rsh

..    // ...
37    (A, B) => {
38      const informTimeout = () => {
39        each([A, B], () => {
40          interact.informTimeout(); }); };
41    
42      A.only(() => {
..    // ...
复制代码
  • 第 38 行将函数定义为箭头表达式。
  • 第 39 行让每个参与者执行一个本地步骤。
  • 第 40 行让他们调用新的通知超时的方法。

我们不会更改 Alice 的第一条动作,因为此时她的不参与不产生任何影响:如果她不开始比赛,那么没有对任何人不利。
tut-6/index.rsh

..    // ...
46    A.publish(wager, commitA)
47      .pay(wager);
..    // ...
复制代码

不过,我们会对 Bob 的第一条动作进行调整,因为如果他不参与,那么 Alice 会损失她初始的赌注。
tut-6/index.rsh

..    // ...
54    B.publish(handB)
55      .pay(wager)
56      .timeout(DEADLINE, () => closeTo(A, informTimeout));
..    // ...
复制代码
  • 第 56 行在 Bob 的程序里添加了超时处理程序。

超时处理程序指定:如果 Bob 没有在 DEADLINE 的时间内完成操作,则应用程序会调用箭头表达式给出的步骤。在这里,这个步骤是对Reach 标准库函数 closeTo 的调用,它向 Alice 发送消息并将合约中的所有资金转移给她自己,然后再调用给定的函数,即 informTimeout 。这意味着,如果 Bob 未能公布他的牌,则 Alice 将拿回她的代币。
我们将为 Alice 的第二个动作添加一个类似的超时处理程序。
tut-6/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-6/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 run
Alice played Rock
Bob accepts the wager of 5.
Bob played Paper
Bob saw outcome Bob wins
Alice saw outcome Bob wins
Alice went from 10 to 4.9999.
Bob went from 10 to 14.9999.

$ ./reach run
Alice played Scissors
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
Bob played Scissors
Bob observed a timeout
Alice observed a timeout
Alice went from 10 to 9.9999.
Bob went from 10 to 9.9999.
 
$ ./reach run
Alice played Paper
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
  Bob takes his sweet time...
Bob played Scissors
Bob observed a timeout
Alice observed a timeout
Alice went from 10 to 9.9999.
Bob went from 10 to 9.9999.
复制代码


当然,在你运行的时候,三次中可能有两次都不会以超时结束。

如果你的版本不能正常运行,看看完整的版本: tut - 6 / index . rshtut . 6 / Index . mj ,并确保你复制下来的一切正确!

现在我们的石头剪刀布游戏的实现对于任何一个游戏参与者都是可行的。在下一步中,我们将扩展应用程序以禁止平局,并让 Alice 和 Bob 再次进行比赛,直到出现胜者。

您知道了吗?:

在一个去中心化应用中,当一个参与者拒绝执行程序的下一步,例如,如果 Alice 拒绝和 Bob 一起玩石头剪刀布的游戏时,会发生什么?

  1. 这是不可能的,因为区块链保证每一方都执行特定的一组动作;
  2. 程序永远挂着等待 Alice 提供值;
  3. Alice 受到惩罚,项目继续进行,默认 Bob 是赢家;
  4. 这取决于程序的编写方式;如果开发人员使用 Reach ,则默认为( 2 ),但是开发人员可以包含一个超时块来实现( 3 )行为。

答案是: 4;Reach 让程序员使用他们想要的业务逻辑来设计应用程序。

上一篇 : 2.5 信任和约定

下一篇 : 2.7 平局则继续比赛直至判断胜负

分类:
开发工具
标签:
收藏成功!
已添加到「」, 点击更改