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

Merkle list and token account update iterator #1398

Merged
merged 59 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
a6c3ebf
merkle list
mitschabaude Jan 22, 2024
f89c5a7
export unconstrained properly
mitschabaude Jan 22, 2024
dc58706
export hash with prefix
mitschabaude Jan 22, 2024
9719eb7
generic merkle tree
mitschabaude Jan 22, 2024
6540c42
start call forest
mitschabaude Jan 22, 2024
8219ef0
call forest logic
mitschabaude Jan 22, 2024
c65c92c
properly generic merkle list
mitschabaude Jan 23, 2024
85f9174
make callforest a merkle list
mitschabaude Jan 23, 2024
c4dc1f4
start writing pop account update
mitschabaude Jan 23, 2024
c21ca68
finish core pop account update logic
mitschabaude Jan 23, 2024
ae3ac66
finish pop account update & refactor partial call forest
mitschabaude Jan 23, 2024
c6b7f6c
improve variable naming
mitschabaude Jan 23, 2024
6a76e43
merkle array type
mitschabaude Jan 23, 2024
e096d3c
make call forest a merkle array, iteration
mitschabaude Jan 23, 2024
fa64041
tweaks, doccomments
mitschabaude Jan 23, 2024
7576435
improve comment
mitschabaude Jan 23, 2024
482007d
move files for now
mitschabaude Jan 23, 2024
f679c15
start creating test
mitschabaude Jan 23, 2024
342705f
compute stack hash that is equivalent between provable & mins-aigner,…
mitschabaude Jan 23, 2024
895dd71
test on wider/deeper trees
mitschabaude Jan 23, 2024
7372293
debugging
mitschabaude Jan 23, 2024
12d7449
fix test by fixing order in hashCons
mitschabaude Jan 24, 2024
4c4ce8e
fix hash.empty() and merkleArray.next()
mitschabaude Jan 24, 2024
737fc24
make some code generic for reuse
mitschabaude Jan 24, 2024
fb8b3fa
refactor, start test that traverses the tree
mitschabaude Jan 24, 2024
e1adaaf
confirm that callforest.next() works
mitschabaude Jan 24, 2024
32e606b
it actually works when not skipping subtrees
mitschabaude Jan 24, 2024
ad069e5
finish unit tests for call forest iteration
mitschabaude Jan 24, 2024
2803ed9
rename, doccomments
mitschabaude Jan 24, 2024
18fb569
unify base types of merklelist/array
mitschabaude Jan 25, 2024
d3d777b
simple way to update unconstrained
mitschabaude Jan 25, 2024
a970de2
change merkle array start index from -1 to 0
mitschabaude Jan 25, 2024
42fcb39
invert internal order in merkle list
mitschabaude Jan 25, 2024
28e906b
make merkle list the main callforest intf
mitschabaude Jan 25, 2024
e0c44fd
lower level deps for merkle list
mitschabaude Jan 25, 2024
cbde4ff
move merkle list
mitschabaude Jan 25, 2024
c6813a4
expose merkle list/array
mitschabaude Jan 25, 2024
de2f3ca
remove unnecessary code
mitschabaude Jan 25, 2024
dc09c20
fix dependencies
mitschabaude Jan 25, 2024
bd8f535
fix build
mitschabaude Jan 25, 2024
985f570
add edge case to investigate
mitschabaude Jan 27, 2024
09f9fb9
export token contract
mitschabaude Jan 29, 2024
05b300b
move call forest code next to account update
mitschabaude Jan 31, 2024
7cf2a85
remove accidental/premature changes
mitschabaude Feb 1, 2024
4632f93
prune import not needed yet
mitschabaude Feb 1, 2024
b8b5cf6
renames and changelog
mitschabaude Feb 1, 2024
12060ae
better file name
mitschabaude Feb 1, 2024
687891a
revert rename which became unnecessary
mitschabaude Feb 1, 2024
1a4bd8a
bindings
mitschabaude Feb 1, 2024
cfc8172
more renaming
mitschabaude Feb 1, 2024
d1f87be
add popIf and doccomments
mitschabaude Feb 1, 2024
c8d6bfc
improve clarity of forest iteration logic
mitschabaude Feb 1, 2024
0f38354
normalize comments
mitschabaude Feb 1, 2024
dacc269
Merge branch 'main' into feature/call-forest-iterator
mitschabaude Feb 5, 2024
fbeb4ff
submodules
mitschabaude Feb 5, 2024
4703db0
fix changelog
mitschabaude Feb 5, 2024
1aecab1
Merge branch 'main' into feature/call-forest-iterator
mitschabaude Feb 6, 2024
a7b70af
support custom empty hash in merkle list
mitschabaude Feb 12, 2024
f3c1762
merkle list example that treats actions as a merkle list
mitschabaude Feb 12, 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased](https://github.com/o1-labs/o1js/compare/834a44002...HEAD)

### Added

- `MerkleList<T>` to enable provable operations on a dynamically-sized list https://github.com/o1-labs/o1js/pull/1398
mitschabaude marked this conversation as resolved.
Show resolved Hide resolved
- including `MerkleListIterator<T>` to iterate over a merkle list
- `TokenAccountUpdateIterator`, a primitive for token contracts to iterate over all token account updates in a transaction. https://github.com/o1-labs/o1js/pull/1398

## [0.16.0](https://github.com/o1-labs/o1js/compare/e5d1e0f...834a44002)

### Breaking changes
Expand Down
148 changes: 148 additions & 0 deletions src/examples/zkapps/reducer/actions-as-merkle-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* This example shows how to iterate through incoming actions, not using `Reducer.reduce` but by
* treating the actions as a merkle list.
*
* This is mainly intended as an example for using `MerkleList`, but it might also be useful as
* a blueprint for processing actions in a custom and more explicit way.
*/
import {
AccountUpdate,
Bool,
Field,
MerkleList,
Mina,
Provable,
PublicKey,
Reducer,
SmartContract,
method,
assert,
} from 'o1js';

const { Actions } = AccountUpdate;

// in this example, an action is just a public key
type Action = PublicKey;
const Action = PublicKey;

// the actions within one account update are a Merkle list with a custom hash
const emptyHash = Actions.empty().hash;
const nextHash = (hash: Field, action: Action) =>
Actions.pushEvent({ hash, data: [] }, action.toFields()).hash;

class MerkleActions extends MerkleList.create(Action, nextHash, emptyHash) {}

// the "action state" / actions from many account updates is a Merkle list
// of the above Merkle list, with another custom hash
let emptyActionsHash = Actions.emptyActionState();
const nextActionsHash = (hash: Field, actions: MerkleActions) =>
Actions.updateSequenceState(hash, actions.hash);

class MerkleActionss extends MerkleList.create(
MerkleActions.provable,
nextActionsHash,
emptyActionsHash
) {}

// constants for our static-sized provable code
const MAX_UPDATES_WITH_ACTIONS = 100;
const MAX_ACTIONS_PER_UPDATE = 2;

/**
* This contract allows you to push either 1 or 2 public keys as actions,
* and has a reducer-like method which checks whether a given public key is contained in those actions.
*/
class ActionsContract extends SmartContract {
reducer = Reducer({ actionType: Action });

@method
postAddress(address: PublicKey) {
this.reducer.dispatch(address);
}

// to exhibit the generality of reducer: can dispatch more than 1 action per account update
@method postTwoAddresses(a1: PublicKey, a2: PublicKey) {
this.reducer.dispatch(a1);
this.reducer.dispatch(a2);
}

@method
assertContainsAddress(address: PublicKey) {
// get actions and, in a witness block, wrap them in a Merkle list of lists

// note: need to reverse here because `getActions()` returns the last pushed action last,
// but MerkleList.from() wants it to be first to match the natural iteration order
let actionss = this.reducer.getActions().reverse();

let merkleActionss = Provable.witness(MerkleActionss.provable, () =>
MerkleActionss.from(actionss.map((as) => MerkleActions.from(as)))
);

// prove that we know the correct action state
this.account.actionState.requireEquals(merkleActionss.hash);

// now our provable code to process the actions is very straight-forward
// (note: if we're past the actual sizes, `.pop()` returns a dummy Action -- in this case, the "empty" public key which is not equal to any real address)
let hasAddress = Bool(false);

for (let i = 0; i < MAX_UPDATES_WITH_ACTIONS; i++) {
let merkleActions = merkleActionss.pop();

for (let j = 0; j < MAX_ACTIONS_PER_UPDATE; j++) {
let action = merkleActions.pop();
hasAddress = hasAddress.or(action.equals(address));
}
}

assert(hasAddress);
}
}

// TESTS

// set up a local blockchain

let Local = Mina.LocalBlockchain({ proofsEnabled: false });
Mina.setActiveInstance(Local);

let [
{ publicKey: sender, privateKey: senderKey },
{ publicKey: zkappAddress, privateKey: zkappKey },
{ publicKey: otherAddress },
{ publicKey: anotherAddress },
] = Local.testAccounts;

let zkapp = new ActionsContract(zkappAddress);

// deploy the contract

await ActionsContract.compile();
console.log(
`rows for ${MAX_UPDATES_WITH_ACTIONS} updates with actions`,
ActionsContract.analyzeMethods().assertContainsAddress.rows
);
let deployTx = await Mina.transaction(sender, () => zkapp.deploy());
await deployTx.sign([senderKey, zkappKey]).send();

// push some actions

let dispatchTx = await Mina.transaction(sender, () => {
zkapp.postAddress(otherAddress);
zkapp.postAddress(zkappAddress);
zkapp.postTwoAddresses(anotherAddress, sender);
zkapp.postAddress(anotherAddress);
zkapp.postTwoAddresses(zkappAddress, otherAddress);
});
await dispatchTx.prove();
await dispatchTx.sign([senderKey]).send();

assert(zkapp.reducer.getActions().length === 5);

// check if the actions contain the `sender` address

Local.setProofsEnabled(true);
let containsTx = await Mina.transaction(sender, () =>
zkapp.assertContainsAddress(sender)
);
await containsTx.prove();
await containsTx.sign([senderKey]).send();
12 changes: 10 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export {
} from './lib/foreign-field.js';
export { createForeignCurve, ForeignCurve } from './lib/foreign-curve.js';
export { createEcdsa, EcdsaSignature } from './lib/foreign-ecdsa.js';
export { Poseidon, TokenSymbol } from './lib/hash.js';
export { Poseidon, TokenSymbol, ProvableHashable } from './lib/hash.js';
export { Keccak } from './lib/keccak.js';
export { Hash } from './lib/hashes-combined.js';

Expand All @@ -21,7 +21,6 @@ export type {
FlexibleProvable,
FlexibleProvablePure,
InferProvable,
Unconstrained,
} from './lib/circuit_value.js';
export {
CircuitValue,
Expand All @@ -31,6 +30,7 @@ export {
provable,
provablePure,
Struct,
Unconstrained,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is a fixup - Unconstrained was announced in a previous release but not exported

} from './lib/circuit_value.js';
export { Provable } from './lib/provable.js';
export { Circuit, Keypair, public_, circuitMain } from './lib/circuit.js';
Expand All @@ -40,6 +40,11 @@ export { Packed, Hashed } from './lib/provable-types/packed.js';
export { Gadgets } from './lib/gadgets/gadgets.js';
export { Types } from './bindings/mina-transaction/types.js';

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

export * as Mina from './lib/mina.js';
export type { DeployArgs } from './lib/zkapp.js';
export {
Expand Down Expand Up @@ -70,8 +75,11 @@ export {
Permissions,
ZkappPublicInput,
TransactionVersion,
AccountUpdateForest,
} from './lib/account_update.js';

export { TokenAccountUpdateIterator } from './lib/mina/token/forest-iterator.js';

export type { TransactionStatus } from './lib/fetch.js';
export {
fetchAccount,
Expand Down
96 changes: 93 additions & 3 deletions src/lib/account_update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
FlexibleProvable,
provable,
provablePure,
Struct,
} from './circuit_value.js';
import { memoizationContext, memoizeWitness, Provable } from './provable.js';
import { Field, Bool } from './core.js';
Expand Down Expand Up @@ -33,7 +34,12 @@ import {
Actions,
} from '../bindings/mina-transaction/transaction-leaves.js';
import { TokenId as Base58TokenId } from './base58-encodings.js';
import { hashWithPrefix, packToFields } from './hash.js';
import {
hashWithPrefix,
packToFields,
Poseidon,
ProvableHashable,
} from './hash.js';
import {
mocks,
prefixes,
Expand All @@ -47,9 +53,21 @@ import { transactionCommitments } from '../mina-signer/src/sign-zkapp-command.js
import { currentTransaction } from './mina/transaction-context.js';
import { isSmartContract } from './mina/smart-contract-base.js';
import { activeInstance } from './mina/mina-instance.js';
import {
genericHash,
MerkleList,
MerkleListBase,
} from './provable-types/merkle-list.js';
import { Hashed } from './provable-types/packed.js';

// external API
export { AccountUpdate, Permissions, ZkappPublicInput, TransactionVersion };
export {
AccountUpdate,
Permissions,
ZkappPublicInput,
TransactionVersion,
AccountUpdateForest,
};
// internal API
export {
smartContractContext,
Expand All @@ -74,6 +92,8 @@ export {
SmartContractContext,
dummySignature,
LazyProof,
AccountUpdateTree,
hashAccountUpdate,
};

const ZkappStateLength = 8;
Expand Down Expand Up @@ -1496,6 +1516,72 @@ type WithCallers = {
children: WithCallers[];
};

// call forest stuff

function hashAccountUpdate(update: AccountUpdate) {
return genericHash(AccountUpdate, prefixes.body, update);
}

class HashedAccountUpdate extends Hashed.create(
AccountUpdate,
hashAccountUpdate
) {}

type AccountUpdateTree = {
accountUpdate: Hashed<AccountUpdate>;
calls: MerkleListBase<AccountUpdateTree>;
};
const AccountUpdateTree: ProvableHashable<AccountUpdateTree> = Struct({
accountUpdate: HashedAccountUpdate.provable,
calls: MerkleListBase<AccountUpdateTree>(),
});

/**
* Class which represents a forest (list of trees) of account updates,
* in a compressed way which allows iterating and selectively witnessing the account updates.
*
* The (recursive) type signature is:
* ```
* type AccountUpdateForest = MerkleList<AccountUpdateTree>;
* type AccountUpdateTree = {
* accountUpdate: Hashed<AccountUpdate>;
* calls: AccountUpdateForest;
* };
* ```
*/
class AccountUpdateForest extends MerkleList.create(
AccountUpdateTree,
merkleListHash
) {
static fromArray(updates: AccountUpdate[]): AccountUpdateForest {
let nodes = updates.map((update) => {
let accountUpdate = HashedAccountUpdate.hash(update);
let calls = AccountUpdateForest.fromArray(update.children.accountUpdates);
return { accountUpdate, calls };
});

return AccountUpdateForest.from(nodes);
}
}

// how to hash a forest

function merkleListHash(forestHash: Field, tree: AccountUpdateTree) {
return hashCons(forestHash, hashNode(tree));
}
function hashNode(tree: AccountUpdateTree) {
return Poseidon.hashWithPrefix(prefixes.accountUpdateNode, [
tree.accountUpdate.hash,
tree.calls.hash,
]);
}
function hashCons(forestHash: Field, nodeHash: Field) {
return Poseidon.hashWithPrefix(prefixes.accountUpdateCons, [
nodeHash,
forestHash,
]);
}

const CallForest = {
// similar to Mina_base.ZkappCommand.Call_forest.to_account_updates_list
// takes a list of accountUpdates, which each can have children, so they form a "forest" (list of trees)
Expand Down Expand Up @@ -1528,14 +1614,18 @@ const CallForest = {
// hashes a accountUpdate's children (and their children, and ...) to compute
// the `calls` field of ZkappPublicInput
hashChildren(update: AccountUpdate): Field {
if (!Provable.inCheckedComputation()) {
return CallForest.hashChildrenBase(update);
}

let { callsType } = update.children;
// compute hash outside the circuit if callsType is "Witness"
// i.e., allowing accountUpdates with arbitrary children
if (callsType.type === 'Witness') {
return Provable.witness(Field, () => CallForest.hashChildrenBase(update));
}
let calls = CallForest.hashChildrenBase(update);
if (callsType.type === 'Equals' && Provable.inCheckedComputation()) {
if (callsType.type === 'Equals') {
calls.assertEquals(callsType.value);
}
return calls;
Expand Down
17 changes: 17 additions & 0 deletions src/lib/circuit_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,13 @@ and Provable.asProver() blocks, which execute outside the proof.
this.option = { isSome: true, value };
}

/**
* Set the unconstrained value to the same as another `Unconstrained`.
*/
setTo(value: Unconstrained<T>) {
this.option = value.option;
}

/**
* Create an `Unconstrained` with the given `value`.
*/
Expand All @@ -555,6 +562,16 @@ and Provable.asProver() blocks, which execute outside the proof.
);
}

/**
* Update an `Unconstrained` by a witness computation.
*/
updateAsProver(compute: (value: T) => T) {
return Provable.asProver(() => {
let value = this.get();
this.set(compute(value));
});
}

static provable: Provable<Unconstrained<any>> & {
toInput: (x: Unconstrained<any>) => {
fields?: Field[];
Expand Down
Loading
Loading