Ethereum is a computer that can store user programs. Users execute a program by sending a transaction specifying one of its methods.
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).
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.
Note: one account = one snapp
Each method of a snapp is actually compiled to a program which:
under the hood:
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.
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).
The Snapp CLI allows you to easily set up, test, and deploy your smart contract.
$ npm install -g snapp-cli
$ snapp project my-proj
$ 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
To write a snapp, extend the SmartContract
class of snarkyjs.
import { SmartContract, state, method, init, State, Field } from 'snarkyjs';
class MySmartContract extends SmartContract { }
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.
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;
}
}
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:
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).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
.
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);
}
}
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: ??
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
Consult the documentation to see operations on Field
, or let auto-complete do that for you.
snarkyjs provides a number of useful types based on Field
:
UInt64
UInt32
PublicKey
Optional
Bool
array
Group
EndoScalar
Scalar
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;
}
}
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.Account
custom type that contains a counter (represented by a field element) (TODO: can we make it a u64?) and a public key.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.
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:
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.
TODO: do we really need to name them "merkle trees" or "accumulators". Is there a more dev-friendly name?
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,
}
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.
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.
create a smart contract that makes use of a Merkle tree
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() {
}
@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()
TKTK
Do we have time for that?
TKTK
@proofSystem
class RollupProof extends ProofWithInput<RollupStateTransition> {
@branch static processDeposit(
// ...
}