<!-- all slides will have a font of 30px --> <style> .reveal { font-size: 30px; } </style> # Snapps --- <!-- .slide: style="font-size: 30px;" --> ## Snapps Ethereum is a computer that can store user programs. Users execute a program by sending a transaction **specifying one of its methods**. <img src="https://i.imgur.com/dWBFAXU.jpg" height="200"/> Mina is a computer that can store user programs. Users execute a program by sending a transaction **containing the execution result of a method (accompanied with a proof of correct execution)**. --- ![](https://i.imgur.com/afE84R7.png) --- ## Snapps As such, a snapp is eventually compiled down into two programs: one that you can use locally to execute one of its method (and create a proof), and one that others can use to verify that the execution was correct. <img src="https://i.imgur.com/czNhsNQ.png" height="300px"> *Note*: one account = one snapp --- ## Snapps Each method of a snapp is actually compiled to a program which: * takes as argument the method's arguments, as well as a list of preconditions * returns a list of effects/updates to the state <img src="https://i.imgur.com/aNGdpQS.png" height="300px"> --- ## Snapps under the hood: - arguments of the methods are the private inputs of a circuit - any value read from the state is translated as a precondition, encoded as a public input - effects/updates are encoded as public inputs to the circuit as well. The proof is thus of the following statement: > based on a list of preconditions on the world state, and the snapp's state. Running the snapp's method with some secret inputs leads to the following effects/updates on the world state and the snapp's state. --- <img src="https://i.imgur.com/TGI2Leb.png" height="300"> Note: of course effects on the world state can only be applied in constrained ways: an external account can only be credited, not debited. Only the current snapp's state can be updated (not other snapps' states). --- ## Setup (NOT READY FOR WORKSHOP) The Snapp CLI allows you to easily set up, test, and deploy your smart contract. ```console $ npm install -g snapp-cli $ snapp project my-proj ``` --- ## Setup (NOT READY FOR WORKSHOP) ![](https://i.imgur.com/priPLzG.png) --- ## Setup ```console $ git clone git@github.com:o1-labs/snarkyjs.git $ npm install && npm run build $ ./run src/examples/api_exploration.ts ``` or better: ```console $ mkdir my-project && cd my-project $ npm init -y $ npm install @o1labs/snarkyjs typescript ``` or even better: ```console $ git clone git@github.com:o1-labs/snarkyjs-workshop.git $ cd snarkyjs-workshop ``` --- ## Hello World To write a snapp, extend the `SmartContract` class of snarkyjs. ```typescript import { SmartContract, state, method, init, State, Field } from 'snarkyjs'; class MySmartContract extends SmartContract { } ``` --- ## Hello World A snapp can contain some state (mutable storage). ```typescript import { SmartContract, state, method, init, State, Field } from 'snarkyjs'; class MySmartContract extends SmartContract { @state some_var: State<Field>; @state some_other_var: State<Field>; } ``` Here, `some_var` is of type `Field`. It is fundamentally the only type supported, and all other types are created from this type. It's almost exactly the same type as Ethereum's `u256` type, except that it's a bit smaller. --- ## Hello World TODO: explanation about init/constructor... ```typescript import { SmartContract, state, method, init, State, Field } from 'snarkyjs'; class MySmartContract extends SmartContract { @state some_var: State<Field>; @init constructor(some_var: Field) { let perms = Permissions.default(); perms.receive = Perm.proof(); perms.editState = Perm.proof(); this.self.update.permissions.set(perms) this.some_var = some_var; } } ``` --- ## Hello World a snapp can contain multiple methods, and each method can mutate the storage in different ways ```typescript import { SmartContract, state, method, init, State, Field } from 'snarkyjs'; class MySmartContract extends SmartContract { // ... @method update(some_arg: Field) { // ... } @method add(some_arg: Field, more_arg: Field) { // ... } } ``` note: * one snapp = multiple circuits/verifying keys (one for each method) --- ## Hello World The state of your snapp is considered "public", and any method of you snapps' methods can read and update them. Any argument of a method is considered "private", as no one else but you will be able to learn them. (Unless, for example, a method stores an argument directly in the state.) ```typescript import { SmartContract, state, method, init, State, Field } from 'snarkyjs'; class MySmartContract extends SmartContract { @state some_var: State<Field>; // ... @method update(squared: Field) { this.some_var.get().square().assertEqual(squared); // some_var^2 = squared this.some_var.set(squared); // some_var ← squared } } ``` under the hood: * `get()` -> reads the blockchain and encodes a precondition in the public input of the circuit (e.g. `some_var` must be equal to 4 for the execution to be correct). * `square()` -> creates a new temporary variable in the circuit that is constrained to be the square of `some_var`. * `assertEqual()` -> asserts that the temporary variable is equal to the private input `squared`. * `set()` -> encodes an effect on the snapp's state (e.g. it is correct that executing this method sets `some_var` to 8). --- ## Exercise 1 Create a similar smart contract, that requires that you pass the cubic root of `some_var` as private input. Another private input should set the new value for `some_var`. --- ## Exercise 1 ```typescript import { SmartContract, state, method, init, State, Field } from 'snarkyjs'; class MySmartContract extends SmartContract { @state some_var: State<Field>; // TODO: init @method update(sqrt: Field, new_val: Field) { let some_var = this.some_var.get(); let cubed = new_val.square().mul(new_val); cubed.assertEqual(some_var); this.some_var.set(new_val); } } ``` --- ## State of the world What else is public and accessible from a method? Blockchain data, given to you through `this.self` ```typescript class MySmartContract extends SmartContract { // ... @method update(squared: Field) { this.self.protocolState.blockchainLength.assertGt(500); } // ... } ``` problem: state of the blockchain you see when you execute a method (and produce a transaction that includes a proof) is different from the state of the blockchain a block producer sees when they process your transaction. solution: ?? --- ## State of the world You can also modify blockchain data as part of the result of an execution, for example to send minas to another account as part of a method's execution ```typescript this.self.update.X = Y; ``` under the hood: this constrains that specific public output to be equal to Y --- TODO: continue with updating accounts with payments + do exercise to extend the previous one by returning a reward --- ## Types <!-- types --> Consult the documentation to see operations on `Field`, or let auto-complete do that for you. <img src="https://i.imgur.com/QJcfbGg.png" height="300"> --- ## Types snarkyjs provides a number of useful types based on `Field`: * `UInt64` * `UInt32` * `PublicKey` * `Optional` * `Bool` * `array` * `Group` * `EndoScalar` * `Scalar` * ... --- ## Types You can also create custom types by extending snarkyjs' `CircuitValue` and using the `@prop` decorator: ```typescript class RollupTransaction extends CircuitValue { @prop amount: UInt64; @prop nonce: UInt32; @prop sender: PublicKey; @prop receiver: PublicKey; constructor(amount: UInt64, nonce: UInt32, sender: PublicKey, receiver: PublicKey) { super(); this.amount = amount; this.nonce = nonce; this.sender = sender; this.receiver = receiver; } } ``` --- ## Functionalities * just write your code, functions implemented on the snarky types will automatically convert themselves into constraints * use snarky's assert functions to prevent execution of incorrect logic. --- ## Functionalities * `Poseidon.hash(input: Field[]): Field` → a hash function. <!-- .element: class="fragment" data-fragment-index="1" --> * `Circuit.if(b: Bool, x: T, y: T): T` → there's no `if` for flow control, but you can use it as a ternary operator. <!-- .element: class="fragment" data-fragment-index="2" --> * `Signature.verify(publicKey: PublicKey, msg: Field[]): Bool` → verify signature. * ? <!-- .element: class="fragment" data-fragment-index="3" --> --- ## Exercise 2 1. Create an `Account` custom type that contains a counter (represented by a field element) (TODO: can we make it a u64?) and a [public key](https://o1-labs.github.io/snarkyjs/classes/PublicKey.html). 2. Create a smart contract with a single account as state, and a method `increment_counter` that allows you to increment the counter if you can provide a valid signature (using the account's public key) on the current counter. --- ## Exercise 2 ```typescript= import { SmartContract, state, method, init, State, Field } from 'snarkyjs'; class Account extends CircuitValue { @prop: pubkey: State<PublicKey>; @prop: counter: State<Field>; } class Exercise2 extends SmartContract { @state account: State<Account>; // TODO: init @method increment_counter(signature: Signature) { const counter = this.counter.get(); signature.verify(pubkey, [counter]).assertEquals(true); this.counter.set(counter + 1); } } function main() { // keygen const privkey = PrivateKey.random(); const pubkey = privkey.toPublicKey(); // create signature const digest = Poseidon.hash(...); const signature = Signature.create(privkey, [digest]); } ``` note: * we cannot use arrays as arguments of methods, we need fixed-size arguments, so tuples will do. --- ## Dealing with large state Note that the state of a snapp can only contain 8 field values. ```typescript @state some_var: State<Field>; // 1 field value @state some_other_var: State<YourOwnType>; // potentially more ``` To manage larger values, a snapp must use accumulator structures like Merkle trees. Snarkyjs standard library provides a number of functions to help you with that. --- ## Dealing with large state * KeyedAccumulator * SetAccumulator * Stack/ListAccumulator TODO: do we really need to name them "merkle trees" or "accumulators". Is there a more dev-friendly name? --- ## Dealing with large state pro-tip: you do not need to understand how merkle trees work to use them. Simply use them as blackboxes that provide a storage API. ```typescript interface AccumulatorI<A, P> { // adds an element into your set add: (x: A) => P, // creates a proof that an element is part of your set getMembershipProof: (x: A) => P | null, // this is an authenticator for your set, store it in your snapp commitment: () => Field, // this can check a proof that an element is part of a given set check: (x: A, membershipProof: P) => Bool, } ``` --- ## Dealing with large state For example: ```typescript this.my_set.assertEquals(some_set.commitment()); some_set.add(some_value); // can I do this.my_set.add(some_value) ? this.my_set.set(some_set.commitment()); ``` only a field element (a commitment, why?) is stored in the state of a snapp. --- ## Dealing with large state The data associated to the commitment of a merkle tree needs to live off-chain. There's a data availability problem. You can store this on IPFS, locally, etc. but users of a snapp must have a way to retrieve that data in order to correctly use a snapp. --- ## Exercise 3 create a smart contract that makes use of a Merkle tree --- ## Exercise 3 ```typescript import { SmartContract, state, method, init, State, Field } from 'snarkyjs'; class Exercise3 extends SmartContract { @state merkle_root: State<Field>; // TODO: init @method update_balance(merkle_tree: KeyedAccumulator<PublicKey, Field>, public_key: PublicKey) { let balance = this.merkle_root.get(public_key).assertEquals(merklee_tree.commitment()); asserts(balance.is_some); balance.value += val; this.merkle_root.set(merkle_tree.commitment()) } } function main() { } ``` --- ## Making a function payable ```typescript @method depositFunds( depositor: Body, depositAmount: UInt64) { const self = this.self; let delta = SignedAmount.ofUnsigned(depositAmount); self.delta = delta; depositor.delta = delta.neg(); let deposit = new RollupDeposit(depositor.publicKey, depositAmount); this.emitEvent(deposit); const rollupState = this.rollupState.get(); rollupState.pendingDepositsCommitment = MerkleStack.pushCommitment( deposit, rollupState.pendingDepositsCommitment ); this.rollupState.set(rollupState); } ``` ```typescript let depositorBalance = Mina.getBalance(depositorPubkey); let depositor = Party.createSigned(depositorPrivkey); return Mina.transaction(() => { // Deposit some funds into the rollup RollupInstance.depositFunds(depositor, depositorBalance.div(2)); }).send().wait() ``` --- ## Sending money TKTK --- ## Exercise 5 Do we have time for that? --- ## Events TKTK --- ## Recursion ```typescript @proofSystem class RollupProof extends ProofWithInput<RollupStateTransition> { @branch static processDeposit( // ... } ```
{"metaMigratedAt":"2023-06-16T14:36:28.539Z","metaMigratedFrom":"YAML","title":"Snapps Workshop","breaks":true,"slideOptions":"{\"theme\":\"serif\",\"transition\":\"fade\",\"progress\":true}","contributors":"[{\"id\":\"0cd8f44c-d63a-4fb1-a9c1-a6c6da3e43e3\",\"add\":26094,\"del\":12296}]"}
    1010 views
   owned this note