Tutorials
No Results
Tutorial

Intermediate · 30 minutes

Algorand Blockchain Development using Reach Part 5 Non Participation

In the previous tutorial, we removed a security vulnerability from Rock, Paper, Scissors!. We fixed an apparent attack on the viability of our application. This tutorial will focus our efforts on a problem in both centralized and decentralized applications: non-participation.

Put merely, non-participation is the act of one party ceasing to continue playing their role in an application.

Requirements

Background

Anyone with an interest in Blockchain / Web Development or is just curious about how this all works. Explore these resources:

Steps

What is Non Participation in decentralized applications

Non participation is when a client fails to send required requests to the server, or the server stops sending responses to the client. Decentralized applications must have these design decisions embedded in their behavior in the face of non-participation where our participants Alice and Bob stop communication during a game play.

Tip

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.

Modify the participant interact interface

In Reach, non-participation is handled through a “timeout” mechanism 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 modify the participant interact interface to allow the frontend to be informed that a timeout occurred.

const Player =
    { ...hasRandom,
          getHand: Fun([], UInt),
          seeOutcome: Fun([UInt], Null),
          informTimeout: Fun([], Null) };

The above code introduces a new method, informTimeout, that receives no arguments and returns no information. We’ll call this function when a timeout occurs.

Now we’ll make a slight modfication to the Javascript frontend to recieve the message and display it to the console.

index.mjs

const Player = (Who) => ({
     ...stdlib.hasRandom,
     getHand: () => {
      const hand = Math.floor(Math.random() * 3);
    console.log(`${Who} played ${HAND[hand]}`);
      return hand;
    },
    seeOutcome: (outcome) => {
      console.log(`${Who} saw outcome ${OUTCOME[outcome]}`);
},
    informTimeout: () => {
  console.log(`${Who} observed a timeout`);
    },
  });

We will now return to our Reach program and define an identifier at the top of our program to use a standard deadline throughout the program.

Update Index.rsh to handle timeOut

index.rsh

    const DEADLINE = 10;
    export const main =

The above code defines the deadline as ten time delta units, which are an abstraction of the underlying notion of time in the consensus network. In many networks, like Ethereum, this number is a number of blocks.

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

    (A, B) => {

      const informTimeout = () => {

The above code defines the function as an arrow expression

        each([A, B], () => {
          interact.informTimeout(); }); };
      A.only(() => {

The above code has each of the participants perform a local step and has them call the new informTimeout method.

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.

index.rsh

A.publish(wager, commitA)
    .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.

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

The above code adds a timeout handler to Bob’s publication.

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.

Next we will add a similar timeout handler to Alice’s second message

index.rsh

A.publish(saltA, handA)
    .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.

index.mjs

    await Promise.all([
      backend.Alice(ctcAlice, {
        ...Player('Alice'),
        wager: stdlib.parseCurrency(5),
      }),
      backend.Bob(ctcBob, {
        ...Player('Bob'),
        acceptWager: async (amt) => { // <-- async now
          if ( Math.random() <= 0.5 ) {
            for ( let i = 0; i < 10; i++ ) {
              console.log(`  Bob takes his sweet time...`);
              await stdlib.wait(1); }
          } else {
            console.log(`Bob accepts the wager of ${fmt(amt)}.`);
          }
        },
      }),

The above code redefine Bob’s acceptWager method as an asynchronous function where half of the time it will take at least ten blocks on the Ethereum network by waiting for ten units of time to pass. We know that ten is the value of DEADLINE, so this will cause a timeout.

Compile and Display Output

Here is some simulated output.

$ ./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.

Completed Source Code

index.rsh

'reach 0.1';

const [ isHand, ROCK, PAPER, SCISSORS ] = makeEnum(3);
const [ isOutcome, B_WINS, DRAW, A_WINS ] = makeEnum(3);

const winner = (handA, handB) =>
      ((handA + (4 - handB)) % 3);

assert(winner(ROCK, PAPER) == B_WINS);
assert(winner(PAPER, ROCK) == A_WINS);
assert(winner(ROCK, ROCK) == DRAW);

forall(UInt, handA =>
  forall(UInt, handB =>
    assert(isOutcome(winner(handA, handB)))));

forall(UInt, (hand) =>
  assert(winner(hand, hand) == DRAW));

const Player =
      { ...hasRandom,
        getHand: Fun([], UInt),
        seeOutcome: Fun([UInt], Null),
        informTimeout: Fun([], Null) };
const Alice =
      { ...Player,
        wager: UInt };
const Bob =
      { ...Player,
        acceptWager: Fun([UInt], Null) };

const DEADLINE = 10;
export const main =
  Reach.App(
    {},
    [['Alice', Alice], ['Bob', Bob]],
    (A, B) => {
      const informTimeout = () => {
        each([A, B], () => {
          interact.informTimeout(); }); };

      A.only(() => {
        const _handA = interact.getHand();
        const [_commitA, _saltA] = makeCommitment(interact, _handA);
        const [wager, commitA] = declassify([interact.wager, _commitA]); });
      A.publish(wager, commitA)
        .pay(wager);
      commit();

      unknowable(B, A(_handA, _saltA));
      B.only(() => {
        interact.acceptWager(wager);
        const handB = declassify(interact.getHand()); });
      B.publish(handB)
        .pay(wager)
        .timeout(DEADLINE, () => closeTo(A, informTimeout));
      commit();

      A.only(() => {
        const [saltA, handA] = declassify([_saltA, _handA]); });
      A.publish(saltA, handA)
        .timeout(DEADLINE, () => closeTo(B, informTimeout));
      checkCommitment(commitA, saltA, handA);

      const outcome = winner(handA, handB);
      const [forA, forB] =
            outcome == A_WINS ? [2, 0] :
            outcome == B_WINS ? [0, 2] :
            [1, 1];
      transfer(forA * wager).to(A);
      transfer(forB * wager).to(B);
      commit();

      each([A, B], () => {
        interact.seeOutcome(outcome); });
      exit(); });

index.mjs

import { loadStdlib } from '@reach-sh/stdlib';
import * as backend from './build/index.main.mjs';

(async () => {
  const stdlib = await loadStdlib();
  const startingBalance = stdlib.parseCurrency(10);
  const accAlice = await stdlib.newTestAccount(startingBalance);
  const accBob = await stdlib.newTestAccount(startingBalance);

  const fmt = (x) => stdlib.formatCurrency(x, 4);
  const getBalance = async (who) => fmt(await stdlib.balanceOf(who));
  const beforeAlice = await getBalance(accAlice);
  const beforeBob = await getBalance(accBob);

  const ctcAlice = accAlice.deploy(backend);
  const ctcBob = accBob.attach(backend, ctcAlice.getInfo());

  const HAND = ['Rock', 'Paper', 'Scissors'];
  const OUTCOME = ['Bob wins', 'Draw', 'Alice wins'];
  const Player = (Who) => ({
    ...stdlib.hasRandom,
    getHand: () => {
      const hand = Math.floor(Math.random() * 3);
      console.log(`${Who} played ${HAND[hand]}`);
      return hand;
    },
    seeOutcome: (outcome) => {
      console.log(`${Who} saw outcome ${OUTCOME[outcome]}`);
    },
    informTimeout: () => {
      console.log(`${Who} observed a timeout`);
    },
  });

  await Promise.all([
    backend.Alice(ctcAlice, {
      ...Player('Alice'),
      wager: stdlib.parseCurrency(5),
    }),
    backend.Bob(ctcBob, {
      ...Player('Bob'),
      acceptWager: async (amt) => { // <-- async now
        if ( Math.random() <= 0.5 ) {
          for ( let i = 0; i < 10; i++ ) {
            console.log(`  Bob takes his sweet time...`);
            await stdlib.wait(1); }
        } else {
          console.log(`Bob accepts the wager of ${fmt(amt)}.`);
        }
      },
    }),
  ]);

  const afterAlice = await getBalance(accAlice);
  const afterBob = await getBalance(accBob);

  console.log(`Alice went from ${beforeAlice} to ${afterAlice}.`);
  console.log(`Bob went from ${beforeBob} to ${afterBob}.`);

})();

We now have a foundation that all of our dApps may fundamentally build upon. Our implementation of Rock, Paper, Scissors! is robust against either participant dropping from the game.

Reach

January 14, 2021