这一节我们讨论处理不参与和超时问题的应对方式,确保各个用户的权益!
[Reach教程翻译] Reach是安全简单的Dapp开发语言 让用户可以像开发传统App一样开发DApp 目前使用Reach开发的智能合约可以部署在以太坊、Conflux、Algorand Reach官网 Reach官方文挡
在上一节中,我们的“石头剪刀布”程序弥补了一个安全漏洞!这显然是让游戏可以顺利进行的重大一步。在这节里,我们将重点讨论另一个去中心化应用独有的重要问题:不参与。
这里的不参与是指一方玩家不再继续游戏的行为。
在传统的客户端-服务器程序(如 Web 服务器)中,这个情况可能是客户机不再向服务器发送请求,或者服务器停止向客户机发送响应。此时,不参与通常会导致客户机出现错误消息,这最多只会导致服务器出现日志条目。出现这种情况时,传统程序可能需要回收如网络端口等资源,但如果程序以正常方式结束时,它们也需要这样做。换句话说,对于传统的客户端--服务器程序,程序员没有必要仔细考虑不参与的后果。
然而,去中心化应用程序必须仔细的考虑不参与的情况。以我们的石头剪刀布的游戏为例,如果Alice下了她的赌注后,Bob一直不接受,应用程序不再继续,会发生什么呢 ? 在这种时后,Alice的网络代币将被锁定在合约里。再比如,如果Bob接受并支付了他的赌注后,Alice就停止了参与,不提交她出的手势,那么两人的钱就都会被锁住。如果出现这些情况,双方都会遭受损失,他们对这样的结果的担忧会给交易带来额外的代价,从而降低游戏带给他们的利益。也许在这样的游戏里,这不太重要,但请记住,这个石头剪刀布是去中心化应用的缩影。
从技术上来说,在第一种情况下,当 Bob 启动应用程序失败时, Alice 的资金还没有锁定:因为 Bob 的身份直到他发出第一条消息后才被固定,所以Alice可以作为 Bob的 角色再进入游戏,然后赢得所有的资金,只是必须付出共识网络的交易成本。但在第二种情况下,任何一方对锁定的资金都没有索要权。
在本节余下部分,我们将讨论 Reach 如何解决不参与问题。若想了解更详细的讨论,请参阅关于不参与的指南一章。
在 Reach 中,不参与是通过一种“超时”机制来处理的,在这种机制下,如果共识转移的发起者未能在特定时间之前发布所需的内容,则“超时”机制会使每个参与者执行某个指定的步骤。我们把这种机制集成到我们的石头剪刀布中,并有意地在 JavaScript 测试程序中插入不参与程序,以观察结果。
首先,我们将修改参与者交互界面,让前端知道发生了超时。
.. // ...
20 const Player = {
21 ...hasRandom,
22 getHand: Fun([], UInt),
23 seeOutcome: Fun([UInt], Null),
24 informTimeout: Fun([], Null),
25 };
.. // ...
我们稍微调整一下 JavaScript 前端,使其在接收到此消息时在控制台上显示谁超时了 :
.. // ...
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。
.. // ...
28 const Alice = Participant('Alice', {
29 ...Player,
30 wager: UInt, // atomic units of currency
31 deadline: UInt, // time delta (blocks/rounds)
32 });
.. // ...
接下来,在 Reach 应用程序的一开始,我们将定义一个辅助函数,通过调用这个新方法来通知每个参与者超时的发生。
.. // ...
39 const informTimeout = () => {
40 each([Alice, Bob], () => {
41 interact.informTimeout();
42 });
43 };
.. // ...
我们不会更改 Alice 的第一步,因为此时她的不参与不产生任何影响:如果她不开始比赛,那么没有对任何人不利。
.. // ...
45 Alice.only(() => {
46 const wager = declassify(interact.wager);
47 const _handAlice = interact.getHand();
48 const [_commitAlice, _saltAlice] = makeCommitment(interact, _handAlice);
49 const commitAlice = declassify(_commitAlice);
50 const deadline = declassify(interact.deadline);
51 });
52 Alice.publish(wager, commitAlice, deadline)
53 .pay(wager);
54 commit();
.. // ...
deadline
解密。不过,我们会对 Bob 的第一条动作进行调整,因为如果他不参与,那么 Alice 会损失她初始的赌注。
.. // ...
61 Bob.publish(handBob)
62 .pay(wager)
63 .timeout(relativeTime(deadline), () => closeTo(Alice, informTimeout));
64 commit();
.. // ...
超时处理程序指定:如果 Bob 没有在 DEADLINE 的时间内完成操作,则应用程序会调用箭头表达式给出的步骤。在这里,这个步骤是对Reach 标准库函数 closeTo 的调用,它向 Alice 发送消息并将合约中的所有资金转移给她自己,然后再调用给定的函数,即 informTimeout 。这意味着,如果 Bob 未能公布他出的拳,则 Alice 将拿回她的代币。**
我们接着为 Alice 的第二个动作添加一个类似的超时处理程序。
.. // ...
70 Alice.publish(saltAlice, handAlice)
71 .timeout(relativeTime(deadline), () => closeTo(Bob, informTimeout));
.. // ...
但在这种情况下,如果 Alice 不参加,Bob 将能够获得所有的代币。你可能觉得应该把 Alice 的代币还给 Alice ,把 Bob 的代币还给 Bob ,这样就“公平”了。但是,如果我们这样实现它,当 Alice 要输的时后 ( 她可以知道是因为她知道自己和 Bob 的手势 ) ,她可以一直故意超时。
为了完美应对不参与情况,以上就是我们需要对 Reach 程序代码所做的修改:只要七行!
接下来我们修改 JavaScript 前端,让 Bob 在轮到他接受赌注时故意造成超时。 rps-6-timeouts/index.mjs
.. // ...
35 await Promise.all([
36 backend.Alice(ctcAlice, {
37 ...Player('Alice'),
38 wager: stdlib.parseCurrency(5),
39 deadline: 10,
40 }),
41 backend.Bob(ctcBob, {
42 ...Player('Bob'),
43 acceptWager: async (amt) => { // <-- async now
44 if ( Math.random() <= 0.5 ) {
45 for ( let i = 0; i < 10; i++ ) {
46 console.log(` Bob takes his sweet time...`);
47 await stdlib.wait(1);
48 }
49 } else {
50 console.log(`Bob accepts the wager of ${fmt(amt)}.`);
51 }
52 },
53 }),
54 ]);
.. // ...
acceptWager
方法重新定义为一个异步函数,其中一半的时间需要等待 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.
在你运行的时候,可能多试几次才会出现一次超时的结果。
如果你的版本不能正常运行,看看完整的版本: rps-6-timeouts/index.rsh 和 rps-6-timeouts/index.mjs ,并确保你复制下来的一切正确!
现在我们的石头剪刀布游戏的实现对于任何一个游戏参与者都是可行的。在下一步中,我们将进一步修改应用程序以禁止平手,并让 Alice 和 Bob 接着猜拳,直到出现胜者。
您知道了吗?:
在一个去中心化应用中,当一个参与者拒绝执行程序的下一步,例如,如果 Alice 拒绝和 Bob 一起玩石头剪刀布的游戏时,会发生什么?
答案是:
4;Reach 让程序员可以使用他们想要的业务逻辑来设计应用程序。
欢迎关注Reach微信公众号 并在公众号目录 -> 加入群聊 选择加入官方开发者微信群与官方Discord群 与更多Reach开发者一起学习交流!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!