From 6d6f52f303d26108504de547d3e449eb8cd7f5a2 Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 1 Dec 2022 16:19:00 +0100 Subject: [PATCH 01/10] impl party limit --- src/lib/mina.ts | 134 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/src/lib/mina.ts b/src/lib/mina.ts index ea4354422..b81d268ee 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -180,6 +180,7 @@ function createTransaction( let accountUpdates = currentTransaction.get().accountUpdates; CallForest.addCallers(accountUpdates); accountUpdates = CallForest.toFlatList(accountUpdates); + verifyTransactionLimits(accountUpdates); try { // check that on-chain values weren't used without setting a precondition for (let accountUpdate of accountUpdates) { @@ -1039,3 +1040,136 @@ 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 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[]): { + proof: number; + signedPair: number; + signedSingle: number; + } { + 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, + }; + } + + let eventElements: { + events: number; + sequence: number; + } = { + events: 0, + sequence: 0, + }; + + let authTypes: { + proof: number; + signedPair: number; + signedSingle: number; + }; + + 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 = ''; + console.log(eventElements); + console.log(authTypes); + + if (!isWithinCostLimit) { + error += `Error sending transaction: 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.\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( + 'An error occurred during transaction construction:\n\n' + error + ); +} From 09f0c9b7d67d0f7280b8ce3d52ba424de567d0cb Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 1 Dec 2022 18:41:28 +0100 Subject: [PATCH 02/10] rm debug log --- src/lib/mina.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib/mina.ts b/src/lib/mina.ts index b81d268ee..4dcbc35d1 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -1151,8 +1151,6 @@ function verifyTransactionLimits(accountUpdates: AccountUpdate[]) { eventElements['sequence'] <= maxSequenceEventElements; let error = ''; - console.log(eventElements); - console.log(authTypes); if (!isWithinCostLimit) { error += `Error sending transaction: The transaction is too expensive, try reducing the number of AccountUpdates that are attached to the transaction. From 882d23000decff4be6294d824a5ec59183f0466e Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 1 Dec 2022 18:43:13 +0100 Subject: [PATCH 03/10] todo comment --- src/lib/mina.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/mina.ts b/src/lib/mina.ts index 4dcbc35d1..427524591 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -1153,6 +1153,7 @@ function verifyTransactionLimits(accountUpdates: AccountUpdate[]) { 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 sending transaction: 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.\n\n`; From d3517f99af02798e29d57e5d4d5c53145824c56b Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 1 Dec 2022 19:29:53 +0100 Subject: [PATCH 04/10] move verifyTransactionLimits to tx.send --- src/lib/mina.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/lib/mina.ts b/src/lib/mina.ts index 427524591..7737f5c53 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -180,7 +180,7 @@ function createTransaction( let accountUpdates = currentTransaction.get().accountUpdates; CallForest.addCallers(accountUpdates); accountUpdates = CallForest.toFlatList(accountUpdates); - verifyTransactionLimits(accountUpdates); + try { // check that on-chain values weren't used without setting a precondition for (let accountUpdate of accountUpdates) { @@ -378,6 +378,8 @@ function LocalBlockchain({ JSON.stringify(zkappCommandToJson(txn.transaction)) ); + verifyTransactionLimits(txn.transaction.accountUpdates); + for (const update of txn.transaction.accountUpdates) { let account = ledger.getAccount( update.body.publicKey, @@ -618,6 +620,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) { @@ -1128,7 +1132,6 @@ function verifyTransactionLimits(accountUpdates: AccountUpdate[]) { return json.body.authorizationKind; }) ); - /* np := proof n2 := signedPair @@ -1154,9 +1157,11 @@ function verifyTransactionLimits(accountUpdates: AccountUpdate[]) { if (!isWithinCostLimit) { // TODO: we should add a link to the docs explaining the reasoning behind it once we have such an explainer - error += `Error sending transaction: The transaction is too expensive, try reducing the number of AccountUpdates that are attached to the transaction. + 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.\n\n`; +Certain layouts of AccountUpdates require more proving time than others, and therefore are too expensive. +${authTypes.proof} +\n\n`; } if (!isWithinEventsLimit) { @@ -1167,8 +1172,5 @@ Certain layouts of AccountUpdates require more proving time than others, and the 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( - 'An error occurred during transaction construction:\n\n' + error - ); + if (error) throw Error('Error during transaction sending:\n\n' + error); } From b024f222f1462cd39b47f37be9d0a7a7a8b20efe Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 1 Dec 2022 19:43:59 +0100 Subject: [PATCH 05/10] add types of updates to error msg --- src/lib/mina.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/mina.ts b/src/lib/mina.ts index 7737f5c53..18f8cc584 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -1160,7 +1160,8 @@ function verifyTransactionLimits(accountUpdates: AccountUpdate[]) { 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. -${authTypes.proof} + +${JSON.stringify(authTypes)} \n\n`; } From ddd61e1669d48ad2114dfc7881788c6624fb3eca Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 1 Dec 2022 21:23:48 +0100 Subject: [PATCH 06/10] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) 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 From 75587ea54ce969650194690e860e2bd89d067616 Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 1 Dec 2022 21:24:26 +0100 Subject: [PATCH 07/10] add flag to local blockchain and export helpers for tests --- src/lib/mina.ts | 123 ++++++++++++++++++++---------------------- src/lib/token.test.ts | 5 +- 2 files changed, 61 insertions(+), 67 deletions(-) diff --git a/src/lib/mina.ts b/src/lib/mina.ts index 18f8cc584..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; @@ -288,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(); @@ -378,7 +381,8 @@ function LocalBlockchain({ JSON.stringify(zkappCommandToJson(txn.transaction)) ); - verifyTransactionLimits(txn.transaction.accountUpdates); + if (enforceTransactionLimits) + verifyTransactionLimits(txn.transaction.accountUpdates); for (const update of txn.transaction.accountUpdates) { let account = ledger.getAccount( @@ -1056,75 +1060,12 @@ function verifyTransactionLimits(accountUpdates: AccountUpdate[]) { const maxSequenceEventElements = 16; const maxEventElements = 16; - 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[]): { - proof: number; - signedPair: number; - signedSingle: number; - } { - 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, - }; - } - - let eventElements: { - events: number; - sequence: number; - } = { + let eventElements = { events: 0, sequence: 0, }; - let authTypes: { - proof: number; - signedPair: number; - signedSingle: number; - }; - - authTypes = filterGroups( + let authTypes = filterGroups( accountUpdates.map((update) => { let json = update.toJSON(); eventElements.events += update.body.events.data.length; @@ -1175,3 +1116,53 @@ ${JSON.stringify(authTypes)} 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/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; From 9e11eea35691487009a1e610c759b3b7af0f33c7 Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 1 Dec 2022 21:25:48 +0100 Subject: [PATCH 08/10] tests --- src/lib/mina.unit-test.ts | 102 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/lib/mina.unit-test.ts diff --git a/src/lib/mina.unit-test.ts b/src/lib/mina.unit-test.ts new file mode 100644 index 000000000..e30e88912 --- /dev/null +++ b/src/lib/mina.unit-test.ts @@ -0,0 +1,102 @@ +import { filterGroups } from './mina.js'; +import { expect } from 'expect'; + +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, +}); From 76135e9bce66f401f5f4322fa13d594b9d33e3bf Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 2 Dec 2022 01:05:25 +0100 Subject: [PATCH 09/10] fix ci --- src/lib/mina.unit-test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/mina.unit-test.ts b/src/lib/mina.unit-test.ts index e30e88912..00c07d60a 100644 --- a/src/lib/mina.unit-test.ts +++ b/src/lib/mina.unit-test.ts @@ -1,5 +1,6 @@ import { filterGroups } from './mina.js'; import { expect } from 'expect'; +import { shutdown } from '../index.js'; let S = 'Signature'; let N = 'None_given'; @@ -100,3 +101,4 @@ expect(filterGroups([S, S])).toEqual({ signedPair: 1, signedSingle: 0, }); +shutdown(); From 837ca2f194916b81306315aa30b198199a108370 Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 2 Dec 2022 11:29:53 +0100 Subject: [PATCH 10/10] fix tests --- src/examples/zkapps/dex/run.ts | 10 ++++++++-- src/examples/zkapps/voting/demo.ts | 1 + src/examples/zkapps/voting/deployContracts.ts | 2 ++ 3 files changed, 11 insertions(+), 2 deletions(-) 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);