Skip to content

Commit

Permalink
Merge pull request #1577 from o1-labs/reducer-v1-prep
Browse files Browse the repository at this point in the history
use MerkleList for reducer
  • Loading branch information
Trivo25 committed Apr 18, 2024
2 parents f66233b + af580c0 commit cd428b6
Show file tree
Hide file tree
Showing 15 changed files with 385 additions and 317 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Added `Poseidon.Unsafe.hashToGroup()` as a more efficient, non-deterministic version for advanced use cases
- A `Transaction`'s `prove` method no longer returns the proofs promise directly, but rather returns a `Transaction` promise, the resolved value of which contains a `proofs` prop. https://github.com/o1-labs/o1js/pull/1567
- The `Transaction` type now has two type params `Proven extends boolean` and `Signed extends boolean`, which are used to conditionally show/hide relevant state. https://github.com/o1-labs/o1js/pull/1567
- Improved functionality of `MerkleList` and `MerkleListIterator` for easier traversal of `MerkleList`s. https://github.com/o1-labs/o1js/pull/1562
- Simplified internal logic of reducer. https://github.com/o1-labs/o1js/pull/1577
- `contract.getActions()` now returns a `MerkleList`

### Added

Expand Down
13 changes: 6 additions & 7 deletions src/examples/zkapps/dex/dex-with-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,16 +261,16 @@ class DexTokenHolder extends SmartContract {
let l = Provable.witness(UInt64, (): UInt64 => {
let l = dex.totalSupply.get().toBigInt();
// dex.totalSupply.assertNothing();
for (let [action] of actions) {
l += action.dl.toBigInt();
for (let action of actions.data.get()) {
l += action.element.data.get()[0].element.dl.toBigInt();
}
return UInt64.from(l);
});

// get our token balance
let x = this.account.balance.getAndRequireEquals();

let redeemActionState = dex.reducer.forEach(
dex.reducer.forEach(
actions,
({ address, dl }) => {
// for every user that redeemed liquidity, we calculate the token output
Expand All @@ -286,19 +286,18 @@ class DexTokenHolder extends SmartContract {
l = l.sub(dl);
x = x.add(dx);
},
fromActionState,
{
maxTransactionsWithActions: DexTokenHolder.redeemActionBatchSize,
maxUpdatesWithActions: DexTokenHolder.redeemActionBatchSize,
// DEX contract doesn't allow setting preconditions from outside (= w/o proof)
skipActionStatePrecondition: true,
}
);

// update action state so these payments can't be triggered a 2nd time
this.redeemActionState.set(redeemActionState);
this.redeemActionState.set(actions.hash);

// precondition on the DEX contract, to prove we used the right actions & token supply
await dex.assertActionsAndSupply(redeemActionState, l);
await dex.assertActionsAndSupply(actions.hash, l);
}

// this works for both directions (in our case where both tokens use the same contract)
Expand Down
74 changes: 29 additions & 45 deletions src/examples/zkapps/reducer/actions-as-merkle-list-iterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@
* a blueprint for processing actions in a custom and more explicit way.
*/
import {
AccountUpdate,
Bool,
Field,
MerkleList,
Mina,
Provable,
State,
state,
Reducer,
Expand All @@ -20,28 +16,6 @@ import {
assert,
} from 'o1js';

const { Actions } = AccountUpdate;

// in this example, an action is just a increment of type Field
// the actions within one account update are a Merkle list with a custom hash
const emptyHash = Actions.empty().hash;
const nextHash = (hash: Field, action: Field) =>
Actions.pushEvent({ hash, data: [] }, action.toFields()).hash;

class MerkleActions extends MerkleList.create(Field, 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;
Expand All @@ -60,32 +34,42 @@ class ActionsContract extends SmartContract {
this.reducer.dispatch(inc);
}

@method
async twoIncrements(inc1: Field, inc2: Field) {
this.reducer.dispatch(inc1);
this.reducer.dispatch(inc2);
}

@method
async accumulate() {
// get actions and, in a witness block, wrap them in a Merkle list of lists

// get all actions
let actionss = this.reducer.getActions();

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

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

let counter = Field(0);

let iter = merkleActionss.startIterating();
let iter = actions.startIterating();
let lastAction = Field(0);

for (let i = 0; i < MAX_UPDATES_WITH_ACTIONS; i++) {
let merkleActions = iter.next();
let innerIter = merkleActions.startIterating();
for (let j = 0; j < MAX_ACTIONS_PER_UPDATE; j++) {
let action = innerIter.next();
counter = counter.add(action);

// we require that every action is greater than the previous one, except for dummy (0) actions
// this checks that actions are applied in the right order
assert(action.equals(0).or(action.greaterThan(lastAction)));
lastAction = action;
}
innerIter.assertAtEnd();
}
iter.assertAtEnd();

this.counter.set(this.counter.getAndRequireEquals().add(counter));
}
Expand All @@ -98,9 +82,9 @@ class ActionsContract extends SmartContract {
let Local = await Mina.LocalBlockchain({ proofsEnabled: false });
Mina.setActiveInstance(Local);

let [sender, zkappAddress] = Local.testAccounts;
let [sender, contractAddress] = Local.testAccounts;

let zkapp = new ActionsContract(zkappAddress);
let contract = new ActionsContract(contractAddress);

// deploy the contract

Expand All @@ -109,28 +93,28 @@ console.log(
`rows for ${MAX_UPDATES_WITH_ACTIONS} updates with actions`,
(await ActionsContract.analyzeMethods()).accumulate.rows
);
let deployTx = await Mina.transaction(sender, async () => zkapp.deploy());
await deployTx.sign([sender.key, zkappAddress.key]).send();
let deployTx = await Mina.transaction(sender, async () => contract.deploy());
await deployTx.sign([sender.key, contractAddress.key]).send();

// push some actions

let dispatchTx = await Mina.transaction(sender, async () => {
await zkapp.increment(Field(1));
await zkapp.increment(Field(3));
await zkapp.increment(Field(1));
await zkapp.increment(Field(9));
await zkapp.increment(Field(18));
await contract.increment(Field(1));
await contract.increment(Field(3));
await contract.increment(Field(5));
await contract.increment(Field(9));
await contract.twoIncrements(Field(18), Field(19));
});
await dispatchTx.prove();
await dispatchTx.sign([sender.key]).send();

assert(zkapp.reducer.getActions().length === 5);
assert(contract.reducer.getActions().data.get().length === 5);

// accumulate actions

Local.setProofsEnabled(true);
let accTx = await Mina.transaction(sender, () => zkapp.accumulate());
let accTx = await Mina.transaction(sender, () => contract.accumulate());
await accTx.prove();
await accTx.sign([sender.key]).send();

assert(zkapp.counter.get().toBigInt() === 32n);
assert(contract.counter.get().toBigInt() === 55n);
46 changes: 7 additions & 39 deletions src/examples/zkapps/reducer/actions-as-merkle-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,19 @@
* 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;
Expand All @@ -68,33 +43,26 @@ class MerkleListReducing extends SmartContract {

@method
async 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();

let merkleActionss = Provable.witness(MerkleActionss.provable, () =>
MerkleActionss.from(actionss.map((as) => MerkleActions.fromReverse(as)))
);
let actions = this.reducer.getActions();

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

// now our provable code to process the actions is very straight-forward
// our provable code to process actions in reverse order 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();
let merkleActions = actions.pop();

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

assert(hasAddress);
assert(actions.isEmpty()); // we processed all actions
assert(hasAddress); // we found the address
}
}

Expand Down Expand Up @@ -132,7 +100,7 @@ let dispatchTx = await Mina.transaction(sender, async () => {
await dispatchTx.prove();
await dispatchTx.sign([sender.key]).send();

assert(contract.reducer.getActions().length === 5);
assert(contract.reducer.getActions().data.get().length === 5);

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

Expand Down
9 changes: 3 additions & 6 deletions src/examples/zkapps/reducer/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class StorageContract extends SmartContract {

let keyHash = Poseidon.hash(key.toFields());

let { state: optionValue } = this.reducer.reduce(
let optionValue = this.reducer.reduce(
pendingActions,
Option,
(state, action) => {
Expand All @@ -73,11 +73,8 @@ class StorageContract extends SmartContract {
value: Provable.if(currentMatch, action.value, state.value),
};
},
{
state: Option.empty(),
actionState: Reducer.initialActionState,
},
{ maxTransactionsWithActions: k }
Option.empty(),
{ maxUpdatesWithActions: k }
);

return optionValue;
Expand Down
35 changes: 19 additions & 16 deletions src/examples/zkapps/reducer/reducer-composite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,31 +40,29 @@ class Counter extends SmartContract {

@method async rollupIncrements() {
// get previous counter & actions hash, assert that they're the same as on-chain values
let counter = this.counter.get();
this.counter.requireEquals(counter);
let actionState = this.actionState.get();
this.actionState.requireEquals(actionState);
let counter = this.counter.getAndRequireEquals();
let actionState = this.actionState.getAndRequireEquals();

// compute the new counter and hash from pending actions
let pendingActions = this.reducer.getActions({
fromActionState: actionState,
});

let { state: newCounter, actionState: newActionState } =
this.reducer.reduce(
pendingActions,
// state type
Field,
// function that says how to apply an action
(state: Field, action: MaybeIncrement) => {
return Provable.if(action.isIncrement, state.add(1), state);
},
{ state: counter, actionState }
);
let newCounter = this.reducer.reduce(
pendingActions,
// state type
Field,
// function that says how to apply an action
(state: Field, action: MaybeIncrement) => {
return Provable.if(action.isIncrement, state.add(1), state);
},
counter,
{ maxUpdatesWithActions: 10 }
);

// update on-chain state
this.counter.set(newCounter);
this.actionState.set(newActionState);
this.actionState.set(pendingActions.hash);
}
}

Expand All @@ -88,6 +86,11 @@ if (doProofs) {
await Counter.compile();
}

console.log(
'rows: ',
(await Counter.analyzeMethods())['rollupIncrements'].rows
);

console.log('deploy');
let tx = await Mina.transaction(feePayer, async () => {
AccountUpdate.fundNewAccount(feePayer);
Expand Down
6 changes: 3 additions & 3 deletions src/examples/zkapps/voting/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ await tx.sign([feePayer.key]).send();
*/
console.log(
'3 events?? ',
contracts.voterContract.reducer.getActions({}).length === 3
(await contracts.voterContract.reducer.fetchActions()).length === 3
);

/*
Expand Down Expand Up @@ -194,7 +194,7 @@ await tx.sign([feePayer.key]).send();
*/
console.log(
'2 events?? ',
contracts.candidateContract.reducer.getActions({}).length === 2
(await contracts.candidateContract.reducer.fetchActions()).length === 2
);

/*
Expand Down Expand Up @@ -270,7 +270,7 @@ vote(0n);

console.log(
'1 vote sequence event? ',
contracts.voting.reducer.getActions({}).length === 1
(await contracts.voting.reducer.fetchActions()).length === 1
);

/*
Expand Down
Loading

0 comments on commit cd428b6

Please sign in to comment.