Skip to content

Commit

Permalink
Merge pull request #1412 from o1-labs/fix/valid-permissions
Browse files Browse the repository at this point in the history
Fix: Keep track of changing permissions during transaction validation
  • Loading branch information
mitschabaude authored Feb 6, 2024
2 parents 4d9bea4 + 6c9606d commit a6a985a
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src/bindings
Submodule bindings updated 0 files
2 changes: 1 addition & 1 deletion src/examples/zkapps/dex/upgradability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ async function atomicActionsTest({ withVesting }: { withVesting: boolean }) {
});
await tx.prove();
await expect(tx.sign([feePayerKey, keys.dex]).send()).rejects.toThrow(
/Update_not_permitted_delegate/
/Cannot update field 'delegate'/
);

/**
Expand Down
42 changes: 31 additions & 11 deletions src/lib/mina.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import {
type ActionStates,
type NetworkConstants,
} from './mina/mina-instance.js';
import { SimpleLedger } from './mina/transaction-logic/ledger.js';
import { assert } from './gadgets/common.js';

export {
createTransaction,
Expand Down Expand Up @@ -395,12 +397,10 @@ function LocalBlockchain({

if (enforceTransactionLimits) verifyTransactionLimits(txn.transaction);

for (const update of txn.transaction.accountUpdates) {
let accountJson = ledger.getAccount(
Ml.fromPublicKey(update.body.publicKey),
Ml.constFromField(update.body.tokenId)
);
// create an ad-hoc ledger to record changes to accounts within the transaction
let simpleLedger = SimpleLedger.create();

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()
Expand All @@ -411,16 +411,31 @@ function LocalBlockchain({
);
}

if (accountJson) {
let account = Account.fromJSON(accountJson);
let account = simpleLedger.load(update.body);

// 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);
simpleLedger.store(storedAccount);
account = storedAccount;
}
}

// TODO: verify account update even if the account doesn't exist yet, using a default initial account
if (account !== undefined) {
await verifyAccountUpdate(
account,
update,
commitments,
this.proofsEnabled,
this.getNetworkId()
);
simpleLedger.apply(update);
}
}

Expand Down Expand Up @@ -1239,15 +1254,20 @@ async function verifyAccountUpdate(
publicOutput: [],
};

let verificationKey = account.zkapp?.verificationKey?.data!;
let verificationKey = account.zkapp?.verificationKey?.data;
assert(
verificationKey !== undefined,
'Account does not have a verification key'
);

isValidProof = await verify(proof, verificationKey);
if (!isValidProof) {
throw Error(
`Invalid proof for account update\n${JSON.stringify(update)}`
);
}
} catch (error) {
errorTrace += '\n\n' + (error as Error).message;
errorTrace += '\n\n' + (error as Error).stack;
isValidProof = false;
}
}
Expand All @@ -1261,7 +1281,7 @@ async function verifyAccountUpdate(
networkId
);
} catch (error) {
errorTrace += '\n\n' + (error as Error).message;
errorTrace += '\n\n' + (error as Error).stack;
isValidSignature = false;
}
}
Expand Down Expand Up @@ -1289,7 +1309,7 @@ async function verifyAccountUpdate(
if (!verified) {
throw Error(
`Transaction verification failed: Cannot update field '${field}' because permission for this field is '${p}', but the required authorization was not provided or is invalid.
${errorTrace !== '' ? 'Error trace: ' + errorTrace : ''}`
${errorTrace !== '' ? 'Error trace: ' + errorTrace : ''}\n\n`
);
}
}
Expand Down
13 changes: 12 additions & 1 deletion src/lib/mina/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,23 @@ import { jsLayout } from '../../bindings/mina-transaction/gen/js-layout.js';
import { ProvableExtended } from '../circuit_value.js';

export { FetchedAccount, Account, PartialAccount };
export { accountQuery, parseFetchedAccount, fillPartialAccount };
export { newAccount, accountQuery, parseFetchedAccount, fillPartialAccount };

type AuthRequired = Types.Json.AuthRequired;
type Account = Types.Account;
const Account = Types.Account;

function newAccount(accountId: {
publicKey: PublicKey;
tokenId?: Field;
}): Account {
let account = Account.empty();
account.publicKey = accountId.publicKey;
account.tokenId = accountId.tokenId ?? Types.TokenId.empty();
account.permissions = Permissions.initial();
return account;
}

type PartialAccount = Omit<Partial<Account>, 'zkapp'> & {
zkapp?: Partial<Account['zkapp']>;
};
Expand Down
30 changes: 30 additions & 0 deletions src/lib/mina/transaction-logic/apply.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Apply transactions to a ledger of accounts.
*/
import { type AccountUpdate } from '../../account_update.js';
import { Account } from '../account.js';

export { applyAccountUpdate };

/**
* Apply a single account update to update an account.
*
* TODO:
* - This must receive and return some context global to the transaction, to check validity
* - Should operate on the value / bigint type, not the provable type
*/
function applyAccountUpdate(account: Account, update: AccountUpdate): Account {
account.publicKey.assertEquals(update.publicKey);
account.tokenId.assertEquals(update.tokenId, 'token id mismatch');

// clone account (TODO: do this efficiently)
let json = Account.toJSON(account);
account = Account.fromJSON(json);

// update permissions
if (update.update.permissions.isSome.toBoolean()) {
account.permissions = update.update.permissions.value;
}

return account;
}
60 changes: 60 additions & 0 deletions src/lib/mina/transaction-logic/ledger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* A ledger of accounts - simple model of a local blockchain.
*/
import { PublicKey } from '../../signature.js';
import { type AccountUpdate, TokenId } from '../../account_update.js';
import { Account, newAccount } from '../account.js';
import { Field } from '../../field.js';
import { applyAccountUpdate } from './apply.js';

export { SimpleLedger };

class SimpleLedger {
accounts: Map<bigint, Account>;

constructor() {
this.accounts = new Map();
}

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

exists({ publicKey, tokenId = TokenId.default }: InputAccountId): boolean {
return this.accounts.has(accountId({ publicKey, tokenId }));
}

store(account: Account): void {
this.accounts.set(accountId(account), account);
}

load({
publicKey,
tokenId = TokenId.default,
}: InputAccountId): Account | undefined {
let id = accountId({ publicKey, tokenId });
let account = this.accounts.get(id);
return account;
}

apply(update: AccountUpdate): void {
let id = accountId(update.body);
let account = this.accounts.get(id);
account ??= newAccount(update.body);

let updated = applyAccountUpdate(account, update);
this.accounts.set(id, updated);
}
}

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

function accountId(account: AccountId): bigint {
let id = account.publicKey.x.toBigInt();
id <<= 1n;
id |= BigInt(account.publicKey.isOdd.toBoolean());
id <<= BigInt(Field.sizeInBits);
id |= account.tokenId.toBigInt();
return id;
}

0 comments on commit a6a985a

Please sign in to comment.