Algorand Blockchain Development using Reach Part 4 Trust - Commitments
In the previous Bets & Wagers, we added the functionality to transfer funds between our participants “Alice” and “Bob”. Although, this is great and fundamental to all Blockchain development. It is dependant on an “honest” version of “Bob”. We will fix this in this tutorial.
Requirements
Please revisit Bets & Wagers,to get caught up.
Background
Anyone with an interest in dApp development targeting Algorand and Ethereum blockchains.
Steps
1. Defining the rules of Rock,Paper, Scissors
'reach 0.1';
The above code is the usual Reach version header.
const [ isHand, ROCK, PAPER, SCISSORS ] = makeEnum(3);
const [ isOutcome, B_WINS, DRAW, A_WINS ] = makeEnum(3);
The above code defines enumerations for the hands that may be played, as well as the outcomes of the game.
const winner = (handA, handB) =>
((handA + (4 - handB)) % 3);
The above code defines the function that computes the winner of the game.
2. Testing in Reach
When we first wrote Rock, Paper, Scissors!, we asked you to trust that this formula for computing the winner is correct, but is good to actually check. One way to check would be to implement a JavaScript frontend that didn’t interact with a real user, nor would it randomly generate values, but instead, it would return specific testing scenario values and check that the output is as expected. That’s a typical way to debug and is possible with Reach. However, Reach allows us to write such test cases directly into the Reach program as verification assertions.
assert(winner(ROCK, PAPER) == B_WINS);
assert(winner(PAPER, ROCK) == A_WINS);
assert(winner(ROCK, ROCK) == DRAW);
The above code makes an assertion that when Alice plays Rock and Bob plays Paper, then Bob wins as expected.
Reach’s automatic verification allows us to express even more powerful statements about our program’s behavior. For example, we can state that no matter what values are provided for handA and handB, winner will always provide a valid outcome:
forall(UInt, handA =>
forall(UInt, handB =>
assert(isOutcome(winner(handA, handB)))));
And we can specify that whenever the same value is provided for both hands, no matter what it is, winner always returns DRAW:
forall(UInt, (hand) =>
assert(winner(hand, hand) == DRAW));
The above examples both use forall, which allows Reach programmers to quantify over all possible values that might be provided to a part of their program. You might think that these theorems will take a very long time to prove, because they have to loop over all 1,552,518,092,300,708,935,148,…247 digits…,468,750,892,846,853,816,057,856 possibilities (e.g., Ethereum uses 256-bits for its unsigned integers) for the bits of handA (twice!) and handB.
Updating the Player interface
Let’s continue the program by specifying the participant interact interfaces for Alice and Bob. These will be mostly the same as before, except that we will also expect that each frontend can provide access to random numbers. We’ll use these later on to protect Alice’s hand.
const Player =
{ ...hasRandom, // <--- new!
getHand: Fun([], UInt),
seeOutcome: Fun([UInt], Null) };
The only update is different is the addition of hasRandom, from the Reach standard library, in the interface.
const Player = (Who) => ({
...stdlib.hasRandom, // <--- new!
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]}`);
},
});
Similarly, we only need to modify one line of our JavaScript frontend. The above allows each participant’s Reach code to generate random numbers as necessary.
Cryptographic Commitment Scheme
We’re now at the crucial juncture where we will implement the actual application and ensure that Alice’s hand is protected until after Bob reveals his hand. The simplest thing would be to have Alice just publish the wager, but this, of course, would just leave Bob vulnerable. We need Alice to be able to publish her hand, but also keep it secret. This is a job for a cryptographic commitment scheme. Reach’s standard library comes with makeCommitment to make this easier for you.
A.only(() => {
const _handA = interact.getHand();
The above line Alice compute her hand, but not declassify it.
const [_commitA,_saltA] = makeCommitment(interact,_handA);
The above code computes a commitment to the hand. It comes with a secret “salt” value that must be revealed later.
const [wager,commitA] = declassify([interact.wager,_commitA]);});
The above code has Alice declassify the commitment and her wager.
A.publish(wager,commitA).pay(wager);
commit();
The above code has her publish them and has her include the wager funds in the publication.
Knowledge Assertion
At this point, we can state the knowledge assertion that Bob can’t know either the hand or the “salt” and continue with his part of the program.
unknowable(B,A(_handA,_saltA));
The above code states the knowledge assertion.
B.only(() => {
interact.acceptWager(wager);
const handB = declassify(interact.getHand());});
B.publish(handB).pay(wager);
The above code is unchanged from our previous example.
commit();
The above code has the transaction commit, without computing the payout, we can’t yet, because Alice’s hand is not yet public.
Checking the commitment
We now return to Alice who can reveal her secrets
A.only(() => {
const [saltA,handA] = declassify([_saltA,_handA]);});
The above line has Alice declassify the secret information.
A.publish(saltA,handA);
The above line has her publish it.
checkCommitment(commitA,saltA,handA);
The above line checks that the published values match the original values. This will always be the case with honest participants, but dishonest participants may violate this assumption.
The rest of the program is unchanged from the original version, except that it uses the new names for the outcomes:
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(); });
Since we didn’t have to change the frontend in any meaningful way, the output of running ./reach run
is still the same as it ever was:
$ ./reach run
Alice played Scissors
Bob accepts the wager of 5.
Bob played Paper
Bob saw outcome Alice wins
Alice saw outcome Alice wins
Alice went from 10 to 14.9999.
Bob went from 10 to 4.9999.
$ ./reach run
Alice played Paper
Bob accepts the wager of 5.
Bob played Scissors
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 accepts the wager of 5.
Bob played Scissors
Bob saw outcome Draw
Alice saw outcome Draw
Alice went from 10 to 9.9999.
Bob went from 10 to 9.9999.
When we compile this version of the application, Reach’s automatic formal verification engine proves many theorems and protects us against a plethora of mistakes one might make when writing even a simple application like this. Non-Reach programmers that try to write decentralized applications are on their own trying to ensure that these problems don’t exist.
Here is a Youtube video showing the process
Here is the completed source.
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, // <--- new!
getHand: Fun([], UInt),
seeOutcome: Fun([UInt], Null) };
const Alice =
{ ...Player,
wager: UInt };
const Bob =
{ ...Player,
acceptWager: Fun([UInt], Null) };
export const main =
Reach.App(
{},
[['Alice', Alice], ['Bob', Bob]],
(A, B) => {
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);
commit();
A.only(() => {
const [saltA, handA] = declassify([_saltA, _handA]); });
A.publish(saltA, handA);
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(); });
And the index.mjs
frontend
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, // <--- new!
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]}`);
},
});
await Promise.all([
backend.Alice(ctcAlice, {
...Player('Alice'),
wager: stdlib.parseCurrency(5),
}),
backend.Bob(ctcBob, {
...Player('Bob'),
acceptWager: (amt) => {
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}.`);
})();