diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c8dab38d..fc78cb399 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/examples/zkapps/dex/run.ts b/src/examples/zkapps/dex/run.ts index baf2bc958..6dc730b6d 100644 --- a/src/examples/zkapps/dex/run.ts +++ b/src/examples/zkapps/dex/run.ts @@ -13,7 +13,10 @@ import { expect } from 'expect'; await isReady; let doProofs = false; -let Local = Mina.LocalBlockchain({ proofsEnabled: doProofs }); +let Local = Mina.LocalBlockchain({ + proofsEnabled: doProofs, + enforceTransactionLimits: false, +}); Mina.setActiveInstance(Local); let accountFee = Mina.accountCreationFee(); let [{ privateKey: feePayerKey }] = Local.testAccounts; @@ -37,7 +40,10 @@ await TokenContract.compile(); await main({ withVesting: false }); // swap out ledger so we can start fresh -Local = Mina.LocalBlockchain({ proofsEnabled: doProofs }); +Local = Mina.LocalBlockchain({ + proofsEnabled: doProofs, + enforceTransactionLimits: false, +}); Mina.setActiveInstance(Local); [{ privateKey: feePayerKey }] = Local.testAccounts; feePayerAddress = feePayerKey.toPublicKey(); diff --git a/src/examples/zkapps/voting/demo.ts b/src/examples/zkapps/voting/demo.ts index 4704dddf6..36b1fb078 100644 --- a/src/examples/zkapps/voting/demo.ts +++ b/src/examples/zkapps/voting/demo.ts @@ -20,6 +20,7 @@ import { let Local = Mina.LocalBlockchain({ proofsEnabled: false, + enforceTransactionLimits: false, }); Mina.setActiveInstance(Local); diff --git a/src/examples/zkapps/voting/deployContracts.ts b/src/examples/zkapps/voting/deployContracts.ts index d843259d5..5cde24c78 100644 --- a/src/examples/zkapps/voting/deployContracts.ts +++ b/src/examples/zkapps/voting/deployContracts.ts @@ -55,6 +55,7 @@ export async function deployContracts( }> { let Local = Mina.LocalBlockchain({ proofsEnabled, + enforceTransactionLimits: false, }); Mina.setActiveInstance(Local); @@ -119,6 +120,7 @@ export async function deployInvalidContracts( }> { let Local = Mina.LocalBlockchain({ proofsEnabled: false, + enforceTransactionLimits: false, }); Mina.setActiveInstance(Local); diff --git a/src/lib/mina.ts b/src/lib/mina.ts index ea4354422..82eddf2b1 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -49,6 +49,8 @@ export { fetchEvents, getActions, FeePayerSpec, + // for internal testing only + filterGroups, }; interface TransactionId { wait(): Promise; @@ -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) { @@ -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(); @@ -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, @@ -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) { @@ -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; + + // 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, + }; +} diff --git a/src/lib/mina.unit-test.ts b/src/lib/mina.unit-test.ts new file mode 100644 index 000000000..00c07d60a --- /dev/null +++ b/src/lib/mina.unit-test.ts @@ -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(); diff --git a/src/lib/token.test.ts b/src/lib/token.test.ts index df284c91d..20802816f 100644 --- a/src/lib/token.test.ts +++ b/src/lib/token.test.ts @@ -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;