On this page:
4.2.1 Problem Analysis
4.2.2 Data Definition
4.2.3 Communication Construction
4.2.4 Assertion Insertion
4.2.5 Interaction Introduction
4.2.6 Deployment Decisions
4.2.7 Discussion and Next Steps

4.2 Workshop: Relay Account

In this workshop, we’ll revisit the problem of allowing a payer to transfer funds to another party before knowing their identity. However, unlike in Workshop: Hash Lock, we will use a technique that is safe against malicious miners. One deployment of a decentralized application like this is as a "gift card" where a funder provides a fixed amount of currency to another without knowing their identity.

This workshop assumes that you have recently completed Workshop: Hash Lock.

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

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.2.1 Problem Analysis

For this workshop, we’ll provide some constraints on your solution and problem analysis, since we’d like you to explore writing a Reach program with a specific design.

The overall purpose of this application is so that:
  • Alice can decide an amount of funds to provide.

  • Alice later decides who will have access to this by sharing a secret with them. We call this person, Bob.

  • Bob can transfer the funds to wherever he’d like.

In Workshop: Hash Lock, we designed the application so that the "secret" was a special number that the contract compared against a known digest to release the funds. This approach was flawed, because when Bob used the secret to gain access, it was possible for anyone else to see the transaction and attempt to play it themselves.

In today’s workshop, we’ll use a crucial insight about decentralized applications: account ownership is fluid and account credentials are a form of secret knowledge that every consensus network builds in to their foundation. With that in mind, let’s use the following design:

Alice will represent her secret as an account that is authorized to withdraw the funds.

This is called a relay account, because it exists temporarily to faciliate the relaying of funds from Alice to Bob.

With this in mind, let’s answer the questions:
  • Who are the principals of the application?

  • What are the participants of the program?

  • 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?

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

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

The most surprising thing about this application is that Bob is not one of the participants in the application! Of course, the Relay will actually run under the auspices of Bob, after Alice shares the account credentials with him, but there is a distinction in the program between Bob’s identity and the Relay’s.

4.2.2 Data Definition

The next step of designing our program is representing this information in our program and deciding the participant interact interface for each participant. Which pieces of information go with which participants? Which are functions and which are values? Finally, how should the Relay account information and Bob’s identity be represented? (Hint: Reach has a type named Address that represents an account address!)

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

Write down the data definitions for this program as definitions.

Let’s compare notes again. Here’s what we wrote in our program:

..    // ...
 6    [Participant('Alice', { amt : UInt,
 7                 getRelay: Fun([], Address) }),
 8     Participant('Relay', { getBob: Fun([], Address) }) ],
..    // ...

We chose to represent the amount as a UInt field, which should be unsurprising. We then have two functions that take no arguments and return an Address which respectively return the Relay identity and the Bob identity. The idea here is that Alice will create the Relay account in the midst of the program and Bob will provide his own identity when he’s acting as Relay.

4.2.3 Communication Construction

Now, we can write down the structure of communication and action in our application. Try this on your own based on your experience with Workshop: Hash Lock.

Write down the communication pattern for this program as comments.

Here’s what we wrote:

// 1. Alice pays the amount and says who the Relay is.
// 2. The consensus remembers who the Relay is.
// 3. The Relay publishes who Bob is.
// 4. The consensus pays Bob.

We assume that most of you found it natural to think of steps one, three, and four, but found step two to be a strange addition. Perhaps you felt that step two is implied by step one, where Alice says who the Relay is. But, it all depends upon what the meaning of the word "is" is. Since that is unclear to some, we’ll make it explicit by stating that the consensus will remember the Relay’s identity.

The next step is to convert this pattern into actual program code using publish, pay, and commit. However, we expect that you’ll need a bit of help with step two. Reach has a special operation, available only in consensus steps, for asserting the identity of a participant: Participant.set. You can write Relay.set(someAddr) to assert that the address of the Relay is someAddr. With that in mind...

Write down the communication pattern for this program as code.

The body of your application should look something like:

// 1. Alice pays the amount and says who the Relay is.
Alice.publish(amt, relay)

// 2. The consensus remembers who the Relay is.

// 3. The Relay publishes who Bob is.

// 4. The consensus pays Bob.

We expect that for most of you, the coding of step four is also a bit strange, because we’ve never seen an example where the destination of a transfer is not a participant. You may have thought that the to position in a transfer must be a participant, but actually it can be any address. Participants, however, can be used as addresses if they are bound.

You might like to re-write this program to have a third participant, Bob, who takes no actions, and try to write transfer(amt).to(Bob). You’ll find that Reach rejects this program because Bob is not bound. You can correct this by adding Bob.set(bob) after the Relay publishes Bob’s address. There’s nothing better about this version of the program, but it is unneccessary to have a participant like Bob that performs no part in the computation.

4.2.4 Assertion Insertion

As usual, we should consider what assertions we can add to our program. In some ways this is what we just did with the Relay.set(relay) line above, but that is unlike a normal assertion in that it is added primarily to direct the runtime activities on the consensus contract, rather than as a statement about the logical properties of our program variables.

Sometimes it can be difficult to decide which things are part of the application, like this, and which things are properties of the application, like the assertions we’ve seen before. This is a general problem in verification where the logical properties of the desired program are often mixed up with the logical properties of the actual program. If you’re interested in this topic, you might like to spend time reading about formal specification on Wikipedia.

Now, if we were devious, we might send you on a SNARK hunt after some more assertions to add to our program, but we’re not mean, so we’ll just tell you that there’s nothing else to assert about this program.

4.2.5 Interaction Introduction

Next, we need to insert the appropriate calls to interact. In this case, our program is very simple and we expect you’ll do a great job without further discussion.

Insert interact calls to the frontend into the program.

Let’s look at our whole program now:

 1    'reach 0.1';
 2    'use strict';
 4    export const main = Reach.App(
 5      {},
 6      [Participant('Alice', { amt : UInt,
 7                   getRelay: Fun([], Address) }),
 8       Participant('Relay', { getBob: Fun([], Address) }) ],
 9      (Alice, Relay) => {
10        Alice.only(() => {
11          const [ amt, relay ] =
12                declassify([ interact.amt,
13                             interact.getRelay()]); });
14        Alice.publish(amt, relay)
15          .pay(amt);
16        Relay.set(relay);
17        commit();
19        Relay.only(() => {
20          const bob = declassify(interact.getBob()); });
21        Relay.publish(bob);
22        transfer(amt).to(bob);
23        commit();
25        exit(); } );

4.2.6 Deployment Decisions

This program is a bit odd to test, because it relies on Alice creating a temporary account and then sharing its information with Bob. We don’t know of any beautiful way to derive this program from first principles, and instead must appeal to your JavaScript programming skills. If you’d like a hint, remember that you can call stdlib.newTestAccount any number of times and that a backend’s participant functions don’t need to be called at the same time.

If you’re brave, then try it yourself; otherwise, scroll down to see our solution.

Decide how you will deploy and use this application.

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);
18      let accRelayProvide = null;
19      const accRelayP = new Promise((resolve, reject) => {
20        accRelayProvide = resolve;
21      });
23      await Promise.all([
24        backend.Alice(ctcAlice, {
25          amt: stdlib.parseCurrency(25),
26          getRelay: async () => {
27            console.log(`Alice creates a Relay account.`);
28            const accRelay = await stdlib.newTestAccount(stdlib.minimumBalance);
29            console.log(`Alice shares it with Bob outside of the network.`);
30            accRelayProvide(accRelay);
31            return accRelay.networkAccount;
32          },
33        }),
34        (async () => {
35          console.log(`Bob waits for Alice to give him the information about the Relay account.`);
36          const accRelay = await accRelayP;
37          console.log(`Bob deposits some funds into the Relay to use it.`);
38          await stdlib.transfer(accBob, accRelay, stdlib.parseCurrency(10));
39          console.log(`Bob attaches to the contract as the Relay.`);
40          const ctcRelay = accRelay.attach(backend, ctcAlice.getInfo());
41          console.log(`Bob joins the application as the Relay.`);
42          return backend.Relay(ctcRelay, {
43            getBob: async () => {
44              console.log(`Bob, acting as the Relay, gives his information.`);
45              return accBob.networkAccount;
46            },
47          });
48        })(),
49      ]);
51      const afterAlice = await getBalance(accAlice);
52      const afterBob = await getBalance(accBob);
54      console.log(`Alice went from ${beforeAlice} to ${afterAlice}.`);
55      console.log(`Bob went from ${beforeBob} to ${afterBob}.`);
57    })();

We do a few sneaky things in this program:
  • Lines 18 through 21 create a JavaScript Promise that will be filled in later by Alice.

  • Alice’s getRelay function (lines 26 through 32) creates the new account and communicates it "outside of the network" through the aforementioned Promise.

  • Bob’s thread (lines 34 through 48) waits for the Promise to resolve and then connects to the application with this new account.

  • The Relay’s getBob function (lines 43 through 46) returns his own address to receive the funds.

If this program is scary for you, don’t worry! It uses some fairly esoteric JavaScript features to make a completely automated test of this program. If instead you wrote it so that it ran interactively and had Bob paste in the information about the new Relay account, it might be easier for you to code with those two aspects totally separated.

Let’s see what it looks like when we run this program:

$ ../reach run

Alice creates a Relay account.

Bob waits for Alice to give him the information about the Relay account.

Alice shares it with Bob off chain.

Bob deposits some funds into the Relay to use it

Bob attaches to the contract as the Relay.

Bob joins the application as the Relay.

Bob, acting as the Relay, gives his information.

Alice went from 100.0 to 74.999999999999804065.

Bob went from 100.0 to 123.999999999999979.

4.2.7 Discussion and Next Steps

Great job! You could use this application today and start minting gift cards of tokens for your friends on their birthdays! Wouldn’t that be fun?

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

If you’d like to make this application a little more interesting, maybe you’d like to have a secret password just like the hash lock as well, so Alice can separate the revealing of information to Bob.

If you want to know what to do next to advance your study of decentralized application design, a natural extension of the concepts in this workshop is a trust fund. Why don’t you check it out?