Snapps


Snapps

Ethereum is a computer that can store user programs. Users execute a program by sending a transaction specifying one of its methods.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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).


Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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.


Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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.

$ npm install -g snapp-cli
$ snapp project my-proj

Setup (NOT READY FOR WORKSHOP)


Setup

$ git clone git@github.com:o1-labs/snarkyjs.git
$ npm install && npm run build
$ ./run src/examples/api_exploration.ts

or better:

$ mkdir my-project && cd my-project
$ npm init -y
$ npm install @o1labs/snarkyjs typescript

or even better:

$ 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.

import { SmartContract, state, method, init, State, Field } from 'snarkyjs';

class MySmartContract extends SmartContract { }

Hello World

A snapp can contain some state (mutable storage).

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

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

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.)

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

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

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

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

Consult the documentation to see operations on Field, or let auto-complete do that for you.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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:

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.
  • 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.
  • Signature.verify(publicKey: PublicKey, msg: Field[]): Bool → verify signature.
  • ?

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.
  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

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.

@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.

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:

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

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

@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);
}
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

@proofSystem
class RollupProof extends ProofWithInput<RollupStateTransition> {
  @branch static processDeposit(
  // ...
}
Select a repo