-
Notifications
You must be signed in to change notification settings - Fork 172
enhancement: account state unit tests #2693
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| /** | ||
| * Example zkApp demonstrating maximum on-chain state usage (32 field elements) | ||
| * using composite data structures with Struct. | ||
| */ | ||
| import { | ||
| AccountUpdate, | ||
| Field, | ||
| Mina, | ||
| Provable, | ||
| SmartContract, | ||
| State, | ||
| Struct, | ||
| method, | ||
| state, | ||
| } from 'o1js'; | ||
|
|
||
| // composite structure representing a 2D point (2 field elements) | ||
| class Point extends Struct({ | ||
| x: Field, | ||
| y: Field, | ||
| }) { | ||
| static zero() { | ||
| return new Point({ x: Field(0), y: Field(0) }); | ||
| } | ||
|
|
||
| add(other: Point): Point { | ||
| return new Point({ | ||
| x: this.x.add(other.x), | ||
| y: this.y.add(other.y), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // larger composite structure representing game state (8 field elements) | ||
| class GameState extends Struct({ | ||
| score: Field, | ||
| level: Field, | ||
| health: Field, | ||
| mana: Field, | ||
| position: Point, // 2 fields | ||
| velocity: Point, // 2 fields | ||
| }) { | ||
| static initial() { | ||
| return new GameState({ | ||
| score: Field(0), | ||
| level: Field(1), | ||
| health: Field(100), | ||
| mana: Field(50), | ||
| position: Point.zero(), | ||
| velocity: Point.zero(), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // struct for storing multiple field elements | ||
| class DataBlock extends Struct({ | ||
| values: Provable.Array(Field, 8), | ||
| }) { | ||
| static empty() { | ||
| return new DataBlock({ values: Array(8).fill(Field(0)) }); | ||
| } | ||
|
|
||
| static fromSeed(seed: Field) { | ||
| const values = []; | ||
| let current = seed; | ||
| for (let i = 0; i < 8; i++) { | ||
| values.push(current); | ||
| current = current.add(Field(1)); | ||
| } | ||
| return new DataBlock({ values }); | ||
| } | ||
| } | ||
|
|
||
| class BigStateZkapp extends SmartContract { | ||
| @state(GameState) gameState = State<GameState>(); | ||
| @state(DataBlock) dataBlock1 = State<DataBlock>(); | ||
| @state(DataBlock) dataBlock2 = State<DataBlock>(); | ||
| @state(DataBlock) dataBlock3 = State<DataBlock>(); | ||
|
|
||
| @method async initializeState() { | ||
| this.gameState.set(GameState.initial()); | ||
| this.dataBlock1.set(DataBlock.empty()); | ||
| this.dataBlock2.set(DataBlock.empty()); | ||
| this.dataBlock3.set(DataBlock.empty()); | ||
| } | ||
|
|
||
| @method async updateGameState(newPosition: Point, newVelocity: Point) { | ||
| const current = this.gameState.getAndRequireEquals(); | ||
|
|
||
| const updated = new GameState({ | ||
| score: current.score.add(Field(10)), | ||
| level: current.level, | ||
| health: current.health, | ||
| mana: current.mana, | ||
| position: current.position.add(newPosition), | ||
| velocity: newVelocity, | ||
| }); | ||
|
|
||
| this.gameState.set(updated); | ||
| } | ||
|
|
||
| @method async setDataBlocks(seed1: Field, seed2: Field, seed3: Field) { | ||
| this.dataBlock1.set(DataBlock.fromSeed(seed1)); | ||
| this.dataBlock2.set(DataBlock.fromSeed(seed2)); | ||
| this.dataBlock3.set(DataBlock.fromSeed(seed3)); | ||
| } | ||
|
|
||
| @method async levelUp() { | ||
| const current = this.gameState.getAndRequireEquals(); | ||
|
|
||
| const updated = new GameState({ | ||
| score: current.score, | ||
| level: current.level.add(Field(1)), | ||
| health: Field(100), | ||
| mana: Field(50), | ||
| position: current.position, | ||
| velocity: current.velocity, | ||
| }); | ||
|
|
||
| this.gameState.set(updated); | ||
| } | ||
| } | ||
| const doProofs = true; | ||
|
|
||
| console.log('BigStateZkapp Example - using 32 on-chain state fields\n'); | ||
|
|
||
| let Local = await Mina.LocalBlockchain({ proofsEnabled: doProofs }); | ||
| Mina.setActiveInstance(Local); | ||
|
|
||
| const [sender] = Local.testAccounts; | ||
| const zkappAccount = Mina.TestPublicKey.random(); | ||
| const zkapp = new BigStateZkapp(zkappAccount); | ||
|
|
||
| if (doProofs) { | ||
| console.log('Compiling...'); | ||
| console.time('compile'); | ||
| await BigStateZkapp.compile(); | ||
| console.timeEnd('compile'); | ||
| } | ||
|
|
||
| console.log('\nDeploying zkApp...'); | ||
| let tx = await Mina.transaction(sender, async () => { | ||
| AccountUpdate.fundNewAccount(sender); | ||
| await zkapp.deploy(); | ||
| }); | ||
| await tx.prove(); | ||
| await tx.sign([sender.key, zkappAccount.key]).send(); | ||
|
|
||
| console.log('Initializing state...'); | ||
| tx = await Mina.transaction(sender, async () => { | ||
| await zkapp.initializeState(); | ||
| }); | ||
| await tx.prove(); | ||
| await tx.sign([sender.key]).send(); | ||
|
|
||
| // read initial state | ||
| let gameState = zkapp.gameState.get(); | ||
| console.log('\nInitial game state:'); | ||
| console.log(` Score: ${gameState.score}`); | ||
| console.log(` Level: ${gameState.level}`); | ||
| console.log(` Health: ${gameState.health}`); | ||
| console.log(` Position: (${gameState.position.x}, ${gameState.position.y})`); | ||
|
|
||
| console.log('\nUpdating game state...'); | ||
| tx = await Mina.transaction(sender, async () => { | ||
| await zkapp.updateGameState( | ||
| new Point({ x: Field(10), y: Field(20) }), | ||
| new Point({ x: Field(1), y: Field(2) }) | ||
| ); | ||
| }); | ||
| await tx.prove(); | ||
| await tx.sign([sender.key]).send(); | ||
|
|
||
| gameState = zkapp.gameState.get(); | ||
| console.log('Updated game state:'); | ||
| console.log(` Score: ${gameState.score}`); | ||
| console.log(` Position: (${gameState.position.x}, ${gameState.position.y})`); | ||
| console.log(` Velocity: (${gameState.velocity.x}, ${gameState.velocity.y})`); | ||
|
|
||
| console.log('\nSetting data blocks with seeds...'); | ||
| tx = await Mina.transaction(sender, async () => { | ||
| await zkapp.setDataBlocks(Field(100), Field(200), Field(300)); | ||
| }); | ||
| await tx.prove(); | ||
| await tx.sign([sender.key]).send(); | ||
|
|
||
| const dataBlock1 = zkapp.dataBlock1.get(); | ||
| console.log(`DataBlock1 values: [${dataBlock1.values.join(', ')}]`); | ||
|
|
||
| console.log('\nLeveling up...'); | ||
| tx = await Mina.transaction(sender, async () => { | ||
| await zkapp.levelUp(); | ||
| }); | ||
| await tx.prove(); | ||
| await tx.sign([sender.key]).send(); | ||
|
|
||
| gameState = zkapp.gameState.get(); | ||
| console.log('After level up:'); | ||
| console.log(` Level: ${gameState.level}`); | ||
| console.log(` Health: ${gameState.health} (restored)`); | ||
| console.log(` Mana: ${gameState.mana} (restored)`); | ||
|
|
||
| // verify all 32 state fields are used | ||
| const account = Mina.getAccount(zkappAccount); | ||
| console.log('\nOn-chain state (all 32 fields):'); | ||
| account.zkapp!.appState.forEach((field, i) => { | ||
| console.log(` appState[${i}]: ${field}`); | ||
| }); | ||
|
|
||
| console.log('\nBigStateZkapp example completed successfully!'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import assert from 'node:assert'; | ||
| import { describe, it } from 'node:test'; | ||
| import { | ||
| AccountUpdate, | ||
| Field, | ||
| Mina, | ||
| PublicKey, | ||
| SmartContract, | ||
| State, | ||
| declareState, | ||
| method, | ||
| } from 'o1js'; | ||
|
|
||
| type TestSpec = { | ||
| nAppStateFields: number; | ||
| expectsToReject?: boolean; | ||
| }; | ||
|
|
||
| function specToString(t: TestSpec) { | ||
| return `(fields: ${t.nAppStateFields})`; | ||
| } | ||
|
|
||
| async function runTest(t: TestSpec, feePayer: Mina.TestPublicKey) { | ||
| const nFields = t.nAppStateFields; | ||
|
|
||
| class Contract extends SmartContract { | ||
| constructor(address: PublicKey) { | ||
| super(address); | ||
| // init State() for each field in constructor | ||
| for (let idx = 0; idx < nFields; idx++) { | ||
| (this as any)[`field${idx}`] = State<Field>(); | ||
| } | ||
| } | ||
|
|
||
| @method async updateAllFields() { | ||
| for (let idx = 0; idx < nFields; idx++) { | ||
| const state = (this as any)[`field${idx}`] as State<Field>; | ||
| // set each field to idx + 1 (non-zero values) | ||
| state.set(Field(idx + 1)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // dynamically declare state fields | ||
| const entries = []; | ||
| for (let idx = 0; idx < t.nAppStateFields; idx++) { | ||
| entries.push([`field${idx}`, Field]); | ||
| } | ||
| const fields = Object.fromEntries(entries); | ||
| declareState(Contract, fields); | ||
|
|
||
| const contractAccount = Mina.TestPublicKey.random(); | ||
| const contract = new Contract(contractAccount); | ||
|
|
||
| await Contract.compile(); | ||
|
|
||
| { | ||
| const tx = await Mina.transaction(feePayer, async () => { | ||
| AccountUpdate.fundNewAccount(feePayer); | ||
| await contract.deploy(); | ||
| }); | ||
| await tx.sign([feePayer.key, contractAccount.key]).send(); | ||
| } | ||
|
|
||
| // update all state fields with non-zero values | ||
| { | ||
| const tx = await Mina.transaction(feePayer, async () => { | ||
| await contract.updateAllFields(); | ||
| }); | ||
| await tx.prove(); | ||
| await tx.sign([feePayer.key]).send(); | ||
| } | ||
|
|
||
| // verify all state fields have the expected values | ||
| const account = Mina.getAccount(contractAccount); | ||
| for (let idx = 0; idx < t.nAppStateFields; idx++) { | ||
| const expectedValue = Field(idx + 1); | ||
| const actualValue = account.zkapp!.appState[idx]; | ||
| assert.deepStrictEqual( | ||
| actualValue.toBigInt(), | ||
| expectedValue.toBigInt(), | ||
| `field${idx} should be ${idx + 1}` | ||
| ); | ||
| } | ||
|
|
||
| return contract; | ||
| } | ||
|
|
||
| await describe('app state updates', async () => { | ||
| let Local = await Mina.LocalBlockchain({ proofsEnabled: true }); | ||
| Mina.setActiveInstance(Local); | ||
| const [feePayer] = Local.testAccounts; | ||
|
|
||
| const tests: TestSpec[] = [ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. while this checks if we can declare
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree! you can also define a field array |
||
| { | ||
| nAppStateFields: 1, | ||
| }, | ||
| { | ||
| nAppStateFields: 8, | ||
| }, | ||
| { | ||
| nAppStateFields: 32, | ||
| }, | ||
| { | ||
| nAppStateFields: 33, | ||
| expectsToReject: true, | ||
| }, | ||
| { | ||
| nAppStateFields: 64, | ||
| expectsToReject: true, | ||
| }, | ||
| ]; | ||
|
|
||
| for (const test of tests) { | ||
| await it(`should succeed with spec: ${specToString(test)}`, async () => { | ||
| if (test.expectsToReject) { | ||
| await assert.rejects(async () => { | ||
| await runTest(test, feePayer); | ||
| }, 'the contract should not deploy properly'); | ||
| } else { | ||
| await assert.doesNotReject(async () => { | ||
| await runTest(test, feePayer); | ||
| }, 'the contract should deploy properly'); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| const rejects: TestSpec[] = [ | ||
| { | ||
| nAppStateFields: 33, | ||
| }, | ||
| ]; | ||
| for (const test of rejects) { | ||
| await it(`should reject with spec: ${specToString(test)}`, async () => { | ||
| await assert.rejects(async () => { | ||
| await runTest(test, feePayer); | ||
| }); | ||
| }); | ||
| } | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does the title mention
account-updateas well?