Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
210 changes: 210 additions & 0 deletions src/examples/zkapps/big-state-zkapp.ts
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!');
140 changes: 140 additions & 0 deletions src/lib/mina/v1/account-update-state.unit-test.ts
Copy link
Contributor

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-update as well?

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[] = [
Copy link
Member

Choose a reason for hiding this comment

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

while this checks if we can declare n state fields, it doesn't check that we actually properly allocate them becuse they are 0 by default anyway. we can do this in a different test but we should make sure that when we declare n state fields, we should also try to modify them

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree! you can also define a field array Provable.Array(Field, 32) and play around with the number of fields and state changes etc..

{
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);
});
});
}
});
Loading