Skip to content

Commit cf81984

Browse files
reimplementing local ledger -- WIP
1 parent e2d5762 commit cf81984

File tree

8 files changed

+303
-326
lines changed

8 files changed

+303
-326
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ This project adheres to
6363
https://github.com/o1-labs/o1js/pull/2436
6464
- Improved the runtime table API with a `RuntimeTable` class with better
6565
readability https://github.com/o1-labs/o1js/pull/2402
66+
- Internal `Mina.LocalBlockchain` type exported via `Mina`. https://github.com/o1-labs/o1js/pull/2538
6667

6768
### Fixed
6869

src/examples/zkapps/hello-world/run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ try {
6363
await txn.prove();
6464
await txn.sign([feePayer1.key]).send();
6565
} catch (err: any) {
66-
handleError(err, 'Account_delegate_precondition_unsatisfied');
66+
handleError(err, 'delegate precondition');
6767
}
6868

6969
if (!correctlyFails) {

src/lib/mina/v1/ledger/ledger.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { PublicKey } from '../../../provable/crypto/signature.js';
2+
import { Int64, UInt64 } from '../../../provable/int.js';
3+
import { Field } from '../../../provable/wrapped.js';
4+
import { AccountUpdate as AccountUpdateV2 } from '../../v2/account-update.js';
5+
import { Account as AccountV2 } from '../../v2/account.js';
6+
import { ZkappFeePayment } from '../../v2/transaction.js';
7+
import { ChainView } from '../../v2/views.js';
8+
import {
9+
AccountUpdateApplyResult,
10+
ApplyState,
11+
checkAndApplyAccountUpdate,
12+
checkAndApplyFeePayment,
13+
} from '../../v2/zkapp-logic.js';
14+
import { TokenId, ZkappCommand } from '../account-update.js';
15+
import { Account as AccountV1, newAccount } from '../account.js';
16+
17+
export const DefaultTokenId = 1n;
18+
19+
export type AccountId = { publicKey: PublicKey; tokenId?: Field };
20+
21+
export class LocalLedger {
22+
_nextLocation: bigint;
23+
_accounts: Map<bigint, any>;
24+
_locations: Map<string, bigint>; // keyed by "pubkey|tokenId"
25+
26+
constructor() {
27+
this._nextLocation = 1n;
28+
this._accounts = new Map<bigint, any>();
29+
this._locations = new Map<string, bigint>();
30+
}
31+
32+
static create(): LocalLedger {
33+
return new LocalLedger();
34+
}
35+
36+
_key(publicKey: PublicKey, tokenId: Field) {
37+
return `${publicKey.toBase58()}|${tokenId}`;
38+
}
39+
40+
saveAccount(publicKey: PublicKey, account: AccountV1) {
41+
const location = (() => {
42+
const key = this._key(publicKey, TokenId.default);
43+
const existing = this._locations.get(key);
44+
if (existing === undefined) throw new Error('account with public key does not exist');
45+
return existing;
46+
})();
47+
this._accounts.set(location, account);
48+
}
49+
50+
addAccount(publicKey: PublicKey, balance: bigint | number | string): void {
51+
const accountId = { publicKey, tokenId: TokenId.default };
52+
const location = (() => {
53+
const key = this._key(publicKey, TokenId.default);
54+
const existing = this._locations.get(key);
55+
if (existing !== undefined) throw new Error('account with public key already exists');
56+
const newLocation = this._nextLocation;
57+
this._nextLocation += 1n;
58+
return newLocation;
59+
})();
60+
61+
const account = newAccount(accountId);
62+
account.balance = UInt64.from(balance);
63+
const key = this._key(publicKey, TokenId.default);
64+
this._locations.set(key, location);
65+
this._accounts.set(location, account);
66+
}
67+
68+
getAccount(publicKey: PublicKey, tokenId: Field = TokenId.default): AccountV1 | undefined {
69+
const key = this._key(publicKey, tokenId);
70+
const location = this._locations.get(key);
71+
if (location === undefined) return undefined;
72+
return this._accounts.get(location);
73+
}
74+
75+
getOrCreateAccount(publicKey: PublicKey, tokenId: Field = TokenId.default): AccountV1 {
76+
const account = this.getAccount(publicKey, tokenId);
77+
if (account != undefined) {
78+
return account;
79+
}
80+
81+
this.addAccount(publicKey, 0);
82+
return this.getAccount(publicKey, tokenId)!;
83+
}
84+
85+
applyTransaction(transaction: ZkappCommand, fee: UInt64, networkState: ChainView): void {
86+
const feePayerAccount = this.getAccount(transaction.feePayer.body.publicKey);
87+
if (!feePayerAccount) {
88+
throw new Error('fee payer account not found');
89+
}
90+
91+
const feePayerAccountV2 = AccountV2.fromV1(feePayerAccount);
92+
const feePayerUpdate = checkAndApplyFeePayment(
93+
networkState,
94+
feePayerAccountV2,
95+
new ZkappFeePayment({
96+
publicKey: feePayerAccount.publicKey,
97+
nonce: feePayerAccount.nonce,
98+
fee,
99+
validUntil: transaction.feePayer.body.validUntil,
100+
})
101+
);
102+
if (feePayerUpdate.status == 'Failed') {
103+
throw new Error(`failed to apply fee payment with errors: ${feePayerUpdate.errors}`);
104+
}
105+
this.saveAccount(feePayerAccount.publicKey, feePayerUpdate.updatedAccount.toV1());
106+
107+
let feeExcessState: ApplyState<Int64> = { status: 'Alive', value: Int64.zero };
108+
109+
for (const update of transaction.accountUpdates) {
110+
const { body, authorization } = update;
111+
if (body.authorizationKind.isProved.toBoolean() && !authorization.proof) {
112+
throw Error(
113+
`The actual authorization does not match the expected authorization kind. Did you forget to invoke \`await tx.prove()\`?`
114+
);
115+
}
116+
const account = this.getOrCreateAccount(body.publicKey, body.tokenId);
117+
const accountUpdateV2 = AccountUpdateV2.fromInternalRepr(update.body);
118+
const accountV2 = AccountV2.fromV1(account);
119+
const applied: AccountUpdateApplyResult = checkAndApplyAccountUpdate(
120+
networkState,
121+
accountV2,
122+
accountUpdateV2,
123+
feeExcessState
124+
);
125+
if (applied.status == 'Failed') {
126+
throw new Error(`Failed to apply account update with errors: ${applied.errors}`);
127+
}
128+
this.saveAccount(body.publicKey, applied.updatedAccount.toV1());
129+
feeExcessState = applied.updatedFeeExcessState;
130+
}
131+
}
132+
}

src/lib/mina/v1/local-blockchain.ts

Lines changed: 17 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import { Ledger, Test, initializeBindings } from '../../../bindings.js';
2-
import { Types, TypesBigint } from '../../../bindings/mina-transaction/v1/types.js';
1+
import { Test, initializeBindings } from '../../../bindings.js';
2+
import { TypesBigint } from '../../../bindings/mina-transaction/v1/types.js';
33
import { transactionCommitments } from '../../../mina-signer/src/sign-zkapp-command.js';
44
import { NetworkId } from '../../../mina-signer/src/types.js';
5-
import { Ml } from '../../ml/conversion.js';
65
import { PrivateKey, PublicKey } from '../../provable/crypto/signature.js';
76
import { UInt32, UInt64 } from '../../provable/int.js';
87
import { Field } from '../../provable/wrapped.js';
9-
import { prettifyStacktrace } from '../../util/errors.js';
108
import { TupleN } from '../../util/types.js';
119
import { Actions, Authorization, TokenId, ZkappCommand } from './account-update.js';
1210
import { Account } from './account.js';
1311
import { invalidTransactionError } from './errors.js';
12+
import { LocalLedger } from './ledger/ledger.js';
1413
import {
1514
Mina,
1615
defaultNetworkConstants,
@@ -71,24 +70,16 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits
7170
const slotTime = 3 * 60 * 1000;
7271
const startTime = Date.now();
7372
const genesisTimestamp = UInt64.from(startTime);
74-
const ledger = Ledger.create();
73+
const ledger = LocalLedger.create();
7574
let networkState = defaultNetworkState();
7675

77-
function addAccount(publicKey: PublicKey, balance: string) {
78-
try {
79-
ledger.addAccount(Ml.fromPublicKey(publicKey), balance);
80-
} catch (error) {
81-
throw prettifyStacktrace(error);
82-
}
83-
}
84-
8576
let testAccounts = [] as never as TupleN<TestPublicKey, 10>;
8677

8778
for (let i = 0; i < 10; ++i) {
8879
let MINA = 10n ** 9n;
8980
const largeValue = 1000n * MINA;
9081
const testAccount = TestPublicKey.random();
91-
addAccount(testAccount, largeValue.toString());
82+
ledger.addAccount(testAccount, largeValue.toString());
9283
testAccounts.push(testAccount);
9384
}
9485

@@ -109,14 +100,14 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits
109100
return UInt32.from(Math.ceil((new Date().valueOf() - startTime) / slotTime));
110101
},
111102
hasAccount(publicKey: PublicKey, tokenId: Field = TokenId.default) {
112-
return !!ledger.getAccount(Ml.fromPublicKey(publicKey), Ml.constFromField(tokenId));
103+
return !!ledger.getAccount(publicKey, tokenId);
113104
},
114105
getAccount(publicKey: PublicKey, tokenId: Field = TokenId.default): Account {
115-
let accountJson = ledger.getAccount(Ml.fromPublicKey(publicKey), Ml.constFromField(tokenId));
116-
if (accountJson === undefined) {
106+
let account = ledger.getAccount(publicKey, tokenId);
107+
if (account === undefined) {
117108
throw new Error(reportGetAccountError(publicKey.toBase58(), TokenId.toBase58(tokenId)));
118109
}
119-
return Types.Account.fromJSON(accountJson);
110+
return account;
120111
},
121112
getNetworkState() {
122113
return networkState;
@@ -137,7 +128,7 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits
137128
for (const update of txn.transaction.accountUpdates) {
138129
let authIsProof = !!update.authorization.proof;
139130
let kindIsProof = update.body.authorizationKind.isProved.toBoolean();
140-
// checks and edge case where a proof is expected, but the developer forgot to invoke await tx.prove()
131+
// checks an edge case where a proof is expected, but the developer forgot to invoke await tx.prove()
141132
// this resulted in an assertion OCaml error, which didn't contain any useful information
142133
if (kindIsProof && !authIsProof) {
143134
throw Error(
@@ -149,12 +140,8 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits
149140

150141
// the first time we encounter an account, use it from the persistent ledger
151142
if (account === undefined) {
152-
let accountJson = ledger.getAccount(
153-
Ml.fromPublicKey(update.body.publicKey),
154-
Ml.constFromField(update.body.tokenId)
155-
);
156-
if (accountJson !== undefined) {
157-
let storedAccount = Account.fromJSON(accountJson);
143+
let storedAccount = ledger.getAccount(update.body.publicKey, update.body.tokenId);
144+
if (storedAccount !== undefined) {
158145
simpleLedger.store(storedAccount);
159146
account = storedAccount;
160147
}
@@ -178,10 +165,10 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits
178165
let status: PendingTransactionStatus = 'pending';
179166
const errors: string[] = [];
180167
try {
181-
ledger.applyJsonTransaction(
182-
JSON.stringify(zkappCommandJson),
183-
defaultNetworkConstants.accountCreationFee.toString(),
184-
JSON.stringify(networkState)
168+
ledger.applyTransaction(
169+
txn.transaction,
170+
defaultNetworkConstants.accountCreationFee,
171+
networkState
185172
);
186173
} catch (err: any) {
187174
status = 'rejected';
@@ -310,13 +297,6 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits
310297
});
311298
});
312299
},
313-
applyJsonTransaction(json: string) {
314-
return ledger.applyJsonTransaction(
315-
json,
316-
defaultNetworkConstants.accountCreationFee.toString(),
317-
JSON.stringify(networkState)
318-
);
319-
},
320300
async fetchEvents(publicKey: PublicKey, tokenId: Field = TokenId.default) {
321301
// Return events in reverse chronological order (latest events at the beginning)
322302
const reversedEvents = (
@@ -363,7 +343,7 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits
363343
}
364344
return currentActions.slice(startIndex, endIndex);
365345
},
366-
addAccount,
346+
addAccount: ledger.addAccount.bind(ledger),
367347
/**
368348
* An array of 10 test accounts that have been pre-filled with
369349
* 30000000000 units of currency.

src/lib/mina/v2/account-update.unit-test.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,42 @@
1-
import { AccountUpdate, Authorized, GenericData } from './account-update.js';
2-
import { AccountId, AccountTiming } from './account.js';
3-
import { AccountUpdateAuthorizationKind } from './authorization.js';
4-
import { TokenId, Update } from './core.js';
5-
import { Precondition } from './preconditions.js';
6-
import { GenericStatePreconditions, GenericStateUpdates } from './state.js';
7-
import { AccountUpdate as V1AccountUpdateImpl } from '../v1/account-update.js';
8-
import { VerificationKey } from '../../proof-system/verification-key.js';
9-
import { Bool } from '../../provable/bool.js';
10-
import { Field } from '../../provable/field.js';
11-
import { UInt32, UInt64, Int64, Sign } from '../../provable/int.js';
12-
import { PrivateKey } from '../../provable/crypto/signature.js';
1+
import { expect } from 'expect';
2+
import { jsLayout as layoutV1 } from '../../../bindings/mina-transaction/gen/v1/js-layout.js';
3+
import * as ValuesV1 from '../../../bindings/mina-transaction/gen/v1/transaction-bigint.js';
4+
import * as JsonV1 from '../../../bindings/mina-transaction/gen/v1/transaction-json.js';
5+
import * as TypesV1 from '../../../bindings/mina-transaction/gen/v1/transaction.js';
136
import {
147
Actions as V1Actions,
158
Events as V1Events,
169
Sign as V1Sign,
1710
TokenSymbol as V1TokenSymbol,
1811
ZkappUri as V1ZkappUri,
1912
} from '../../../bindings/mina-transaction/v1/transaction-leaves.js';
20-
import * as TypesV1 from '../../../bindings/mina-transaction/gen/v1/transaction.js';
21-
import * as ValuesV1 from '../../../bindings/mina-transaction/gen/v1/transaction-bigint.js';
22-
import * as JsonV1 from '../../../bindings/mina-transaction/gen/v1/transaction-json.js';
23-
import { jsLayout as layoutV1 } from '../../../bindings/mina-transaction/gen/v1/js-layout.js';
24-
import { expect } from 'expect';
13+
import { VerificationKey } from '../../proof-system/verification-key.js';
14+
import { Bool } from '../../provable/bool.js';
15+
import { PrivateKey } from '../../provable/crypto/signature.js';
16+
import { Field } from '../../provable/field.js';
17+
import { Int64, Sign, UInt32, UInt64 } from '../../provable/int.js';
18+
import { AccountUpdate as V1AccountUpdateImpl } from '../v1/account-update.js';
19+
import { AccountUpdate, Authorized, GenericData } from './account-update.js';
20+
import { AccountId, AccountTiming } from './account.js';
21+
import { AccountUpdateAuthorizationKind } from './authorization.js';
22+
import { TokenId, Update } from './core.js';
23+
import { Precondition } from './preconditions.js';
24+
import { GenericStatePreconditions, GenericStateUpdates } from './state.js';
2525

26+
import {
27+
Signature,
28+
signFieldElement,
29+
zkAppBodyPrefix,
30+
} from '../../../mina-signer/src/signature.js';
2631
import { ZkappConstants } from '../v1/constants.js';
2732
import {
2833
testV1V2ClassEquivalence,
2934
testV1V2ValueEquivalence,
3035
testV2Encoding,
3136
} from './test/utils.js';
32-
import {
33-
Signature,
34-
signFieldElement,
35-
zkAppBodyPrefix,
36-
} from '../../../mina-signer/src/signature.js';
3737

3838
import { Types } from '../../../bindings/mina-transaction/v1/types.js';
39-
import { packToFields, hashWithPrefix } from '../../../lib/provable/crypto/poseidon.js';
39+
import { hashWithPrefix, packToFields } from '../../../lib/provable/crypto/poseidon.js';
4040

4141
function testHashEquality(v1: TypesV1.AccountUpdate, v2: Authorized) {
4242
expect(TypesV1.AccountUpdate.toInput(v1)).toEqual(v2.toInput());

0 commit comments

Comments
 (0)