On this page:
4.1.1 Problem Analysis
4.1.2 Data Definition
4.1.3 Communication Construction
4.1.4 Assertion Insertion
4.1.5 Interaction Introduction
4.1.6 Deployment Decisions
4.1.7 Discussion

4.1 Workshop: Hash Lock

In this workshop, we’ll design an application that allows a payer to lock funds with a secret password, independent from their consensus network identity, which can be drawn by anyone possessing the secret password. This is a useful way for a payer to show that they have funds and have committed to disbursing them, without deciding beforehand who they are paying.

This workshop is independent of all others.

We assume that you’ll go through this workshop in a directory named ~/reach/workshop-hash-lock:
  $ mkdir -p ~/reach/workshop-hash-lock && cd ~/reach/workshop-hash-lock

And that you have a copy of Reach installed in ~/reach so you can write
  $ ../reach version

And it will run Reach.

You should start off by initializing your Reach program:
  $ ../reach init

4.1.1 Problem Analysis

The first step in any program design is to perform problem analysis and determine what information is relevant to the problem. When writing decentralized applications in Reach, this information analysis includes an analysis of the set of participants involved in a computation.

In this case, let’s ask the questions:
  • Who is involved in this application?

  • What information do they know at the start of the program?

  • What information are they going to discover and use in the program?

  • What funds change ownership during the application and how?

You should write your answers in your Reach program (index.rsh) using a comment. /* Remember comments are written like this. */

Write down the problem analysis of this program as a comment.

Let’s see how your answers compare to our answers:

It’s okay if your answers are different than ours. Problem analysis is a "loose" process that is more like creative artistry than it is like rote calculation. But, that doesn’t mean it is superfluous and unnecessary or unneeded.

Problem analysis is a crucial step that helps us understand what our application is supposed to be doing. Remember, programming in general, and Reach in particular, does not solve problems for you; instead, programs encode automatic solutions to problems you’ve already solved. Compared to normal languages, Reach does do a bit automatically for you: it automatically discovers problems you may not have realized your program had. You still have to solve them yourself though! But, at least you know about them because of Reach.

4.1.2 Data Definition

Humans and their social systems deal with information, but computers can only interact with data, which is merely a representation of information using particular structures, like numbers, arrays, and so on. After problem analysis, we know what information our program will deal with, but next we need to decide how to translate that information into concrete data.

So, for this program, we should decide:
  • What data type will represent the amount Alice transfers?

  • What data type will represent Alice’s password?

Refer to Types for a reminder of what data types are available in Reach.

After deciding those things, you should think about how the program will be provided these values. In other words:

You should look back at your problem analysis to do this step. Whenever a participant starts off knowing something, then it is a field in the interact object. If they learn something, then it will be an argument to a function. If they provide something later, then it will be the result of a function.

You should write your answers in your Reach file (index.rsh) as the participant interact interface for each of the participants.

Write down the data definitions for this program as definitions.

Let’s compare notes again.
  • We’re going to represent the amount Alice transfers as an unsigned integer (UInt) named amt.

  • We will represent the password as another unsigned integer (UInt) named pass.

  • These two values are the only fields of Alice’s interface, but Bob will have a function named getPass that will return the password that he knows.

We wrote this in our program as:

[Participant('Alice', { amt : UInt,
             pass: UInt }),
 Participant('Bob', { getPass: Fun([], UInt) }) ],

It would be very surprising if you choose the exact same names as us in your code, but did you choose the same types? We expect that many of you might have chosen to represent the password by a string of bytes using the Reach type, Bytes. There’s nothing necessarily wrong with this option, but we did not choose it because it is hard to decide exactly how long to make it, but we are satisfied with an unsigned integer, because it has a minimum of 64 bits on typical consensus networks.

At this point, you can modify your JavaScript file (index.mjs) to contain definitions of these values, although you may want to use a placeholder like 42 or something for the actual value. When you’re writing a Reach program, especially in the early phases, you should have these two files open side-by-side and update them in tandem as you’re deciding the participant interact interface.

4.1.3 Communication Construction

A fundamental aspect of a decentralized application is the pattern of communication and transfer among the participants, including the consensus network. For example, who initiates the application? Who responds next? Is there a repeated segment of the program that occurs over and over again? We should explicitly write down this structure as comments in our program. For example, for the tutorial version of Rock, Paper, Scissors!, we might write:

// Alice publishes the wager and pays it
// Bob accepts the wager and pays it
// While there's a draw
//  Alice publishes her hand secretly
//  Bob publishes his hand publicly
//  Alice reveals her hand
//  The consensus ensures it's the same hand as before
// The consensus pays out the wager

You should do this now, in your Reach program (index.rsh).

Write down the communication pattern for this program as comments.

This is a simple application, so we should all share the same communication pattern. Here’s what we wrote:

// Alice pays the amount
// Bob publishes the password
// The consensus ensures it's the right password and pays Bob

However, looking at this pattern reveals a subtlety in this application: how can the consensus ensure that Bob publishes the correct password? The only way is for Alice to publish something first that can be checked by the consensus. For example, we could use the pattern:

// Alice publishes the password and pays the amount
// Bob publishes the password
// The consensus ensures it's the right password and pays Bob

However, this is definitely wrong, because Alice doesn’t want to share her password with the world across the network, she only wants to share it with Bob, potentially at some later moment. So, she should not publish the password, but instead, publish a digest of the password, that can be checked against the actual password later. In other words, we should use a pattern like:

// Alice publishes a digest of the password and pays the amount
// Bob publishes the password
// The consensus ensures it's the right password and pays Bob

It is cheaper to go through this iteration process in the human-centered design phase than in the code-centered programming phase, even when you’re using a high-level language like Reach for programming.

Next, we need to convert this pattern into actual program code using publish, pay, and commit.

Write down the communication pattern for this program as code.

The body of your application should look something like:

// Alice publishes a digest of the password and pays the amount
Alice.publish(passDigest, amt)

// Bob publishes the password

// The consensus ensures it's the right password and pays Bob

We can now move on to the next part of designing a decentralized application: verification.

4.1.4 Assertion Insertion

When we are programming, we hold a complex theory of the behavior of the program inside of our minds that helps us know what should happen next in the program based on what has happened before and what is true at every point in the program. As programs become more complex, this theory becomes more and more difficult to grasp, so we might make mistakes. Furthermore, when another programmer reads our code (such as a version of ourselves from the future trying to modify the program), it can be very difficult to understand this theory for ourselves. Assertions are ways of encoding this theory directly into the text of the program in a way that will be checked by Reach and available to all future readers and editors of the code.

Look at your application. What are the assumptions you have about the values in the program?

Write down the properties you know are true about the various values in the program.

There are three main assumptions we came up with for this program:
  • Before Bob publishes the password, it is unknowable by him and everyone else except Alice.

  • Bob assumes that the password digest published by Alice matches the digest of the password he’s publishing.

  • The consensus requires that Alice’s digest and the digest of Bob’s password match.

We expect that the third of these is the least controversial and the most obvious property, but the others are important too. The first property essentially guarantees that the erroneous version of the application we contemplated, where Alice directly sent her password over the network, is disallowed. The second property encodes Bob’s assumption of good will and integrity when he submits his value: an honest version of the Bob participant would not willingly send a password that wasn’t the correct one. Furthermore, it is possible for any participant to check, without going through consensus, if they know what the password is.

Now that we know what the properties are, we need to encode them into our program via calls to Reach functions like unknowable, assume, and require. Let’s do that now.

Insert assertions into the program corresponding to facts that should be true.

Here’s what we did:

// First
unknowable(Bob, Alice(_pass));

Bob.only(() => {
 // Second
 assume( passDigest == digest(pass) ); });

// Third
require( passDigest == digest(pass) );

At this point, we are almost ready to complete our program and make it so that we can run it. You’ve probably noticed that in our samples, the variables pass, amt, and passDigest are undefined. We’ll handle that next.

4.1.5 Interaction Introduction

A key concept of Reach programs is that they are concerned solely with the communication and consensus portions of a decentralized application. Frontends are responsible for all other aspects of the program. Thus, eventually a Reach programmer needs to insert calls into their code to send data to and from the frontend via the participant interact interfaces that they defined during the Data Definition step.

In our program, that means defining amt and passDigest by Alice and pass by Bob. Do that now.

Insert interact calls to the frontend into the program.

Here’s what we did:

 1    'reach 0.1';
 2    'use strict';
 4    export const main = Reach.App(
 5      { },
 6      [Participant('Alice', { amt : UInt,
 7                   pass: UInt }),
 8       Participant('Bob', { getPass: Fun([], UInt) }) ],
 9      (Alice, Bob) => {
10        Alice.only(() => {
11          const _pass = interact.pass;
12          const [ amt, passDigest ] =
13                declassify([ interact.amt,
14                             digest(_pass) ]); });
15        Alice.publish(passDigest, amt)
16          .pay(amt);
17        commit();
19        unknowable(Bob, Alice(_pass));
20        Bob.only(() => {
21          const pass = declassify(interact.getPass());
22          assume( passDigest == digest(pass) ); });
23        Bob.publish(pass);
24        require( passDigest == digest(pass) );
25        transfer(amt).to(Bob);
26        commit();
28        exit(); } );

Did you notice that we didn’t mention what line 5 is for? We’ll discuss that in the next section; don’t worry!

At this point, when we

  $ ../reach compile

We’ll get a happy message that all our theorems are true. Great job! But we still need to run our program!

4.1.6 Deployment Decisions

At this point, we need to decide how we’re going to deploy this program and really use it in the world. We need to decide how to deploy the contract, as well as what kind of user interaction modality we’ll implement inside of our frontend.

Decide how you will deploy and use this application.

Unfortunately, on many consensus networks, like Ethereum and Algorand, this application is dangerous to run. The problem is that a malicious miner, like Eve, can intercept Bob’s message that provides him the funds, refuse to forward it through to the consensus network, take the password from it, and submit it for her own account. There is not a good general solution to this problem, meaning a theorem that we could insert into our program to make sure this attack isn’t possible, because the whole point of this application is that Bob’s identity is not known at the time that Alice sends the first message. Ideally, such networks would support a kind of cryptographic operation where Bob could prove that he knows the password without revealing it. There are some ideas on how to provide this sort of thing through zero-knowledge proofs and homomorphic encryption, but there is no widely accepted and available solution.

In short: Don’t run this program. If you want to do something like this, then continue to the next workshop on relays. If you want to do exactly this, then stay tuned for a more complex zero-knowledge version.

Next, we’ll settle for a simple testing program for now to show the application, and let the rest of our full stack team deal with actually building the interface. Here’s the JavaScript frontend we wrote:

 1    import { loadStdlib } from '@reach-sh/stdlib';
 2    import * as backend from './build/index.main.mjs';
 4    (async () => {
 5      const stdlib = await loadStdlib();
 6      const startingBalance = stdlib.parseCurrency(100);
 8      const accAlice = await stdlib.newTestAccount(startingBalance);
 9      const accBob = await stdlib.newTestAccount(startingBalance);
11      const getBalance = async (who) =>
12            stdlib.formatCurrency(await stdlib.balanceOf(who), 4);
13      const beforeAlice = await getBalance(accAlice);
14      const beforeBob = await getBalance(accBob);
16      const ctcAlice = accAlice.deploy(backend);
17      const ctcBob = accBob.attach(backend, ctcAlice.getInfo());
19      const thePass = stdlib.randomUInt();
21      await Promise.all([
22        backend.Alice(ctcAlice, {
23          amt: stdlib.parseCurrency(25),
24          pass: thePass,
25        }),
26        backend.Bob(ctcBob, {
27          getPass: () => {
28            console.log(`Bob asked to give the preimage.`);
29            console.log(`Returning: ${thePass}`);
30            return thePass;
31          },
32        }),
33      ]);
35      const afterAlice = await getBalance(accAlice);
36      const afterBob = await getBalance(accBob);
38      console.log(`Alice went from ${beforeAlice} to ${afterAlice}.`);
39      console.log(`Bob went from ${beforeBob} to ${afterBob}.`);
41    })();

In this case, Bob learns the password outside of the Reach program by directly sharing memory with Alice. In a real deployment, she might give Bob the password through some other channel, like an encrypted email message, or a calligraphic scroll delivered by raven or intoned from Himalayan cliffs.

With this testing frontend in place, we can run

  $ ../reach run

and see an example execution:

$ ../reach run

Bob asked to give the preimage.

Returning: 40816662354916515903581596667174503941307255426903039386763272451578996162763

Alice went from 100.0 to 74.999999999999823944.

Bob went from 100.0 to 124.999999999999978599.

4.1.7 Discussion

You did it!

You implemented a Reach program totally on your own, with only a little bit of prodding.

Unlike the tutorial, this workshop uses a "top-down" perspective on Reach application design, where you derive the program from the requirements and slowly fill out the shell, while knowing that each step was correct before moving on. In contrast, in the tutorial, we demonstrated a "bottom-up" style where you start implementing the easy parts and realize the problems and their fixes as you go. There’s no right way to program and in our own Reach development, we use a combination of the two tactics. Try both and keep them both in mind during your own development.

If you found this workshop rewarding, please let us know on the Discord community!

If you want to know what to do next, a natural extension of the concepts in this workshop is a relay account. Why don’t you check it out?