-
Notifications
You must be signed in to change notification settings - Fork 142
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #296 from o1-labs/feature/composability-misc
zkApp composability, pt 2: multiple proofs
- Loading branch information
Showing
6 changed files
with
281 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
/** | ||
* This is just two zkapps mixed together in one file, with their respective interactions bundled | ||
* in the same transaction, to check that this actually works. | ||
* -) "simple zkapp", testing state updates + events + account preconditions + child parties | ||
* -) "counter rollup", testing state updates + sequence events / reducer | ||
*/ | ||
|
||
import { | ||
Field, | ||
state, | ||
State, | ||
method, | ||
UInt64, | ||
PrivateKey, | ||
SmartContract, | ||
Mina, | ||
Party, | ||
isReady, | ||
Permissions, | ||
DeployArgs, | ||
UInt32, | ||
Bool, | ||
PublicKey, | ||
Circuit, | ||
Experimental, | ||
} from 'snarkyjs'; | ||
|
||
const doProofs = true; | ||
|
||
await isReady; | ||
|
||
const INCREMENT = Field.one; | ||
|
||
let offchainStorage = { | ||
pendingActions: [] as Field[][], | ||
}; | ||
|
||
class CounterZkapp extends SmartContract { | ||
// the "reducer" field describes a type of action that we can dispatch, and reduce later | ||
reducer = Experimental.Reducer({ actionType: Field }); | ||
|
||
// on-chain version of our state. it will typically lag behind the | ||
// version that's implicitly represented by the list of actions | ||
@state(Field) counter = State<Field>(); | ||
// helper field to store the point in the action history that our on-chain state is at | ||
@state(Field) actionsHash = State<Field>(); | ||
|
||
deploy(args: DeployArgs) { | ||
super.deploy(args); | ||
this.setPermissions({ | ||
...Permissions.default(), | ||
editState: Permissions.proofOrSignature(), | ||
editSequenceState: Permissions.proofOrSignature(), | ||
}); | ||
this.actionsHash.set(Experimental.Reducer.initialActionsHash); | ||
} | ||
|
||
@method incrementCounter() { | ||
this.reducer.dispatch(INCREMENT); | ||
} | ||
|
||
@method rollupIncrements() { | ||
// get previous counter & actions hash, assert that they're the same as on-chain values | ||
let counter = this.counter.get(); | ||
this.counter.assertEquals(counter); | ||
let actionsHash = this.actionsHash.get(); | ||
this.actionsHash.assertEquals(actionsHash); | ||
|
||
// compute the new counter and hash from pending actions | ||
// remark: it's not feasible to pass in the pending actions as method arguments, because they have dynamic size | ||
let { state: newCounter, actionsHash: newActionsHash } = | ||
this.reducer.reduce( | ||
offchainStorage.pendingActions, | ||
// state type | ||
Field, | ||
// function that says how to apply an action | ||
(state: Field, _action: Field) => { | ||
return state.add(1); | ||
}, | ||
{ state: counter, actionsHash } | ||
); | ||
|
||
// update on-chain state | ||
this.counter.set(newCounter); | ||
this.actionsHash.set(newActionsHash); | ||
} | ||
} | ||
|
||
class SimpleZkapp extends SmartContract { | ||
@state(Field) x = State<Field>(); | ||
|
||
events = { | ||
update: Field, | ||
payout: UInt64, | ||
payoutReceiver: PublicKey, | ||
}; | ||
|
||
deploy(args: DeployArgs) { | ||
super.deploy(args); | ||
this.setPermissions({ | ||
...Permissions.default(), | ||
editState: Permissions.proofOrSignature(), | ||
send: Permissions.proofOrSignature(), | ||
}); | ||
this.balance.addInPlace(UInt64.fromNumber(initialBalance)); | ||
this.x.set(initialState); | ||
} | ||
|
||
@method update(y: Field) { | ||
this.emitEvent('update', y); | ||
let x = this.x.get(); | ||
this.x.assertEquals(x); | ||
this.x.set(x.add(y)); | ||
} | ||
|
||
/** | ||
* This method allows a certain privileged account to claim half of the zkapp balance, but only once | ||
* @param caller the privileged account | ||
*/ | ||
@method payout(caller: PrivateKey) { | ||
// check that caller is the privileged account | ||
let callerAddress = caller.toPublicKey(); | ||
callerAddress.assertEquals(privilegedAddress); | ||
|
||
// assert that the caller nonce is 0, and increment the nonce - this way, payout can only happen once | ||
let callerParty = Experimental.createChildParty(this.self, callerAddress); | ||
callerParty.account.nonce.assertEquals(UInt32.zero); | ||
callerParty.body.incrementNonce = Bool(true); | ||
|
||
// pay out half of the zkapp balance to the caller | ||
let balance = this.account.balance.get(); | ||
this.account.balance.assertEquals(balance); | ||
// FIXME UInt64.div() doesn't work on variables | ||
let halfBalance = Circuit.witness(UInt64, () => | ||
balance.toConstant().div(2) | ||
); | ||
this.balance.subInPlace(halfBalance); | ||
callerParty.balance.addInPlace(halfBalance); | ||
|
||
// emit some events | ||
this.emitEvent('payoutReceiver', callerAddress); | ||
this.emitEvent('payout', halfBalance); | ||
} | ||
} | ||
|
||
let Local = Mina.LocalBlockchain(); | ||
Mina.setActiveInstance(Local); | ||
|
||
// a test account that pays all the fees, and puts additional funds into the zkapp | ||
let feePayer = Local.testAccounts[0].privateKey; | ||
|
||
// the zkapp account | ||
let zkappKey = PrivateKey.random(); | ||
let zkappAddress = zkappKey.toPublicKey(); | ||
|
||
// a special account that is allowed to pull out half of the zkapp balance, once | ||
let privilegedKey = Local.testAccounts[1].privateKey; | ||
let privilegedAddress = privilegedKey.toPublicKey(); | ||
|
||
let initialBalance = 10_000_000_000; | ||
let initialState = Field(1); | ||
let zkapp = new SimpleZkapp(zkappAddress); | ||
|
||
let counterZkappKey = PrivateKey.random(); | ||
let counterZkappAddress = counterZkappKey.toPublicKey(); | ||
let counterZkapp = new CounterZkapp(counterZkappAddress); | ||
|
||
if (doProofs) { | ||
console.log('compile'); | ||
await SimpleZkapp.compile(zkappAddress); | ||
await CounterZkapp.compile(counterZkappAddress); | ||
} | ||
|
||
console.log('deploy'); | ||
let tx = await Mina.transaction(feePayer, () => { | ||
Party.fundNewAccount(feePayer, { | ||
initialBalance: Mina.accountCreationFee().add(initialBalance), | ||
}); | ||
zkapp.deploy({ zkappKey }); | ||
counterZkapp.deploy({ zkappKey: counterZkappKey }); | ||
}); | ||
tx.send(); | ||
|
||
console.log('initial state: ' + zkapp.x.get()); | ||
console.log(`initial balance: ${zkapp.account.balance.get().div(1e9)} MINA`); | ||
|
||
console.log('update & dispatch increment'); | ||
tx = await Mina.transaction(feePayer, () => { | ||
zkapp.update(Field(3)); | ||
counterZkapp.incrementCounter(); | ||
if (!doProofs) { | ||
zkapp.sign(zkappKey); | ||
counterZkapp.sign(counterZkappKey); | ||
} | ||
}); | ||
if (doProofs) await tx.prove(); | ||
tx.send(); | ||
offchainStorage.pendingActions.push([INCREMENT]); | ||
console.log('state (on-chain): ' + counterZkapp.counter.get()); | ||
console.log('pending actions:', JSON.stringify(offchainStorage.pendingActions)); | ||
|
||
console.log('payout & rollup'); | ||
tx = await Mina.transaction(feePayer, () => { | ||
zkapp.payout(privilegedKey); | ||
counterZkapp.rollupIncrements(); | ||
if (!doProofs) { | ||
zkapp.sign(zkappKey); | ||
counterZkapp.sign(counterZkappKey); | ||
} | ||
}); | ||
if (doProofs) await tx.prove(); | ||
console.log(tx.toJSON()); | ||
tx.send(); | ||
offchainStorage.pendingActions = []; | ||
|
||
console.log('final state: ' + zkapp.x.get()); | ||
console.log(`final balance: ${zkapp.account.balance.get().div(1e9)} MINA`); | ||
console.log('state (on-chain): ' + counterZkapp.counter.get()); | ||
console.log('pending actions:', JSON.stringify(offchainStorage.pendingActions)); | ||
|
||
console.log('try to payout a second time..'); | ||
tx = await Mina.transaction(feePayer, () => { | ||
zkapp.payout(privilegedKey); | ||
if (!doProofs) zkapp.sign(zkappKey); | ||
}); | ||
try { | ||
if (doProofs) await tx.prove(); | ||
tx.send(); | ||
} catch (err: any) { | ||
console.log('Transaction failed with error', err.message); | ||
} | ||
|
||
console.log('try to payout to a different account..'); | ||
try { | ||
tx = await Mina.transaction(feePayer, () => { | ||
zkapp.payout(Local.testAccounts[2].privateKey); | ||
if (!doProofs) zkapp.sign(zkappKey); | ||
}); | ||
if (doProofs) await tx.prove(); | ||
tx.send(); | ||
} catch (err: any) { | ||
console.log('Transaction failed with error', err.message); | ||
} | ||
|
||
console.log( | ||
`should still be the same final balance: ${zkapp.account.balance | ||
.get() | ||
.div(1e9)} MINA` | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.