Skip to content

Commit

Permalink
Merge pull request #1630 from o1-labs/feature/experimental-offchain-s…
Browse files Browse the repository at this point in the history
…tate

Experimental offchain storage pt 1
  • Loading branch information
mitschabaude committed May 15, 2024
2 parents 23cdfa3 + 28a4e69 commit 89c59c7
Show file tree
Hide file tree
Showing 25 changed files with 1,539 additions and 341 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,20 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Added

- `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`
- `Unconstrained.provableWithEmpty()` to create an unconstrained provable type with a known `empty()` value https://github.com/o1-labs/o1js/pull/1630
- `Permissions.VerificationKey`, a namespace for verification key permissions https://github.com/o1-labs/o1js/pull/1639
- Includes more accurate names for the `impossible` and `proof` permissions for verification keys, which are now called `impossibleDuringCurrentVersion` and `proofDuringCurrentVersion` respectively.

### Changed

- `State()` now optionally accepts an initial value as input parameter https://github.com/o1-labs/o1js/pull/1630
- Example: `@state(Field) x = State(Field(1));`
- Initial values will be set in the default `init()` method
- You no longer need a custom `init()` method to set initial values

### Fixes

- Fix absolute imports which prevented compilation in some TS projects that used o1js https://github.com/o1-labs/o1js/pull/1628
Expand Down
2 changes: 1 addition & 1 deletion src/bindings
3 changes: 1 addition & 2 deletions src/examples/simple-zkapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@ const doProofs = true;
const beforeGenesis = UInt64.from(Date.now());

class SimpleZkapp extends SmartContract {
@state(Field) x = State<Field>();
@state(Field) x = State(initialState);

events = { update: Field, payout: UInt64, payoutReceiver: PublicKey };

@method
async init() {
super.init();
this.x.set(initialState);
}

@method.returns(Field)
Expand Down
15 changes: 8 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export type {
FlexibleProvablePure,
InferProvable,
} from './lib/provable/types/struct.js';
export { provable, provablePure, Struct } from './lib/provable/types/struct.js';
export {
provable,
provablePure,
} from './lib/provable/types/provable-derivers.js';
export { Struct } from './lib/provable/types/struct.js';
export { Unconstrained } from './lib/provable/types/unconstrained.js';
export { Provable } from './lib/provable/provable.js';
export {
Expand All @@ -48,6 +52,7 @@ export { Gadgets } from './lib/provable/gadgets/gadgets.js';
export { Types } from './bindings/mina-transaction/types.js';

export { MerkleList, MerkleListIterator } from './lib/provable/merkle-list.js';
export { Option } from './lib/provable/option.js';

export * as Mina from './lib/mina/mina.js';
export {
Expand All @@ -59,12 +64,8 @@ export {
type PendingTransactionPromise,
} from './lib/mina/transaction.js';
export type { DeployArgs } from './lib/mina/zkapp.js';
export {
SmartContract,
method,
declareMethods,
Reducer,
} from './lib/mina/zkapp.js';
export { SmartContract, method, declareMethods } from './lib/mina/zkapp.js';
export { Reducer } from './lib/mina/actions/reducer.js';
export { state, State, declareState } from './lib/mina/state.js';

export type { JsonProof } from './lib/proof-system/zkprogram.js';
Expand Down
3 changes: 1 addition & 2 deletions src/lib/mina/account-update.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {
cloneCircuitValue,
FlexibleProvable,
provable,
provablePure,
StructNoJson,
} from '../provable/types/struct.js';
import { provable, provablePure } from '../provable/types/provable-derivers.js';
import {
memoizationContext,
memoizeWitness,
Expand Down
167 changes: 167 additions & 0 deletions src/lib/mina/actions/offchain-contract.unit-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
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 assert from 'assert';

const offchainState = OffchainState({
accounts: OffchainState.Map(PublicKey, UInt64),
totalSupply: OffchainState.Field(UInt64),
});

class StateProof extends offchainState.Proof {}

// example contract that interacts with offchain state

class ExampleContract extends SmartContract {
// TODO could have sugar for this like
// @OffchainState.commitment offchainState = OffchainState.Commitment();
@state(OffchainStateCommitments) offchainState = State(
OffchainStateCommitments.empty()
);

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

@method
async transfer(from: PublicKey, to: PublicKey, amount: UInt64) {
let fromOption = await offchainState.fields.accounts.get(from);
let fromBalance = fromOption.assertSome('sender account exists');

let toOption = await offchainState.fields.accounts.get(to);
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!
*/
offchainState.fields.accounts.set(from, fromBalance.sub(amount));
offchainState.fields.accounts.set(to, toBalance.add(amount));
}

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

@method.returns(UInt64)
async getBalance(address: PublicKey) {
return (await offchainState.fields.accounts.get(address)).orElse(0n);
}

@method
async settle(proof: StateProof) {
await offchainState.settle(proof);
}
}

// test code below

// setup
const proofsEnabled = true;

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

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

if (proofsEnabled) {
console.time('compile program');
await offchainState.compile();
console.timeEnd('compile program');
console.time('compile contract');
await ExampleContract.compile();
console.timeEnd('compile contract');
}

// deploy and create first account

console.time('deploy');
await Mina.transaction(sender, async () => {
await contract.deploy();
})
.sign([sender.key, contractAccount.key])
.prove()
.send();
console.timeEnd('deploy');

// create first account

console.time('create account');
await Mina.transaction(sender, async () => {
await contract.createAccount(sender, UInt64.from(1000));
})
.sign([sender.key])
.prove()
.send();
console.timeEnd('create account');

// settle

console.time('settlement proof 1');
let proof = await offchainState.createSettlementProof();
console.timeEnd('settlement proof 1');

console.time('settle 1');
await Mina.transaction(sender, () => contract.settle(proof))
.sign([sender.key])
.prove()
.send();
console.timeEnd('settle 1');

// check balance and supply
await checkAgainstSupply(1000n);

// transfer

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

// settle

console.time('settlement proof 2');
proof = await offchainState.createSettlementProof();
console.timeEnd('settlement proof 2');

console.time('settle 2');
await Mina.transaction(sender, () => contract.settle(proof))
.sign([sender.key])
.prove()
.send();
console.timeEnd('settle 2');

// check balance and supply
await checkAgainstSupply(1000n);

// test helper

async function checkAgainstSupply(expectedSupply: 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();

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

0 comments on commit 89c59c7

Please sign in to comment.