|
| 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, we’ll 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 user’s 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 user’s private key, we’ll 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 | +We’ll have to check if this public key matches one of the ones stored on-chain. So let’s 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 account’s 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 doesn’t 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 | +There’s 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 | +That’s it! Save the file, and let’s 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 won’t 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: You’ll 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 | + |
0 commit comments