Skip to content

Commit 1df75ab

Browse files
Added Simple Anonymous Message Board Tutorial page to zkApps section
1 parent 99a9bd4 commit 1df75ab

File tree

2 files changed

+345
-0
lines changed

2 files changed

+345
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
import Page from "@reason/pages/Docs";
2+
import DocLink from "@reason/components/DocLink";
3+
export default Page({ title: "Simple Anonymous Message Board Tutorial" });
4+
5+
<Alert kind="info">
6+
7+
Please note that zkApp programmability is not yet available on Mina, but zkApps
8+
can now be deployed to Berkeley QANet. These docs are a preview of work that is
9+
currently in progress.
10+
11+
</Alert>
12+
13+
# Simple Anonymous Message Board Tutorial
14+
<DocLink copy="How to write a zkApp" url="/zkapps/how-to-write-a-zkapp" /> provides a high-level overview of everything you need to know to build applications on Mina. This tutorial will put these ideas into practice as we walk through the design and implementation of a semi-anonymous messaging protocol.
15+
16+
## Overview
17+
18+
We’ll build a smart contract that allows users to publish messages semi-anonymously. Our contract will allow a specific set of users to create new messages but will not disclose which user within the set has done so. This way, people can leverage one aspect of their identity without revealing exactly who they are. So, for example, a DAO member could make credible statements on behalf of their DAO without revealing their specific individual identity.
19+
20+
## Setup
21+
22+
First, install the [Mina zkApp CLI](https://github.com/o1-labs/zkapp-cli) if you haven’t already done so
23+
24+
#### Dependencies
25+
26+
You'll need the following installed to use the zkApp CLI:
27+
28+
- NodeJS 16+ (or 14 using `--experimental-wasm-threads`)
29+
- NPM 6+
30+
- Git 2+
31+
32+
If you have an older version installed, we suggest installing a newer version
33+
using the package manager for your system: [Homebrew](https://brew.sh/) (Mac),
34+
[Chocolatey](https://chocolatey.org/) (Windows), or apt/yum/etc (Linux). On
35+
Linux, you may need to install a recent NodeJS version via NodeSource
36+
([deb](https://github.com/nodesource/distributions#debinstall) or
37+
[rpm](https://github.com/nodesource/distributions#rpminstall)), as recommended
38+
by the NodeJS Project.
39+
40+
#### Installation
41+
42+
```sh
43+
npm install -g zkapp-cli
44+
```
45+
46+
#### Usage
47+
48+
```sh
49+
zk --help
50+
```
51+
52+
##### Create a new project
53+
54+
```sh
55+
zk project my-proj # or path/to/my-proj
56+
57+
✔ Fetch project template
58+
✔ Initialize Git repo
59+
✔ NPM install
60+
✔ Set project name
61+
✔ Git init commit
62+
63+
Success!
64+
65+
Next steps:
66+
cd my-proj
67+
git remote add origin <your-repo-url>
68+
git push -u origin main
69+
```
70+
71+
This command creates a directory containing a new project template, fully set up
72+
& ready for local development.
73+
74+
- See the included [README](templates/project-ts/README.md) for usage instructions.
75+
All usual commands will be available: `npm run build`, `npm run test`,
76+
`npm run coverage`, etc.
77+
- A Git repo will be initialized in the project directory automatically. For
78+
consistency, we use `main` as the default Git branch, by convention.
79+
- A [Github Actions CI workflow](templates/project-ts/.github/workflows/ci.yml) is
80+
also included. If you push your project to Github, Github Actions will run
81+
your tests (named as `*.test.js`) automatically, whenever you push a commit or
82+
open a pull request.
83+
- Code style consistency (via Prettier) and linting (via ES Lint) are
84+
automatically enforced using Git pre-commit hooks. This requires no
85+
configuration and occurs automatically when you commit to Git--e.g. `git commit -m 'feat: add awesome feature'`.
86+
- To skip all checks in the Git pre-commit hook (not recommended), you can pass
87+
the `-n` flag to Git--e.g. `git commit -m 'a bad commit' -n`. But we'd
88+
recommend avoiding this and resolving any errors which exist in your project
89+
until the pre-commit hook passes.
90+
91+
### Scaffolding
92+
93+
Now that your project is set up, you can open it in your IDE or `cd zk-message` if you work from the command line.
94+
95+
There should be an example smart contract in `./src` called `Add.ts` and test for it in `Add.test.ts`. Let’s create a new contract by running:
96+
97+
```sh
98+
zk file Message
99+
```
100+
101+
Now open it up and paste in the following:
102+
103+
```ts
104+
import {
105+
Field,
106+
SmartContract,
107+
state,
108+
State,
109+
method,
110+
DeployArgs,
111+
Permissions,
112+
PrivateKey,
113+
PublicKey,
114+
isReady,
115+
Poseidon,
116+
Encoding,
117+
} from 'snarkyjs';
118+
119+
export { isReady, Field, Encoding };
120+
121+
// Wait till our SnarkyJS instance is ready
122+
await isReady;
123+
124+
// These private keys are exported so that experimenting with the contract is
125+
// easy. Three of them (the Bobs) are used when the contract is deployed to
126+
// generate the public keys that are allowed to post new messages. Jack's key
127+
// is never added to the contract. So he won't be able to add new messages. In
128+
// real life, we would only use the Bobs' public keys to configure the contract,
129+
// and only they would know their private keys.
130+
131+
export const users = {
132+
Bob: PrivateKey.fromBase58(
133+
'EKFAdBGSSXrBbaCVqy4YjwWHoGEnsqYRQTqz227Eb5bzMx2bWu3F'
134+
),
135+
SuperBob: PrivateKey.fromBase58(
136+
'EKEitxmNYYMCyumtKr8xi1yPpY3Bq6RZTEQsozu2gGf44cNxowmg'
137+
),
138+
MegaBob: PrivateKey.fromBase58(
139+
'EKE9qUDcfqf6Gx9z6CNuuDYPe4XQQPzFBCfduck2X4PeFQJkhXtt'
140+
), // This one says duck in it :)
141+
Jack: PrivateKey.fromBase58(
142+
'EKFS9v8wxyrrEGfec4HXycCC2nH7xf79PtQorLXXsut9WUrav4Nw'
143+
),
144+
};
145+
146+
export class Add extends SmartContract {
147+
// On-chain variable definitions
148+
149+
@method init() {
150+
// Define initial values of on-chain variables
151+
}
152+
153+
@method publishMessage(message: Field, signerPrivateKey: PrivateKey) {
154+
// Compute signerPublicKey from signerPrivateKey argument
155+
// Get approved public keys
156+
// Assert that signerPublicKey is one of the approved public keys
157+
// Update on-chain message variable
158+
// Computer new messageHistoryHash
159+
// Update on-chain messageHistoryHash
160+
}
161+
}
162+
```
163+
164+
This will serve as the scaffolding for the rest of the tutorial and contains a smart contract called `Message` with two methods: `init()` and `publishMessage()`. The `init()` method is similar to the `constructor` in Solidity. It’s a place for you to define any setup that needs to happen before users begin interacting with the contract. `publishMessage()` is the method that users will invoke when they want to create a new message. The `@method` decorator tells SnarkyJS that users should be allowed to call this method and that it should generate a zero-knowledge proof of its execution.
165+
166+
## Wrting the Smart Contract
167+
168+
### Defining On-Chain Storage
169+
170+
Every Mina smart contract includes eight on-chain state variables that each store almost 256 bits of information. In more complex smart contracts, these can store commitments (i.e., the hash of a file, the root of a Merkle tree, etc.) to off-chain storage, but in this case, we’ll store everything on-chain for the sake of simplicity.
171+
172+
<Alert kind="note">
173+
General purpose off-chain storage libraries are planned, but you can always roll your own solution if desired.
174+
</Alert>
175+
176+
In this smart contract, one state variable will store the last message. Another will store the hash of all the previous messages (so a frontend can validate message history), and three more will store user public keys (we could store even more by Merkelizing them, but we’ll keep it to three here for the sake of brevity).
177+
178+
```ts
179+
export class Add extends SmartContract {
180+
// On-chain variable definitions
181+
@state(Field) message = State<Field>();
182+
@state(Field) messageHistoryHash = State<Field>();
183+
@state(PublicKey) user1 = State<PublicKey>();
184+
@state(PublicKey) user2 = State<PublicKey>();
185+
@state(PublicKey) user3 = State<PublicKey>();
186+
```
187+
188+
The `@state(Field)` decorator tells SnarkyJS that the variable should be stored on-chain as a `Field` type.
189+
190+
For practical purposes, the `Field` type is similar to the `uint256` type in Solidity. It can store large integers, and addition, subtraction, and multiplication all work as expected. The only caveats are division and what happens in the event of an overflow. You can learn a little more about them [here](https://en.wikipedia.org/wiki/Finite_field_arithmetic), but it’s not necessary to understand exactly how field arithmetic works for this tutorial. SnarkyJS also provides `UInt32`, `UInt64`, and `Int64` types, but under the hood, all SnarkyJS types are composed of the Field type (including `PublicKey`, as you see above).
191+
192+
### Defining the `init()` method
193+
194+
The `init` method is similar to the `constructor` in Solidity. It’s a place for you to define any setup that needs to happen before users begin interacting with the contract. In this case, we’ll set the public keys of users who can post, and initialize `message` and `messageHistoryHash` as zero (our front end will interpret the zero value to mean that no messages have been posted yet).
195+
196+
```ts
197+
@method init() {
198+
// Define initial values of on-chain variables
199+
this.user1.set(users['Bob'].toPublicKey());
200+
this.user2.set(users['SuperBob'].toPublicKey());
201+
this.user3.set(users['MegaBob'].toPublicKey());
202+
this.message.set(Field.zero);
203+
this.messageHistoryHash.set(Field.zero);
204+
}
205+
```
206+
207+
### Defining `publishMessage()`
208+
209+
The `publishMessage` method will allow an approved user to publish a message. Note the `@method` decorator mentioned earlier. It makes this method callable by users so that they can interact with the smart contract.
210+
211+
For our example, well pass in `message` and `signerPrivateKey` arguments to check that the user holds a private key associated with one of the three on-chain public keys before allowing them to update the message.
212+
213+
```ts
214+
@method publishMessage(message: Field, signerPrivateKey: PrivateKey) {
215+
```
216+
217+
Note that all inputs are private by default and will only exist on the users local machine when the smart contract runs; the Mina network will never see them. Our smart contract will only send values that are stored as state to the Mina blockchain. This means that even though the value of the `message` argument will eventually be public, the value of `signerPrivateKey` will never leave the user's machine (as a result of interacting with the smart contract).
218+
219+
### Computing `signerPublicKey` from `signerPrivateKey`
220+
221+
Now that we have the users private key, well need to derive the associated public key to check it against the list of approved publishers. Luckily the `PrivateKey` type in SnarkyJS includes a `toPublicKey()` method.
222+
223+
```ts
224+
// Compute signerPublicKey from signerPrivateKey argument
225+
const signerPublicKey = signerPrivateKey.toPublicKey();
226+
```
227+
228+
Well have to check if this public key matches one of the ones stored on-chain. So lets grab those as well.
229+
230+
231+
```ts
232+
// Get approved public keys
233+
const user1 = this.user1.get();
234+
const user2 = this.user2.get();
235+
const user3 = this.user3.get();
236+
```
237+
238+
Calling the `get()` method tells SnarkyJS to retrieve these values from the zkApp accounts on-chain state. (Note that SnarkyJS uses a single network request to retrieve all on-chain state values simultaneously.)
239+
240+
Finally, we check if `signerPublicKey` is equal to one of the allowed public keys contained in our `user` variables.
241+
242+
```ts
243+
// Assert that signerPublicKey is one of the approved public keys
244+
signerPublicKey
245+
.equals(user1)
246+
.or(signerPublicKey.equals(user2))
247+
.or(signerPublicKey.equals(user3))
248+
.assertEquals(true);
249+
```
250+
251+
Notice that we call the SnarkyJS `equals()` and `or()` methods instead of using the JavaScript operators (`===`, and `||`). The built-in SnarkyJS methods have the same effect, but they work with SnarkyJS types, and their execution can be verified using a zero-knowledge proof.
252+
253+
`assertEquals(true)` at the end means that it will be impossible to generate a valid proof unless `signerPublicKey` is equal to one of the pre-approved users. The Mina network will reject any transaction sent to a zkApp account that doesnt include a valid zero-knowledge proof for that account. So it will be impossible for users to post new messages unless they have a private key associated with one of the three pre-approved public keys.
254+
255+
### Updating `message`
256+
257+
Until now, we have worked to ensure that only approved users can call `publishMessage()`. When they do, the contract should update the on-chain message variable to their new message.
258+
259+
```ts
260+
// Update on-chain message variable
261+
this.message.set(message);
262+
```
263+
264+
The `set()` method will ask the Mina nodes to update the values of their on-chain message variables, but only if the associated proof is valid.
265+
266+
### Updating `messageHistoryHash`
267+
268+
Theres one more thing we should do. If we want users to be able to keep track of what has been said, then we need to store a commitment to the message history on-chain. There are a few ways to do this, but the simplest is to store a hash of our new `message` and our old `messageHistoryHash` every time we call `publishMessage`.
269+
270+
```ts
271+
// Computer new messageHistoryHash
272+
const oldHash = this.messageHistoryHash.get();
273+
const newHash = Poseidon.hash([oldHash, message]);
274+
275+
// Update on-chain messageHistoryHash
276+
this.messageHistoryHash.set(newHash);
277+
```
278+
279+
Thats it! Save the file, and lets make sure everything compiles.
280+
281+
282+
```sh
283+
npm run build
284+
```
285+
286+
If everything is correct, you should see a new `./build` directory. This is where the compiled version of your project lives that you can import into a user interface.
287+
288+
## Integrating with a User Interface
289+
290+
One of the benefits of writing smart contracts in TypeScript is that they are usually straightforward to <DocLink copy="integrate with UI components" url="/zkapps/how-to-write-a-zkapp" />.
291+
292+
We wont make you write a UI for this tutorial, but we have created a simple command-line interface so you can interact with your smart contract as a user, get a sense of how it works, and experiment with what you have just built. You can clone it from [this](https://github.com/) repository.
293+
294+
```sh
295+
git clone https://placeholder.com
296+
cd ./placeholder
297+
```
298+
299+
Now install your smart contract:
300+
301+
```sh
302+
npm install ../zk-message
303+
```
304+
305+
And start the front end:
306+
307+
```sh
308+
npm run start
309+
310+
Who are you? (Bob, SuperBob, MegaBob, Jack): Bob
311+
312+
What would you like to say? (should only work if you are one of the Bobs): Snarky is a nice sharky.
313+
314+
Message history:
315+
Snarky is a nice sharky.
316+
317+
Who are you? (Bob, SuperBob, MegaBob, Jack): Jack
318+
319+
What would you like to say? (should only work if you are one of the Bobs): Hello World!
320+
321+
** GNARLY ERROR **
322+
```
323+
324+
## Wrapping up
325+
326+
Hopefully you enjoyed this tutorial, and it gave you a sense of what's possible with SnarkyJS. The messaging protocol we built is quite simple but also very powerful. This basic pattern could be used to create a whistleblower system, an anonymous NFT project, or even anonymous DAO voting. SnarkyJS makes it easy for developers to build things that don’t intuitively seem possible, and this is really the point. Zero-knowledge proofs open the door to an entirely different way of thinking about the internet, and we are so excited to see what people like you will build. Make sure to join the [#zkapps-developers](https://placeholder.com) channel on our Discord, and apply for the [zkApp Builders Program](https://placeholder.com) if you are interested in building something more complex with the help of our team at O(1) Labs. More tutorials are on the way ([suggest ideas](https://placeholder.com)), but these are some logical next steps if you are interested in extending this project.
327+
328+
1. Allow users to pass signers into the publishMessage method directly so that many different organizations can use a single contract. (Hint: Youll have to store a commitment to the signers on-chain.)
329+
2. Allow users to pass an arbitrarily large number of signers into the publishMessage method.
330+
3. Store the message history in a Merkle tree so a user interface can check a subset of the messages quickly without evaluating the entire history.
331+
4. Build a shiny front end!
332+
333+

src/components/DocsNavs.re

+12
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ module DocsNavsTranslations = {
125125
let zkAppsForEthereumDevelopers= {
126126
id: "sidenav.zkapps-for-ethereum-developers",
127127
defaultMessage: "zkApps for Ethereum Developers",
128+
};
129+
let simpleAnonymousMessageBoardTutorial= {
130+
id: "sidenav.simple-anonymous-message-board-tutorial",
131+
defaultMessage: "Simple Anonymous Message Board Tutorial",
128132
};
129133
let howZkAppsWork = {
130134
id: "sidenav.how-zkapps-work",
@@ -385,6 +389,10 @@ module SideNav = {
385389
<Item
386390
title={intl->Intl.formatMessage(zkAppsForEthereumDevelopers)}
387391
slug="zkapps-for-ethereum-developers"
392+
/>
393+
<Item
394+
title={intl->Intl.formatMessage(simpleAnonymousMessageBoardTutorial)}
395+
slug="simple-anonymous-message-board-tutorial"
388396
/>
389397
<Item
390398
title={intl->Intl.formatMessage(zkAppsAPIReference)}
@@ -635,6 +643,10 @@ module Dropdown = {
635643
title={intl->Intl.formatMessage(zkAppsForEthereumDevelopers)}
636644
slug="zkapps-for-ethereum-developers"
637645
/>
646+
<Item
647+
title={intl->Intl.formatMessage(simpleAnonymousMessageBoardTutorial)}
648+
slug="simple-anonymous-message-board-tutorial"
649+
/>
638650
<Item
639651
title={intl->Intl.formatMessage(zkAppsAPIReference)}
640652
slug="snarkyjs-reference"

0 commit comments

Comments
 (0)