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

Expose method to fetch internal state on an OffchainStateInstance #1953

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Method for optional types to assert none https://github.com/o1-labs/o1js/pull/1922
- Increased maximum supported amount of methods in a `SmartContract` or `ZkProgram` to 30. https://github.com/o1-labs/o1js/pull/1918
- Expose low-level conversion methods `Proof.{_proofToBase64,_proofFromBase64}` https://github.com/o1-labs/o1js/pull/1928
- Expore `maxProofsVerified()` and a `Proof` class directly on ZkPrograms https://github.com/o1-labs/o1js/pull/1933
- Add `fetchInternalState` method on `OffchainStateInstance` which fetches settled actions from the archive node and rebuilds the internal state trees. https://github.com/o1-labs/o1js/pull/1953
- Expose `maxProofsVerified()` and a `Proof` class directly on ZkPrograms https://github.com/o1-labs/o1js/pull/1933

### Changed

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
"node": ">=18.14.0"
},
"scripts": {
"runner": "npm run build && node /Users/hattington/code/o1labs/o1js/dist/node/lib/mina/actions/offchain-contract-tests/multi-thread-lightnet.js",
"demoSettle": "npm run build && node dist/node/lib/mina/actions/offchain-contract-tests/actionStateErrorDemo.js",
"demoFetch": "npm run build && node dist/node/lib/mina/actions/offchain-contract-tests/actionStateErrorDemo.js",

"dev": "npx tsc -p tsconfig.test.json && node src/build/copy-to-dist.js",
"build": "node src/build/copy-artifacts.js && rimraf ./dist/node && npm run dev && node src/build/build-node.js",
"build:bindings": "./src/bindings/scripts/build-o1js-node.sh",
Expand Down
9 changes: 9 additions & 0 deletions run-ci-live-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ DEX_PROC=$!
FETCH_PROC=$!
./run src/tests/transaction-flow.ts --bundle | add_prefix "TRANSACTION_FLOW" &
TRANSACTION_FLOW_PROC=$!
./run src/lib/mina/actions/offchain-contract-tests/multi-thread-lightnet.ts --bundle | add_prefix "OFFCHAIN_STATE_PROC" &
OFFCHAIN_STATE_PROC=$!

# Wait for each process and capture their exit statuses
FAILURE=0
Expand Down Expand Up @@ -61,6 +63,13 @@ if [ $? -ne 0 ]; then
echo ""
FAILURE=1
fi
wait $OFFCHAIN_STATE_PROC
if [ $? -ne 0 ]; then
echo ""
echo "OFFCHAIN_STATE test failed."
echo ""
FAILURE=1
fi

# Exit with failure if any process failed
if [ $FAILURE -ne 0 ]; then
Expand Down
41 changes: 41 additions & 0 deletions src/lib/mina/actions/offchain-contract-tests/LilyPadContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// offchain state object, this is where the data is actually exposed
import {
AccountUpdate,
Experimental,
fetchAccount,
Lightnet,
method,
Mina,
PrivateKey,
PublicKey,
SmartContract,
state,
} from 'o1js';
import OffchainState = Experimental.OffchainState;

export const LilypadState = OffchainState(
{ currentOccupant: OffchainState.Field(PublicKey) },
{ logTotalCapacity: 4, maxActionsPerUpdate: 2, maxActionsPerProof: 2 }
);
export class LilyPadStateProof extends LilypadState.Proof {}

export class OffchainStorageLilyPad extends SmartContract {
@state(Experimental.OffchainState.Commitments)
offchainStateCommitments = LilypadState.emptyCommitments();
offchainState = LilypadState.init(this);

@method
async visit() {
const senderPublicKey = this.sender.getAndRequireSignature();
const currentOccupantOption = await this.offchainState.fields.currentOccupant.get();
this.offchainState.fields.currentOccupant.update({
from: currentOccupantOption,
to: senderPublicKey,
});
}

@method
async settle(proof: LilyPadStateProof) {
await this.offchainState.settle(proof);
}
}
135 changes: 135 additions & 0 deletions src/lib/mina/actions/offchain-contract-tests/actionStateErrorDemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// offchain state object, this is where the data is actually exposed
import {
AccountUpdate,
fetchAccount,
Lightnet,
Mina,
PrivateKey,
PublicKey,
} from 'o1js';
import {LilypadState, OffchainStorageLilyPad} from "./LilyPadContract.js";

/**
* Set this address before running the fetch state process
* */
let deployedZkAppAddress = '';

// configure lightnet and retrieve keys
Mina.setActiveInstance(
Mina.Network({
mina: 'http://127.0.0.1:8080/graphql',
archive: 'http://127.0.0.1:8282',
lightnetAccountManager: 'http://127.0.0.1:8181',
})
);
const senderPrivateKey = (await Lightnet.acquireKeyPair()).privateKey;

// compile zkprograms
await LilypadState.compile();
await OffchainStorageLilyPad.compile();

let zkApp: OffchainStorageLilyPad;
if (!deployedZkAppAddress) {
// deploy OffchainStorageLilyPad if it is not already deployed
const zkAppPrivateKey: PrivateKey = PrivateKey.random();
zkApp = new OffchainStorageLilyPad(zkAppPrivateKey.toPublicKey());
const deployTx = await Mina.transaction(
{ fee: 1e9, sender: senderPrivateKey.toPublicKey() },
async () => {
AccountUpdate.fundNewAccount(senderPrivateKey.toPublicKey());
await zkApp.deploy();
}
);
await deployTx.prove();
const deployTxPromise = await deployTx
.sign([senderPrivateKey, zkAppPrivateKey])
.send();
await deployTxPromise.wait();
console.log(
`Deployed OffchainStorageLilyPad to address ${zkAppPrivateKey
.toPublicKey()
.toBase58()}`
);
} else {
// init OffchainStorageLilyPad at deployedZkAppAddress
zkApp = new OffchainStorageLilyPad(
PublicKey.fromBase58(deployedZkAppAddress)
);
await fetchAccount({ publicKey: zkApp.address });
console.log(
`Interacting with deployed OffchainStorageLilyPad at address ${deployedZkAppAddress}`
);
}

zkApp.offchainState.setContractInstance(zkApp);
console.log(
'fetchAccount',
(
await fetchAccount({ publicKey: zkApp.address })
)?.account?.publicKey.toBase58()
);
console.log('OffchainStorageLilyPad starting state:');
await logAppState();
// stop settle process here, copy address from logs into deployedZkAppAddress, trigger fetch process and allow to run up to this point
// after fetch process hits this point, execute settle process to run state updates and settlement
// after state updates and settlement, execute fetch process
await zkApp.offchainState.fetchInternalState();
await logAppState();

// call visit on the contract which will dispatch state updates but will not directly update the OffchainStateInstance
await (await visit(senderPrivateKey)).wait();
console.log('Executed visits, app state should be unchanged: ');

// Create a settlement proof
console.log('\nSettling visits on chain');
const settlementProof = await zkApp.offchainState.createSettlementProof();
// await logAppState(); // todo: logging the state here gives a root mismatch error because the internal state map is updated by createSettlementProof before the on chain value is changed
const settleTx = await Mina.transaction(
{ fee: 1e9, sender: senderPrivateKey.toPublicKey() },
async () => {
await zkApp.settle(settlementProof);
}
);
await settleTx.prove();
const settleTxPromise = await settleTx.sign([senderPrivateKey]).send();
await settleTxPromise.wait();

console.log(
'Executed OffchainStorageLilyPad.settle(), on chain state has been updated with the effects of the dispatched visits: '
);

// must call fetchAccount after executing settle transaction in order to retrieve the most up to date on chain commitments
await logAppState();


/**************************************************************************
* Helpers
***************************************************************************/
async function visit(sender: PrivateKey) {
const tx = await Mina.transaction(
{ fee: 1e9, sender: senderPrivateKey.toPublicKey() },
async () => {
await zkApp.visit();
}
);
await tx.prove();
const txPromise = await tx.sign([sender]).send();

console.log(
`${sender.toPublicKey().toBase58()} called OffchainStorageLilyPad.visit()`
);
return txPromise;
}

async function logAppState() {
await fetchAccount({ publicKey: zkApp.address });
const onchainStateCommitment = zkApp.offchainStateCommitments.get();

console.log(
`${process.pid} onchainStateCommitment.root=${onchainStateCommitment.root.toString()} `
);

const currentOccupant =
await zkApp.offchainState.fields.currentOccupant.get();
console.log(`currentOccupant: ${currentOccupant.value.toBase58()}`);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Message } from './multi-thread-lightnet.js';
import { fetchAccount, Mina, PublicKey } from 'o1js';
import {LilypadState, OffchainStorageLilyPad} from "./LilyPadContract.js";

process.send?.(`Starting fetch state process: ${process.pid}`);
/**
* Configure lightnet and retrieve signing keys
* */
const network = Mina.Network({
mina: 'http://localhost:8080/graphql',
archive: 'http://127.0.0.1:8282',
lightnetAccountManager: 'http://localhost:8181',
});
Mina.setActiveInstance(network);
let zkApp: OffchainStorageLilyPad;

await LilypadState.compile();
await OffchainStorageLilyPad.compile();
// notify main process that this process is ready to begin work
process.send?.({ type: 'READY' });

process.on('message', async (msg: Message) => {
console.log(
`Fetch state process received message from root: ${JSON.stringify(msg)}`
);

/**
* Compile offchain state zkprogram and contract, deploy contract
* */
if (msg.type === 'DEPLOYED') {
// account = PublicKey.fromBase58(msg.account);
zkApp = new OffchainStorageLilyPad(PublicKey.fromBase58(msg.address));
zkApp.offchainState.setContractInstance(zkApp);
await logState();
process.send?.({ type: 'INSTANTIATED' });
} else if (msg.type === 'FETCH_STATE') {
// todo: expect root mismatch
await logState();

// todo: this is erroring on the other test
await zkApp.offchainState.fetchInternalState();
await logState();
}
});

async function logState() {
await fetchAccount({ publicKey: zkApp.address });
const root = zkApp.offchainStateCommitments.get().root.toString();
const currentOccupant = (
await zkApp.offchainState.fields.currentOccupant.get()
).value.toString();
const message = `offchainState: (currentOccupant => ${currentOccupant}),(root => ${root})`;
process.send?.(message);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {fork} from 'child_process';

/**
* Multi process offchain state synchronization test
*
* Launch two processes - a settling process and a fetch state process
* Settling process deploys the contract
* Offchain state process instantiates the contract at the deployed address
* Settling process updates the state and calls settle
* Offchain state process reads the state and gets a root mismatch error
* Offchain state process calls fetchInternalState, reads the state, and sees the updated state
* */

export type Message =
| { type: 'READY' }
| { type: 'DEPLOY' }
| { type: 'DEPLOYED'; address: string; account: string }
| { type: 'INSTANTIATED' }
| { type: 'UPDATE_STATE' }
| { type: 'STATE_UPDATED' }
| { type: 'FETCH_STATE' };

let settlingReady = false;
let fetchStateReady = false;

// Launch the processes
const settlingProcess = fork(
'dist/node/lib/mina/actions/offchain-contract-tests/settling-process.js'
);
const fetchStateProcess = fork(
'dist/node/lib/mina/actions/offchain-contract-tests/fetch-state-process.js'
);

// Listen for messages from the child processes
settlingProcess.on('message', (msg: Message) => {
console.log(
`Settling process dispatched message to root: ${JSON.stringify(msg)}`
);

if (msg.type === 'READY') {
settlingReady = true;
// both processes are ready, dispatch DEPLOY to settling process to deploy contract
if (settlingReady && fetchStateReady) {
console.log('Both processes are ready. Starting the test...');
settlingProcess.send({ type: 'DEPLOY' });
}
} else if (msg.type === 'DEPLOYED') {
// settling process finished deploying contract, tell fetchState process to instantiate the contract and offchain state
fetchStateProcess.send({
type: 'DEPLOYED',
address: msg.address,
account: msg.account,
});
} else if (msg.type === 'STATE_UPDATED') {
// settle state process has updated and settled state, tell fetch state process to try to retrieve the new state which lets us test synchronization
fetchStateProcess.send({ type: 'FETCH_STATE' });
}
});
fetchStateProcess.on('message', (msg: Message) => {
console.log(
`Fetch state process dispatched message to root: ${JSON.stringify(msg)}`
);

if (msg.type === 'READY') {
fetchStateReady = true;
// both processes are ready, dispatch DEPLOY to settling process to deploy contract
if (settlingReady && fetchStateReady) {
console.log('Both processes are ready. Starting the test...');
settlingProcess.send({ type: 'DEPLOY' });
}
} else if (msg.type === 'INSTANTIATED') {
// fetch state process instantiated contract, tell settle state process to update the state and settle it
settlingProcess.send({ type: 'UPDATE_STATE' });
}
});

function cleanup() {
settlingProcess.kill();
fetchStateProcess.kill();
console.log('Child processes terminated.');
}

function handleProcessEvents(processName: string, processInstance: any) {
processInstance.on('error', (err: Error) => {
console.error(`${processName} threw an error: ${err.message}`);
});

processInstance.on('exit', (code: number) => {
if (code !== 0) {
console.error(`${processName} exited with code ${code}`);
} else {
console.log(`${processName} exited successfully.`);
}
});
}

handleProcessEvents('Settling process', settlingProcess);
handleProcessEvents('Fetch state process', fetchStateProcess);
Loading
Loading