Skip to content

Commit

Permalink
Merge branch 'main' into fix/method-returns-type
Browse files Browse the repository at this point in the history
  • Loading branch information
Trivo25 committed May 15, 2024
2 parents d6bd723 + 76db7d1 commit 470dd74
Show file tree
Hide file tree
Showing 12 changed files with 648 additions and 196 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
_Security_ in case of vulnerabilities.
-->

## [Unreleased](https://github.com/o1-labs/o1js/compare/4a17de857...HEAD)
## [Unreleased](https://github.com/o1-labs/o1js/compare/6a1012162...HEAD)

## [1.2.0](https://github.com/o1-labs/o1js/compare/4a17de857...6a1012162) - 2024-05-14

### Added

- **Offchain state MVP** exported under `Experimental.OffchainState` https://github.com/o1-labs/o1js/pull/1630 https://github.com/o1-labs/o1js/pull/1652
- allows you to store any number of fields and key-value maps on your zkApp
- implemented using actions which define an offchain Merkle tree
- `Option` for defining an optional version of any provable type https://github.com/o1-labs/o1js/pull/1630
- `MerkleTree.clone()` and `MerkleTree.getLeaf()`, new convenience methods for merkle trees https://github.com/o1-labs/o1js/pull/1630
- `MerkleList.forEach()`, a simple and safe way for iterating over a `MerkleList`
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "o1js",
"description": "TypeScript framework for zk-SNARKs and zkApps",
"version": "1.1.0",
"version": "1.2.0",
"license": "Apache-2.0",
"homepage": "https://github.com/o1-labs/o1js/",
"keywords": [
Expand Down
14 changes: 14 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export { setNumberOfWorkers } from './lib/proof-system/workers.js';

// experimental APIs
import { memoizeWitness } from './lib/provable/provable.js';
import * as OffchainState_ from './lib/mina/actions/offchain-state.js';
export { Experimental };

const Experimental_ = {
Expand All @@ -140,6 +141,19 @@ const Experimental_ = {
*/
namespace Experimental {
export let memoizeWitness = Experimental_.memoizeWitness;

// offchain state
export let OffchainState = OffchainState_.OffchainState;

/**
* Commitments that keep track of the current state of an offchain Merkle tree constructed from actions.
* Intended to be stored on-chain.
*
* Fields:
* - `root`: The root of the current Merkle tree
* - `actionState`: The hash pointing to the list of actions that have been applied to form the current Merkle tree
*/
export class OffchainStateCommitments extends OffchainState_.OffchainStateCommitments {}
}

Error.stackTraceLimit = 100000;
113 changes: 84 additions & 29 deletions src/lib/mina/actions/offchain-contract.unit-test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { OffchainState, OffchainStateCommitments } from './offchain-state.js';
import { PublicKey } from '../../provable/crypto/signature.js';
import { UInt64 } from '../../provable/int.js';
import { SmartContract, method } from '../zkapp.js';
import { Mina, State, state } from '../../../index.js';
import {
SmartContract,
method,
Mina,
State,
state,
PublicKey,
UInt64,
Experimental,
} from '../../../index.js';
import assert from 'assert';

const proofsEnabled = true;

const { OffchainState, OffchainStateCommitments } = Experimental;

const offchainState = OffchainState({
accounts: OffchainState.Map(PublicKey, UInt64),
totalSupply: OffchainState.Field(UInt64),
Expand All @@ -23,12 +32,21 @@ class ExampleContract extends SmartContract {

@method
async createAccount(address: PublicKey, amountToMint: UInt64) {
offchainState.fields.accounts.set(address, amountToMint);

// TODO `totalSupply` easily gets into a wrong state here on concurrent calls.
// and using `.update()` doesn't help either
let totalSupply = await offchainState.fields.totalSupply.get();
offchainState.fields.totalSupply.set(totalSupply.add(amountToMint));
// setting `from` to `undefined` means that the account must not exist yet
offchainState.fields.accounts.update(address, {
from: undefined,
to: amountToMint,
});

// TODO using `update()` on the total supply means that this method
// can only be called once every settling cycle
let totalSupplyOption = await offchainState.fields.totalSupply.get();
let totalSupply = totalSupplyOption.orElse(0n);

offchainState.fields.totalSupply.update({
from: totalSupplyOption,
to: totalSupply.add(amountToMint),
});
}

@method
Expand All @@ -40,19 +58,23 @@ class ExampleContract extends SmartContract {
let toBalance = toOption.orElse(0n);

/**
* FIXME using `set()` here is completely insecure, a sender can easily double-spend by sending multiple transactions,
* which will all use the same initial balance.
* Even using a naive version of `update()` would give a double-spend opportunity, because the updates are not rejected atomically:
* if the `to` update gets accepted but the `from` update fails, it's a double-spend
* => properly implementing this needs a version of `update()` that rejects all state actions in one update if any of them fails!
* Update both accounts atomically.
*
* This is safe, because both updates will only be accepted if both previous balances are still correct.
*/
offchainState.fields.accounts.set(from, fromBalance.sub(amount));
offchainState.fields.accounts.set(to, toBalance.add(amount));
offchainState.fields.accounts.update(from, {
from: fromOption,
to: fromBalance.sub(amount),
});
offchainState.fields.accounts.update(to, {
from: toOption,
to: toBalance.add(amount),
});
}

@method.returns(UInt64)
async getSupply() {
return await offchainState.fields.totalSupply.get();
return (await offchainState.fields.totalSupply.get()).orElse(0n);
}

@method.returns(UInt64)
Expand All @@ -69,12 +91,11 @@ class ExampleContract extends SmartContract {
// test code below

// setup
const proofsEnabled = true;

const Local = await Mina.LocalBlockchain({ proofsEnabled });
Mina.setActiveInstance(Local);

let [sender, receiver, contractAccount] = Local.testAccounts;
let [sender, receiver, contractAccount, other] = Local.testAccounts;
let contract = new ExampleContract(contractAccount);
offchainState.setContractInstance(contract);

Expand Down Expand Up @@ -102,7 +123,11 @@ console.timeEnd('deploy');

console.time('create account');
await Mina.transaction(sender, async () => {
// first call (should succeed)
await contract.createAccount(sender, UInt64.from(1000));

// second call (should fail)
await contract.createAccount(sender, UInt64.from(2000));
})
.sign([sender.key])
.prove()
Expand All @@ -123,20 +148,42 @@ await Mina.transaction(sender, () => contract.settle(proof))
console.timeEnd('settle 1');

// check balance and supply
await checkAgainstSupply(1000n);
await check({ expectedSupply: 1000n, expectedSenderBalance: 1000n });

// transfer
// transfer (should succeed)

console.time('transfer');
await Mina.transaction(sender, () =>
contract.transfer(sender, receiver, UInt64.from(100))
)
await Mina.transaction(sender, async () => {
await contract.transfer(sender, receiver, UInt64.from(100));
})
.sign([sender.key])
.prove()
.send();
console.timeEnd('transfer');

console.time('more transfers');
Local.setProofsEnabled(false); // we run these without proofs to save time

await Mina.transaction(sender, async () => {
// more transfers that should fail
// (these are enough to need two proof steps during settlement)
await contract.transfer(sender, receiver, UInt64.from(200));
await contract.transfer(sender, receiver, UInt64.from(300));
await contract.transfer(sender, receiver, UInt64.from(400));

// create another account (should succeed)
await contract.createAccount(other, UInt64.from(555));

// create existing account again (should fail)
await contract.createAccount(receiver, UInt64.from(333));
})
.sign([sender.key])
.prove()
.send();
console.timeEnd('more transfers');

// settle
Local.setProofsEnabled(proofsEnabled);

console.time('settlement proof 2');
proof = await offchainState.createSettlementProof();
Expand All @@ -150,18 +197,26 @@ await Mina.transaction(sender, () => contract.settle(proof))
console.timeEnd('settle 2');

// check balance and supply
await checkAgainstSupply(1000n);
await check({ expectedSupply: 1555n, expectedSenderBalance: 900n });

// test helper

async function checkAgainstSupply(expectedSupply: bigint) {
async function check({
expectedSupply,
expectedSenderBalance,
}: {
expectedSupply: bigint;
expectedSenderBalance: bigint;
}) {
let supply = (await contract.getSupply()).toBigInt();
assert.strictEqual(supply, expectedSupply);

let balanceSender = (await contract.getBalance(sender)).toBigInt();
let balanceReceiver = (await contract.getBalance(receiver)).toBigInt();
let balanceOther = (await contract.getBalance(other)).toBigInt();

console.log('balance (sender)', balanceSender);
console.log('balance (recv)', balanceReceiver);
assert.strictEqual(balanceSender + balanceReceiver, supply);
assert.strictEqual(balanceSender + balanceReceiver + balanceOther, supply);
assert.strictEqual(balanceSender, expectedSenderBalance);
}
Loading

0 comments on commit 470dd74

Please sign in to comment.