2.6 Timeouts and Participation

In the last section, we removed a security vulnerability from Rock, Paper, Scissors! that was a clear attack on the viability of the application. In this section, we’ll focus on a more subtle issue that is important and unique to decentralized applications: non-participation.

Non-participation refers to the act of one party ceasing to continue playing their role in an application.

In traditional client-server programs, like a Web server, this would be the case of a client stopping sending requests to the server, or the server stopping sending responses to the client. In these sorts of traditional programs, non-participation is an exceptional circumstances that normally leads to an error message for clients and, at most, a log entry for servers. Sometimes traditional programs will need to recycle resources, like network ports, on non-participation, but they would have also needed to do that if the transaction ended by normal means. In other words, for traditional client-server programs, it is not necessary for designers to meticulously consider the consequences of non-participation.

In contrast, decentralized applications must be carefully designed with an eye towards their behavior in the face of non-participation. For example, consider what happens in our Rock, Paper, Scissors! game if after Alice has paid her wager, Bob never accepts and the application doesn’t continue. In this case, Alice’s network tokens would be locked inside of the contract and lost to her. Similarly, if after Bob accepted and paid his wager, Alice stopped participating and never submitted her hand, then both their funds would be locked away forever. In each of these cases, both parties would be greatly hurt and their fear of that outcome would introduce an additional cost to transacting, which would lower the value they got from participating in the application. Of course, in a situation like Rock, Paper, Scissors! this is unlikely to be an important matter, but recall that Rock, Paper, Scissors! is a microcosm of decentralized application design.

Technically, in the first case, when Bob fails to start the application, Alice is not locked away from her funds: since Bob’s identity is not fixed until after his first message, she could start another instance of the game as the Bob role and then she’d win all of the funds, less any transaction costs of the consensus network. In the second case, however, there would be no recourse for either party.

In the rest of this section, we’ll discuss how Reach helps address non-participation. For a longer discussion, refer to the guide chapter on non-participation.

In Reach, non-participation is handled through a "timeout" mechanism whereby each consensus transfer can be paired with a step that occurs for all participants if the originator of the consensus transfer fails to make the required publication before a particular time. We’ll integrate this mechanism into our version of Rock, Paper, Scissors! and deliberately insert non-participation into our JavaScript testing program to watch the consequences play out.

First, we’ll modify the participant interact interface to allow the frontend to be informed that a timeout occurred.

..    // ...
20    const Player =
21          { ...hasRandom,
22            getHand: Fun([], UInt256),
23            seeOutcome: Fun([UInt256], Null),
24            informTimeout: Fun([], Null) };
..    // ...

We’ll make a slight tweak to our JavaScript frontend to be able to receive this message and display it on the console.

..    // ...
18    const Player = (Who) => ({
19      ...stdlib.hasRandom,
20      getHand: () => { const hand = Math.floor(Math.random()*3);
21                       console.log(`${Who} played ${HAND[hand]}`);
22                       return hand; },
23      seeOutcome: (outcome) =>
24      console.log(`${Who} saw outcome ${OUTCOME[outcome]}`),
25      informTimeout: () =>
26      console.log(`${Who} observed a timeout`) });
..    // ...

Back in the Reach program, we’ll define an identifier at the top of our program to use a standard deadline throughout the program.

..    // ...
32    const DEADLINE = 10;
33    export const main =
..    // ...

Next, at the start of the Reach application, we’ll define a helper function to inform each of the participants of the timeout by calling this new method.

..    // ...
37    (A, B) => {
38      const informTimeout = () => {
39        each([A, B], () => {
40          interact.informTimeout(); }); };
41    
42      A.only(() => {
..    // ...

We won’t change Alice’s first message, because there is no consequence to her non-participant: if she doesn’t start the game, then no one is any worse off.

..    // ...
46    A.publish(wager, commitA)
47      .pay(wager);
..    // ...

However, we will adjust Bob’s first message, because if he fails to participate, then Alice’s initial wager will be lost to her.

..    // ...
54    B.publish(handB)
55      .pay(wager)
56      .timeout(DEADLINE, () => closeTo(A, informTimeout));
..    // ...

The timeout handler specifies that if Bob does not complete perform this action within a time delta of DEADLINE, then the application transitions to step given by the arrow expression. In this case, the timeout code is a call to closeTo, which is a Reach standard library function that has Alice send a message and transfer all of the funds in the contract to herself, then call the given function afterwards. This means that if Bob fails to publish his hand, then Alice will take her network tokens back.

We will add a similar timeout handler to Alice’s second message.

..    // ...
61    A.publish(saltA, handA)
62      .timeout(DEADLINE, () => closeTo(B, informTimeout));
..    // ...

But in this case, Bob will be able to claim all of the funds if Alice doesn’t participate. You might think that it would be "fair" for Alice’s funds to be returned to Alice and Bob’s to Bob. However, if we implemented it that way, then Alice would be wise to always timeout if she were going to lose, which she knows will happen, because she knows her hand and Bob’s hand.

These are the only changes we need to make to the Reach program to make it robust against non-participation: seven lines!

Let’s modify the JavaScript frontend to deliberately cause a timeout sometimes when Bob is supposed to accept the wager.

..    // ...
28    await Promise.all([
29      backend.Alice(
30        stdlib, ctcAlice,
31        { ...Player('Alice'),
32          wager: toNetworkFormat('5')
33        }),
34      backend.Bob(
35        stdlib, ctcBob,
36        { ...Player('Bob'),
37          acceptWager: async (amt) => { // <-- async now
38            if ( Math.random() <= 0.5 ) {
39              for ( let i = 0; i < 10; i++ ) {
40                console.log(`  Bob takes his sweet time...`);
41                await stdlib.wait(1); }
42            } else {
43              console.log(`Bob accepts the wager of ${stdlib.fromWei(amt)}.`); } } } )
44    ]);
..    // ...

Let’s run the program and see what happens:

$ ./reach run

Alice played Rock

Bob accepts the wager of 5.0.

Bob played Paper

Bob saw outcome Bob wins

Alice saw outcome Bob wins

Alice went from 10.0 to 4.999999999999386833.

Bob went from 10.0 to 14.999999999999969143.

 

$ ./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.0 to 9.999999999999388565.

Bob went from 10.0 to 9.99999999999979.

 

$ ./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.0 to 9.999999999999388565.

Bob went from 10.0 to 9.99999999999979.

Of course, when you run, you may not get two of the three times ending in a timeout.

If your version isn’t working, look at the complete versions of tut-5/index.rsh and tut-5/index.mjs to make sure you copied everything down correctly!

Now our implementation of Rock, Paper, Scissors! is robust against either participant dropping from the game. In the next step, we’ll extend the application to disallow draws and have Alice and Bob play again until there is a winner.