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
- Functioning version of Reach Tutorial - Part 4: Trust - Commitments
- Familiarity with Reach Developer Documentation
Background
Anyone with an interest in Blockchain / Web Development or is just curious about how this all works. Explore these resources:
- Multi-Chain Blockchain Development in Reach (Udemy Course)
- Blockchain Developer Community (Youtube Channel)
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.