Skip to content

Commit

Permalink
save progress, demo lightnet fetch error
Browse files Browse the repository at this point in the history
  • Loading branch information
hattyhattington17 committed Jan 13, 2025
1 parent 7e49a4e commit f4fbc4c
Show file tree
Hide file tree
Showing 9 changed files with 464 additions and 1 deletion.
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

0 comments on commit f4fbc4c

Please sign in to comment.