Skip to content

Commit f5a3f1d

Browse files
authored
Merge pull request o1-labs#1834 from o1-labs/2024-09-refactor-offchain-state
Refactor offchain state
2 parents 97e5130 + ef3f7f8 commit f5a3f1d

8 files changed

+709
-411
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1717

1818
## [Unreleased](https://github.com/o1-labs/o1js/compare/f15293a69...HEAD)
1919

20+
### Fixes
21+
22+
- Decouple offchain state instances from their definitions https://github.com/o1-labs/o1js/pull/1834
23+
2024
## [1.9.0](https://github.com/o1-labs/o1js/compare/450943...f15293a69) - 2024-10-15
2125

2226
### Added
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {
2+
SmartContract,
3+
method,
4+
state,
5+
PublicKey,
6+
UInt64,
7+
Experimental,
8+
} from '../../../../index.js';
9+
10+
export { offchainState, StateProof, ExampleContract };
11+
12+
const { OffchainState } = Experimental;
13+
14+
const offchainState = OffchainState(
15+
{
16+
accounts: OffchainState.Map(PublicKey, UInt64),
17+
totalSupply: OffchainState.Field(UInt64),
18+
},
19+
{ logTotalCapacity: 10, maxActionsPerProof: 5 }
20+
);
21+
22+
class StateProof extends offchainState.Proof {}
23+
24+
// example contract that interacts with offchain state
25+
class ExampleContract extends SmartContract {
26+
@state(OffchainState.Commitments) offchainStateCommitments =
27+
offchainState.emptyCommitments();
28+
29+
// o1js memoizes the offchain state by contract address so that this pattern works
30+
offchainState: any = offchainState.init(this);
31+
32+
@method
33+
async createAccount(address: PublicKey, amountToMint: UInt64) {
34+
// setting `from` to `undefined` means that the account must not exist yet
35+
this.offchainState.fields.accounts.update(address, {
36+
from: undefined,
37+
to: amountToMint,
38+
});
39+
40+
// TODO using `update()` on the total supply means that this method
41+
// can only be called once every settling cycle
42+
let totalSupplyOption = await this.offchainState.fields.totalSupply.get();
43+
let totalSupply = totalSupplyOption.orElse(0n);
44+
45+
this.offchainState.fields.totalSupply.update({
46+
from: totalSupplyOption,
47+
to: totalSupply.add(amountToMint),
48+
});
49+
}
50+
51+
@method
52+
async transfer(from: PublicKey, to: PublicKey, amount: UInt64) {
53+
let fromOption = await this.offchainState.fields.accounts.get(from);
54+
let fromBalance = fromOption.assertSome('sender account exists');
55+
56+
let toOption = await this.offchainState.fields.accounts.get(to);
57+
let toBalance = toOption.orElse(0n);
58+
59+
/**
60+
* Update both accounts atomically.
61+
*
62+
* This is safe, because both updates will only be accepted if both previous balances are still correct.
63+
*/
64+
this.offchainState.fields.accounts.update(from, {
65+
from: fromOption,
66+
to: fromBalance.sub(amount),
67+
});
68+
69+
this.offchainState.fields.accounts.update(to, {
70+
from: toOption,
71+
to: toBalance.add(amount),
72+
});
73+
}
74+
75+
@method.returns(UInt64)
76+
async getSupply() {
77+
return (await this.offchainState.fields.totalSupply.get()).orElse(0n);
78+
}
79+
80+
@method.returns(UInt64)
81+
async getBalance(address: PublicKey) {
82+
return (await this.offchainState.fields.accounts.get(address)).orElse(0n);
83+
}
84+
85+
@method
86+
async settle(proof: StateProof) {
87+
await this.offchainState.settle(proof);
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import {
2+
SmartContract,
3+
method,
4+
state,
5+
PublicKey,
6+
UInt64,
7+
} from '../../../../index.js';
8+
import * as Mina from '../../mina.js';
9+
import assert from 'assert';
10+
import { ExampleContract } from './ExampleContract.js';
11+
import { settle, transfer } from './utils.js';
12+
13+
const Local = await Mina.LocalBlockchain({ proofsEnabled: false });
14+
Mina.setActiveInstance(Local);
15+
16+
const [
17+
sender,
18+
receiver1,
19+
receiver2,
20+
receiver3,
21+
contractAccountA,
22+
contractAccountB,
23+
] = Local.testAccounts;
24+
25+
const contractA = new ExampleContract(contractAccountA);
26+
const contractB = new ExampleContract(contractAccountB);
27+
contractA.offchainState.setContractInstance(contractA);
28+
contractB.offchainState.setContractInstance(contractB);
29+
30+
console.time('deploy contract');
31+
const deployTx = Mina.transaction(sender, async () => {
32+
await contractA.deploy();
33+
await contractB.deploy();
34+
});
35+
await deployTx.sign([sender.key, contractAccountA.key, contractAccountB.key]);
36+
await deployTx.prove();
37+
await deployTx.send().wait();
38+
console.timeEnd('deploy contract');
39+
40+
console.time('create accounts');
41+
const accountCreationTx = Mina.transaction(sender, async () => {
42+
await contractA.createAccount(sender, UInt64.from(1000));
43+
await contractA.createAccount(receiver2, UInt64.from(1000));
44+
await contractB.createAccount(sender, UInt64.from(1500));
45+
});
46+
await accountCreationTx.sign([sender.key]);
47+
await accountCreationTx.prove();
48+
await accountCreationTx.send().wait();
49+
console.timeEnd('create accounts');
50+
51+
console.time('settle');
52+
await settle(contractA, sender);
53+
await settle(contractB, sender);
54+
console.timeEnd('settle');
55+
56+
assert((await contractA.getSupply()).toBigInt() == 1000n);
57+
assert((await contractB.getSupply()).toBigInt() == 1500n);
58+
59+
console.log('Initial balances:');
60+
console.log(
61+
'Contract A, Sender: ',
62+
(await contractA.offchainState.fields.accounts.get(sender)).value.toBigInt()
63+
);
64+
console.log(
65+
'Contract B, Sender: ',
66+
(await contractB.offchainState.fields.accounts.get(sender)).value.toBigInt()
67+
);
68+
assert((await contractA.getBalance(sender)).toBigInt() == 1000n);
69+
assert((await contractB.getBalance(sender)).toBigInt() == 1500n);
70+
71+
console.time('transfer');
72+
await transfer(contractA, sender, receiver1, UInt64.from(100));
73+
await settle(contractA, sender);
74+
await transfer(contractA, sender, receiver2, UInt64.from(200));
75+
await settle(contractA, sender);
76+
await transfer(contractA, sender, receiver3, UInt64.from(300));
77+
await transfer(contractB, sender, receiver1, UInt64.from(200));
78+
console.timeEnd('transfer');
79+
80+
console.time('settle');
81+
await settle(contractA, sender);
82+
await settle(contractB, sender);
83+
console.timeEnd('settle');
84+
85+
console.log('After Settlement balances:');
86+
console.log(
87+
'Contract A, Sender: ',
88+
(await contractA.offchainState.fields.accounts.get(sender)).value.toBigInt()
89+
);
90+
console.log(
91+
'Contract A, Receiver 1: ',
92+
(
93+
await contractA.offchainState.fields.accounts.get(receiver1)
94+
).value.toBigInt()
95+
);
96+
console.log(
97+
'Contract A, Receiver 2: ',
98+
(
99+
await contractA.offchainState.fields.accounts.get(receiver2)
100+
).value.toBigInt()
101+
);
102+
console.log(
103+
'Contract A, Receiver 3: ',
104+
(
105+
await contractA.offchainState.fields.accounts.get(receiver3)
106+
).value.toBigInt()
107+
);
108+
109+
console.log(
110+
'Contract B, Sender: ',
111+
(await contractB.offchainState.fields.accounts.get(sender)).value.toBigInt()
112+
);
113+
console.log(
114+
'Contract B, Receiver 1: ',
115+
(
116+
await contractB.offchainState.fields.accounts.get(receiver1)
117+
).value.toBigInt()
118+
);
119+
assert((await contractA.getBalance(sender)).toBigInt() == 400n);
120+
assert((await contractA.getBalance(receiver1)).toBigInt() == 100n);
121+
assert((await contractA.getBalance(receiver2)).toBigInt() == 200n);
122+
assert((await contractA.getBalance(receiver3)).toBigInt() == 300n);
123+
124+
assert((await contractB.getBalance(sender)).toBigInt() == 1300n);
125+
assert((await contractB.getBalance(receiver1)).toBigInt() == 200n);
126+
127+
console.time('advance contract A state but leave B unsettled');
128+
await transfer(contractA, sender, receiver1, UInt64.from(150)); // 250, 250, 200, 300
129+
await transfer(contractA, receiver2, receiver3, UInt64.from(20)); // 250, 250, 180, 320
130+
await settle(contractA, sender);
131+
132+
await transfer(contractA, receiver1, receiver2, UInt64.from(50)); // 250, 200, 230, 320
133+
await transfer(contractA, receiver3, sender, UInt64.from(50)); // 300, 200, 230, 270
134+
await settle(contractA, sender);
135+
136+
await transfer(contractB, sender, receiver1, UInt64.from(5));
137+
console.timeEnd('advance contract A state but leave B unsettled');
138+
139+
assert((await contractA.getSupply()).toBigInt() == 1000n);
140+
assert((await contractB.getSupply()).toBigInt() == 1500n);
141+
142+
console.log('Final balances:');
143+
console.log(
144+
'Contract A, Sender: ',
145+
(await contractA.offchainState.fields.accounts.get(sender)).value.toBigInt()
146+
);
147+
console.log(
148+
'Contract A, Receiver 1: ',
149+
(
150+
await contractA.offchainState.fields.accounts.get(receiver1)
151+
).value.toBigInt()
152+
);
153+
console.log(
154+
'Contract A, Receiver 2: ',
155+
(
156+
await contractA.offchainState.fields.accounts.get(receiver2)
157+
).value.toBigInt()
158+
);
159+
console.log(
160+
'Contract A, Receiver 3: ',
161+
(
162+
await contractA.offchainState.fields.accounts.get(receiver3)
163+
).value.toBigInt()
164+
);
165+
166+
console.log(
167+
'Contract B, Sender: ',
168+
(await contractB.offchainState.fields.accounts.get(sender)).value.toBigInt()
169+
);
170+
console.log(
171+
'Contract B, Receiver: ',
172+
(
173+
await contractB.offchainState.fields.accounts.get(receiver1)
174+
).value.toBigInt()
175+
);
176+
177+
assert((await contractA.getBalance(sender)).toBigInt() == 300n);
178+
assert((await contractA.getBalance(receiver1)).toBigInt() == 200n);
179+
assert((await contractA.getBalance(receiver2)).toBigInt() == 230n);
180+
assert((await contractA.getBalance(receiver3)).toBigInt() == 270n);
181+
182+
// The 5 token transfer has not been settled
183+
assert((await contractB.getBalance(sender)).toBigInt() == 1300n);
184+
assert((await contractB.getBalance(receiver1)).toBigInt() == 200n);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { UInt64 } from '../../../../index.js';
2+
import * as Mina from '../../mina.js';
3+
import assert from 'assert';
4+
5+
import {
6+
ExampleContract,
7+
offchainState as exampleOffchainState,
8+
} from './ExampleContract.js';
9+
import { settle, transfer } from './utils.js';
10+
11+
const Local = await Mina.LocalBlockchain({ proofsEnabled: true });
12+
Mina.setActiveInstance(Local);
13+
14+
const [sender, receiver, contractAccount] = Local.testAccounts;
15+
16+
const contract = new ExampleContract(contractAccount);
17+
contract.offchainState.setContractInstance(contract);
18+
19+
console.time('compile offchain state program');
20+
await exampleOffchainState.compile();
21+
console.timeEnd('compile offchain state program');
22+
23+
console.time('compile contract');
24+
await ExampleContract.compile();
25+
console.timeEnd('compile contract');
26+
27+
console.time('deploy contract');
28+
const deployTx = Mina.transaction(sender, async () => {
29+
await contract.deploy();
30+
});
31+
await deployTx.sign([sender.key, contractAccount.key]);
32+
await deployTx.prove();
33+
await deployTx.send().wait();
34+
console.timeEnd('deploy contract');
35+
36+
console.time('create accounts');
37+
const accountCreationTx = Mina.transaction(sender, async () => {
38+
await contract.createAccount(sender, UInt64.from(1000));
39+
});
40+
await accountCreationTx.sign([sender.key]);
41+
await accountCreationTx.prove();
42+
await accountCreationTx.send().wait();
43+
console.timeEnd('create accounts');
44+
45+
console.time('settle');
46+
await settle(contract, sender);
47+
console.timeEnd('settle');
48+
49+
assert((await contract.getSupply()).toBigInt() == 1000n);
50+
assert((await contract.getBalance(sender)).toBigInt() == 1000n);
51+
52+
console.time('transfer');
53+
await transfer(contract, sender, receiver, UInt64.from(100));
54+
console.timeEnd('transfer');
55+
56+
console.time('settle');
57+
await settle(contract, sender);
58+
console.timeEnd('settle');
59+
60+
assert((await contract.getSupply()).toBigInt() == 1000n);
61+
assert((await contract.getBalance(sender)).toBigInt() == 900n);
62+
assert((await contract.getBalance(receiver)).toBigInt() == 100n);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { PublicKey, UInt64 } from '../../../../index.js';
2+
import * as Mina from '../../mina.js';
3+
4+
import { ExampleContract } from './ExampleContract.js';
5+
6+
export { transfer, settle };
7+
8+
async function transfer(
9+
contract: ExampleContract,
10+
sender: Mina.TestPublicKey,
11+
receiver: PublicKey,
12+
amount: UInt64
13+
) {
14+
const tx = Mina.transaction(sender, async () => {
15+
await contract.transfer(sender, receiver, amount);
16+
});
17+
tx.sign([sender.key]);
18+
await tx.prove().send().wait();
19+
}
20+
21+
async function settle(contract: ExampleContract, sender: Mina.TestPublicKey) {
22+
const proof = await contract.offchainState.createSettlementProof();
23+
const tx = Mina.transaction(sender, async () => {
24+
await contract.settle(proof);
25+
});
26+
tx.sign([sender.key]);
27+
await tx.prove().send().wait();
28+
}

0 commit comments

Comments
 (0)