Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ This project adheres to
https://github.com/o1-labs/o1js/pull/2436
- Improved the runtime table API with a `RuntimeTable` class with better
readability https://github.com/o1-labs/o1js/pull/2402
- Internal `Mina.LocalBlockchain` type exported via `Mina`. https://github.com/o1-labs/o1js/pull/2538

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion src/examples/zkapps/hello-world/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ try {
await txn.prove();
await txn.sign([feePayer1.key]).send();
} catch (err: any) {
handleError(err, 'Account_delegate_precondition_unsatisfied');
handleError(err, 'delegate precondition');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this breaks when using the local ledger because our error messages are slightly different. This is probably bad :((

Copy link
Member

Choose a reason for hiding this comment

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

No this should actually be fine! Because we also want to improve the error messages and overall error handling. That being said, we should be able to make the error messages "backwards compatible" via something like this throw Error('Account_delegate_precondition_unsatisfied: ....');

}

if (!correctlyFails) {
Expand Down
140 changes: 140 additions & 0 deletions src/lib/mina/v1/ledger/ledger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { PublicKey } from '../../../provable/crypto/signature.js';
import { Int64, UInt64 } from '../../../provable/int.js';
import { Field } from '../../../provable/wrapped.js';
import { AccountUpdate as AccountUpdateV2 } from '../../v2/account-update.js';
import { Account as AccountV2 } from '../../v2/account.js';
import { ZkappFeePayment } from '../../v2/transaction.js';
import { ChainView } from '../../v2/views.js';
import {
AccountUpdateApplyResult,
ApplyState,
checkAndApplyAccountUpdate,
checkAndApplyFeePayment,
} from '../../v2/zkapp-logic.js';
import { TokenId, ZkappCommand } from '../account-update.js';
import { Account as AccountV1, newAccount } from '../account.js';

export const DefaultTokenId = 1n;

export type AccountId = { publicKey: PublicKey; tokenId?: Field };

export class LocalLedger {
_nextLocation: bigint;
_accounts: Map<bigint, any>;
_locations: Map<string, bigint>; // keyed by "pubkey|tokenId"

constructor() {
this._nextLocation = 1n;
this._accounts = new Map<bigint, any>();
this._locations = new Map<string, bigint>();
}

static create(): LocalLedger {
return new LocalLedger();
}

_key(publicKey: PublicKey, tokenId: Field) {
return `${publicKey.toBase58()}|${tokenId}`;
}

saveAccount(publicKey: PublicKey, account: AccountV1) {
const location = (() => {
const key = this._key(publicKey, TokenId.default);
const existing = this._locations.get(key);
if (existing === undefined) throw new Error('account with public key does not exist');
return existing;
})();
this._accounts.set(location, account);
}

addAccount(publicKey: PublicKey, balance: bigint | number | string): void {
const accountId = { publicKey, tokenId: TokenId.default };
const location = (() => {
const key = this._key(publicKey, TokenId.default);
const existing = this._locations.get(key);
if (existing !== undefined) throw new Error('account with public key already exists');
const newLocation = this._nextLocation;
this._nextLocation += 1n;
return newLocation;
})();

const account = newAccount(accountId);
account.balance = UInt64.from(balance);
const key = this._key(publicKey, TokenId.default);
this._locations.set(key, location);
this._accounts.set(location, account);
}

getAccount(publicKey: PublicKey, tokenId: Field = TokenId.default): AccountV1 | undefined {
const key = this._key(publicKey, tokenId);
const location = this._locations.get(key);
if (location === undefined) return undefined;
return this._accounts.get(location);
}

getOrCreateAccount(publicKey: PublicKey, tokenId: Field = TokenId.default): AccountV1 {
const account = this.getAccount(publicKey, tokenId);
if (account != undefined) {
return account;
}

this.addAccount(publicKey, 0);
return this.getAccount(publicKey, tokenId)!;
}

applyTransaction(transaction: ZkappCommand, fee: UInt64, networkState: ChainView): void {
const feePayerAccount = this.getAccount(transaction.feePayer.body.publicKey);
if (!feePayerAccount) {
throw new Error('fee payer account not found');
}

const feePayerAccountV2 = AccountV2.fromV1(feePayerAccount);
const feePayerUpdate = checkAndApplyFeePayment(
networkState,
feePayerAccountV2,
new ZkappFeePayment({
publicKey: feePayerAccount.publicKey,
nonce: feePayerAccount.nonce,
fee,
validUntil: transaction.feePayer.body.validUntil,
})
);
if (feePayerUpdate.status == 'Failed') {
throw new Error(`failed to apply fee payment with errors: ${feePayerUpdate.errors}`);
}
this.saveAccount(feePayerAccount.publicKey, feePayerUpdate.updatedAccount.toV1());

let feeExcessState: ApplyState<Int64> = { status: 'Alive', value: Int64.zero };

for (const update of transaction.accountUpdates) {
const { body, authorization } = update;
if (body.authorizationKind.isProved.toBoolean() && !authorization.proof) {
throw Error(
`The actual authorization does not match the expected authorization kind. Did you forget to invoke \`await tx.prove()\`?`
);
}
const account = this.getOrCreateAccount(body.publicKey, body.tokenId);
const accountUpdateV2 = AccountUpdateV2.fromInternalRepr(update.body);
const accountV2 = AccountV2.fromV1(account);
try {
const applied: AccountUpdateApplyResult = checkAndApplyAccountUpdate(
networkState,
accountV2,
accountUpdateV2,
feeExcessState
);
if (applied.status == 'Failed') {
throw new Error(`Failed to apply account update with errors: ${applied.errors}`);
}
this.saveAccount(body.publicKey, applied.updatedAccount.toV1());
feeExcessState = applied.updatedFeeExcessState;
} catch (e) {
console.error(e);
console.log(JSON.stringify(account, null, 2));
console.log(JSON.stringify(accountV2, null, 2));
console.log(JSON.stringify(update, null, 2));
throw e;
}
}
}
}
54 changes: 17 additions & 37 deletions src/lib/mina/v1/local-blockchain.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { Ledger, Test, initializeBindings } from '../../../bindings.js';
import { Types, TypesBigint } from '../../../bindings/mina-transaction/v1/types.js';
import { Test, initializeBindings } from '../../../bindings.js';
import { TypesBigint } from '../../../bindings/mina-transaction/v1/types.js';
import { transactionCommitments } from '../../../mina-signer/src/sign-zkapp-command.js';
import { NetworkId } from '../../../mina-signer/src/types.js';
import { Ml } from '../../ml/conversion.js';
import { PrivateKey, PublicKey } from '../../provable/crypto/signature.js';
import { UInt32, UInt64 } from '../../provable/int.js';
import { Field } from '../../provable/wrapped.js';
import { prettifyStacktrace } from '../../util/errors.js';
import { TupleN } from '../../util/types.js';
import { Actions, Authorization, TokenId, ZkappCommand } from './account-update.js';
import { Account } from './account.js';
import { invalidTransactionError } from './errors.js';
import { LocalLedger } from './ledger/ledger.js';
import {
Mina,
defaultNetworkConstants,
Expand Down Expand Up @@ -71,24 +70,16 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits
const slotTime = 3 * 60 * 1000;
const startTime = Date.now();
const genesisTimestamp = UInt64.from(startTime);
const ledger = Ledger.create();
const ledger = LocalLedger.create();
let networkState = defaultNetworkState();

function addAccount(publicKey: PublicKey, balance: string) {
try {
ledger.addAccount(Ml.fromPublicKey(publicKey), balance);
} catch (error) {
throw prettifyStacktrace(error);
}
}

let testAccounts = [] as never as TupleN<TestPublicKey, 10>;

for (let i = 0; i < 10; ++i) {
let MINA = 10n ** 9n;
const largeValue = 1000n * MINA;
const testAccount = TestPublicKey.random();
addAccount(testAccount, largeValue.toString());
ledger.addAccount(testAccount, largeValue.toString());
testAccounts.push(testAccount);
}

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

// the first time we encounter an account, use it from the persistent ledger
if (account === undefined) {
let accountJson = ledger.getAccount(
Ml.fromPublicKey(update.body.publicKey),
Ml.constFromField(update.body.tokenId)
);
if (accountJson !== undefined) {
let storedAccount = Account.fromJSON(accountJson);
let storedAccount = ledger.getAccount(update.body.publicKey, update.body.tokenId);
if (storedAccount !== undefined) {
simpleLedger.store(storedAccount);
account = storedAccount;
}
Expand All @@ -178,10 +165,10 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits
let status: PendingTransactionStatus = 'pending';
const errors: string[] = [];
try {
ledger.applyJsonTransaction(
JSON.stringify(zkappCommandJson),
defaultNetworkConstants.accountCreationFee.toString(),
JSON.stringify(networkState)
ledger.applyTransaction(
txn.transaction,
defaultNetworkConstants.accountCreationFee,
networkState
);
} catch (err: any) {
status = 'rejected';
Expand Down Expand Up @@ -310,13 +297,6 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits
});
});
},
applyJsonTransaction(json: string) {
return ledger.applyJsonTransaction(
json,
defaultNetworkConstants.accountCreationFee.toString(),
JSON.stringify(networkState)
);
},
async fetchEvents(publicKey: PublicKey, tokenId: Field = TokenId.default) {
// Return events in reverse chronological order (latest events at the beginning)
const reversedEvents = (
Expand Down Expand Up @@ -363,7 +343,7 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits
}
return currentActions.slice(startIndex, endIndex);
},
addAccount,
addAccount: ledger.addAccount.bind(ledger),
/**
* An array of 10 test accounts that have been pre-filled with
* 30000000000 units of currency.
Expand Down
46 changes: 23 additions & 23 deletions src/lib/mina/v2/account-update.unit-test.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

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

only sorting imports

Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
import { AccountUpdate, Authorized, GenericData } from './account-update.js';
import { AccountId, AccountTiming } from './account.js';
import { AccountUpdateAuthorizationKind } from './authorization.js';
import { TokenId, Update } from './core.js';
import { Precondition } from './preconditions.js';
import { GenericStatePreconditions, GenericStateUpdates } from './state.js';
import { AccountUpdate as V1AccountUpdateImpl } from '../v1/account-update.js';
import { VerificationKey } from '../../proof-system/verification-key.js';
import { Bool } from '../../provable/bool.js';
import { Field } from '../../provable/field.js';
import { UInt32, UInt64, Int64, Sign } from '../../provable/int.js';
import { PrivateKey } from '../../provable/crypto/signature.js';
import { expect } from 'expect';
import { jsLayout as layoutV1 } from '../../../bindings/mina-transaction/gen/v1/js-layout.js';
import * as ValuesV1 from '../../../bindings/mina-transaction/gen/v1/transaction-bigint.js';
import * as JsonV1 from '../../../bindings/mina-transaction/gen/v1/transaction-json.js';
import * as TypesV1 from '../../../bindings/mina-transaction/gen/v1/transaction.js';
import {
Actions as V1Actions,
Events as V1Events,
Sign as V1Sign,
TokenSymbol as V1TokenSymbol,
ZkappUri as V1ZkappUri,
} from '../../../bindings/mina-transaction/v1/transaction-leaves.js';
import * as TypesV1 from '../../../bindings/mina-transaction/gen/v1/transaction.js';
import * as ValuesV1 from '../../../bindings/mina-transaction/gen/v1/transaction-bigint.js';
import * as JsonV1 from '../../../bindings/mina-transaction/gen/v1/transaction-json.js';
import { jsLayout as layoutV1 } from '../../../bindings/mina-transaction/gen/v1/js-layout.js';
import { expect } from 'expect';
import { VerificationKey } from '../../proof-system/verification-key.js';
import { Bool } from '../../provable/bool.js';
import { PrivateKey } from '../../provable/crypto/signature.js';
import { Field } from '../../provable/field.js';
import { Int64, Sign, UInt32, UInt64 } from '../../provable/int.js';
import { AccountUpdate as V1AccountUpdateImpl } from '../v1/account-update.js';
import { AccountUpdate, Authorized, GenericData } from './account-update.js';
import { AccountId, AccountTiming } from './account.js';
import { AccountUpdateAuthorizationKind } from './authorization.js';
import { TokenId, Update } from './core.js';
import { Precondition } from './preconditions.js';
import { GenericStatePreconditions, GenericStateUpdates } from './state.js';

import {
Signature,
signFieldElement,
zkAppBodyPrefix,
} from '../../../mina-signer/src/signature.js';
import { ZkappConstants } from '../v1/constants.js';
import {
testV1V2ClassEquivalence,
testV1V2ValueEquivalence,
testV2Encoding,
} from './test/utils.js';
import {
Signature,
signFieldElement,
zkAppBodyPrefix,
} from '../../../mina-signer/src/signature.js';

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

function testHashEquality(v1: TypesV1.AccountUpdate, v2: Authorized) {
expect(TypesV1.AccountUpdate.toInput(v1)).toEqual(v2.toInput());
Expand Down
Loading
Loading