Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enforce protocol limits on account updates and events #620

Merged
merged 10 commits into from
Dec 2, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Type inference for Structs with instance methods https://github.com/o1-labs/snarkyjs/pull/567
- also fixes `Struct.fromJSON`

### Changed

- New option `enforceTransactionLimits` for `LocalBlockchain` (default value: `true`), to disable the enforcement of protocol transaction limits (maximum events, maximum sequence events and enforcing certain layout of `AccountUpdate`s depending on their authorization) https://github.com/o1-labs/snarkyjs/pull/620

## [0.7.3](https://github.com/o1-labs/snarkyjs/compare/5f20f496...d880bd6e)

### Fixed
Expand Down
127 changes: 127 additions & 0 deletions src/lib/mina.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export {
fetchEvents,
getActions,
FeePayerSpec,
// for internal testing only
filterGroups,
};
interface TransactionId {
wait(): Promise<void>;
Expand Down Expand Up @@ -180,6 +182,7 @@ function createTransaction(
let accountUpdates = currentTransaction.get().accountUpdates;
CallForest.addCallers(accountUpdates);
accountUpdates = CallForest.toFlatList(accountUpdates);

try {
// check that on-chain values weren't used without setting a precondition
for (let accountUpdate of accountUpdates) {
Expand Down Expand Up @@ -287,6 +290,7 @@ const defaultAccountCreationFee = 1_000_000_000;
function LocalBlockchain({
accountCreationFee = defaultAccountCreationFee as string | number,
proofsEnabled = true,
enforceTransactionLimits = true,
} = {}) {
const msPerSlot = 3 * 60 * 1000;
const startTime = new Date().valueOf();
Expand Down Expand Up @@ -377,6 +381,9 @@ function LocalBlockchain({
JSON.stringify(zkappCommandToJson(txn.transaction))
);

if (enforceTransactionLimits)
verifyTransactionLimits(txn.transaction.accountUpdates);

for (const update of txn.transaction.accountUpdates) {
let account = ledger.getAccount(
update.body.publicKey,
Expand Down Expand Up @@ -617,6 +624,8 @@ function Network(graphqlEndpoint: string): Mina {
async sendTransaction(txn: Transaction) {
txn.sign();

verifyTransactionLimits(txn.transaction.accountUpdates);

let [response, error] = await Fetch.sendZkapp(txn.toJSON());
let errors: any[] | undefined;
if (error === undefined) {
Expand Down Expand Up @@ -1039,3 +1048,121 @@ async function verifyAccountUpdate(
);
}
}

function verifyTransactionLimits(accountUpdates: AccountUpdate[]) {
// constants used to calculate cost of a transaction - originally defined in the genesis_constants file in the mina repo
const proofCost = 10.26;
const signedPairCost = 10.08;
const signedSingleCost = 9.14;
const costLimit = 69.45;
Copy link
Member

Choose a reason for hiding this comment

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

I think we should eventually add this to auto-generation via the snarky_js_constants.ml mechanism, so it'd guaranteed to stay in sync. It's not urgent though and can be an issue for later


// constants that define the maximum number of events in one transaction
const maxSequenceEventElements = 16;
const maxEventElements = 16;

let eventElements = {
events: 0,
sequence: 0,
};

let authTypes = filterGroups(
accountUpdates.map((update) => {
let json = update.toJSON();
eventElements.events += update.body.events.data.length;
eventElements.sequence += update.body.sequenceEvents.data.length;
return json.body.authorizationKind;
})
);
/*
np := proof
n2 := signedPair
n1 := signedSingle
10.26*np + 10.08*n2 + 9.14*n1 < 69.45
formula used to calculate how expensive a zkapp transaction is
*/

let totalTimeRequired =
proofCost * authTypes['proof'] +
signedPairCost * authTypes['signedPair'] +
signedSingleCost * authTypes['signedSingle'];

let isWithinCostLimit = totalTimeRequired < costLimit;

let isWithinEventsLimit = eventElements['events'] <= maxEventElements;
let isWithinSequenceEventsLimit =
eventElements['sequence'] <= maxSequenceEventElements;

let error = '';

if (!isWithinCostLimit) {
// TODO: we should add a link to the docs explaining the reasoning behind it once we have such an explainer
error += `Error: The transaction is too expensive, try reducing the number of AccountUpdates that are attached to the transaction.
Each transaction needs to be processed by the snark workers on the network.
Certain layouts of AccountUpdates require more proving time than others, and therefore are too expensive.
${JSON.stringify(authTypes)}
\n\n`;
}

if (!isWithinEventsLimit) {
error += `Error: The AccountUpdates in your transaction are trying to emit too many events. The maximum allowed amount of events is ${maxEventElements}, but you tried to emit ${eventElements['events']}.\n\n`;
}

if (!isWithinSequenceEventsLimit) {
error += `Error: The AccountUpdates in your transaction are trying to emit too many actions. The maximum allowed amount of actions is ${maxSequenceEventElements}, but you tried to emit ${eventElements['sequence']}.\n\n`;
}

if (error) throw Error('Error during transaction sending:\n\n' + error);
}

let S = 'Signature';
let N = 'None_given';
let P = 'Proof';

const isPair = (pair: string) =>
pair == S + N || pair == N + S || pair == S + S || pair == N + N;

function filterPairs(xs: string[]): {
xs: string[];
pairs: number;
} {
if (xs.length <= 1)
return {
xs,
pairs: 0,
};
if (isPair(xs[0].concat(xs[1]))) {
let rec = filterPairs(xs.slice(2));
return {
xs: rec.xs,
pairs: rec.pairs + 1,
};
} else {
let rec = filterPairs(xs.slice(1));
return {
xs: [xs[0]].concat(rec.xs),
pairs: rec.pairs,
};
}
}

function filterGroups(xs: string[]) {
let pairs = filterPairs(xs);
xs = pairs.xs;

let singleCount = 0;
let proofCount = 0;

xs.forEach((t) => {
if (t == P) proofCount++;
else singleCount++;
});

return {
signedPair: pairs.pairs,
signedSingle: singleCount,
proof: proofCount,
};
}
104 changes: 104 additions & 0 deletions src/lib/mina.unit-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { filterGroups } from './mina.js';
import { expect } from 'expect';
import { shutdown } from '../index.js';

let S = 'Signature';
let N = 'None_given';
let P = 'Proof';

expect(filterGroups([S, S, S, S, S, S])).toEqual({
proof: 0,
signedPair: 3,
signedSingle: 0,
});

expect(filterGroups([N, N, N, N, N, N])).toEqual({
proof: 0,
signedPair: 3,
signedSingle: 0,
});

expect(filterGroups([N, S, S, N, N, S])).toEqual({
proof: 0,
signedPair: 3,
signedSingle: 0,
});

expect(filterGroups([S, P, S, S, S, S])).toEqual({
proof: 1,
signedPair: 2,
signedSingle: 1,
});

expect(filterGroups([N, P, N, P, N, P])).toEqual({
proof: 3,
signedPair: 0,
signedSingle: 3,
});

expect(filterGroups([N, P])).toEqual({
proof: 1,
signedPair: 0,
signedSingle: 1,
});

expect(filterGroups([N, S])).toEqual({
proof: 0,
signedPair: 1,
signedSingle: 0,
});

expect(filterGroups([P, P])).toEqual({
proof: 2,
signedPair: 0,
signedSingle: 0,
});

expect(filterGroups([P, P, S, N, N])).toEqual({
proof: 2,
signedPair: 1,
signedSingle: 1,
});

expect(filterGroups([P])).toEqual({
proof: 1,
signedPair: 0,
signedSingle: 0,
});

expect(filterGroups([S])).toEqual({
proof: 0,
signedPair: 0,
signedSingle: 1,
});

expect(filterGroups([N])).toEqual({
proof: 0,
signedPair: 0,
signedSingle: 1,
});

expect(filterGroups([N, N])).toEqual({
proof: 0,
signedPair: 1,
signedSingle: 0,
});

expect(filterGroups([N, S])).toEqual({
proof: 0,
signedPair: 1,
signedSingle: 0,
});

expect(filterGroups([S, N])).toEqual({
proof: 0,
signedPair: 1,
signedSingle: 0,
});

expect(filterGroups([S, S])).toEqual({
proof: 0,
signedPair: 1,
signedSingle: 0,
});
shutdown();
5 changes: 4 additions & 1 deletion src/lib/token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,10 @@ let zkAppCAddress: PublicKey;
let zkAppC: ZkAppC;

function setupAccounts() {
let Local = Mina.LocalBlockchain({ proofsEnabled: true });
let Local = Mina.LocalBlockchain({
proofsEnabled: true,
enforceTransactionLimits: false,
});
Mina.setActiveInstance(Local);
feePayerKey = Local.testAccounts[0].privateKey;

Expand Down