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.
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) {
// ...
}
}
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 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(
// ...
}