Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental offchain storage pt 1 #1630

Merged
merged 45 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
1d0bdde
move existing reducer code into a new folder
mitschabaude May 2, 2024
941639d
remove `provable` reexport
mitschabaude May 2, 2024
693f06b
for each method on merkle list
mitschabaude May 2, 2024
bf24822
scaffold out merkle map rollup
mitschabaude May 2, 2024
1f2bf0d
some code golf
mitschabaude May 2, 2024
bd676ce
cloneable merkle tree
mitschabaude May 2, 2024
d29d490
get leaf of merkle tree
mitschabaude May 2, 2024
d9c3824
finish v0 of merkle map rollup
mitschabaude May 2, 2024
c5700ee
add outside proof logic
mitschabaude May 2, 2024
a1439c3
some comments with efficiency ideas
mitschabaude May 3, 2024
8fc54e0
serialization and custom efficient hashing
mitschabaude May 3, 2024
f5256c7
finish merkle leaf serialization
mitschabaude May 3, 2024
5a6604c
rename
mitschabaude May 3, 2024
23d926d
start typing out stuff
mitschabaude May 3, 2024
f39e013
nice option type
mitschabaude May 3, 2024
ca2dc19
more api scaffolding
mitschabaude May 3, 2024
6021181
example offchain state interaction
mitschabaude May 3, 2024
d442abf
bindings
mitschabaude May 3, 2024
cd1e402
minor
mitschabaude May 3, 2024
98fb766
start implementation
mitschabaude May 3, 2024
65645b3
minor
mitschabaude May 3, 2024
d19e3d0
fix
mitschabaude May 3, 2024
ecbce96
small fix
mitschabaude May 3, 2024
246d2d6
small fixes
mitschabaude May 3, 2024
9018c93
probably fixes
mitschabaude May 3, 2024
ebf3e47
+ update API
mitschabaude May 6, 2024
7bc958d
implement set()
mitschabaude May 13, 2024
80d66ab
don't use update() for now
mitschabaude May 13, 2024
bc2893d
change serialization and implement get()
mitschabaude May 13, 2024
e00fb47
settling
mitschabaude May 13, 2024
bd966c6
support onchain state default values
mitschabaude May 13, 2024
4f525ba
set state fields explicitly
mitschabaude May 13, 2024
7a10b49
Merge branch 'main' into feature/experimental-offchain-state
mitschabaude May 13, 2024
5ead7a5
fix how we get contract
mitschabaude May 13, 2024
0c51655
improve unconstrained + empty
mitschabaude May 13, 2024
75c6dd2
refactor to two separate program methods for now
mitschabaude May 13, 2024
1e5b5a3
disable proofs
mitschabaude May 13, 2024
e9df987
suprisingly non mutating api?
mitschabaude May 13, 2024
fc7e633
use merkle tree, not map (set leaf is different)
mitschabaude May 13, 2024
d7502aa
make unit test more test-like
mitschabaude May 13, 2024
cda0e1f
fixup contracts without state
mitschabaude May 13, 2024
44d5616
some boyscouting
mitschabaude May 14, 2024
0df6f4e
export option type
mitschabaude May 14, 2024
744251e
option docs, initial state example
mitschabaude May 14, 2024
28a4e69
changelog
mitschabaude May 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to remove this method entirely now, I think?

Copy link
Member Author

@mitschabaude mitschabaude May 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no because this example uses the fact that it's a @method and affects provedState

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';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved Reducer code to a separate file, zkapp.ts is unreadable enough without it

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
Loading