-
Notifications
You must be signed in to change notification settings - Fork 102
/
transaction-validation.ts
350 lines (303 loc) · 11 KB
/
transaction-validation.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
/**
* This module holds the global Mina instance and its interface.
*/
import {
ZkappCommand,
TokenId,
Events,
ZkappPublicInput,
AccountUpdate,
dummySignature,
} from './account-update.js';
import { Field } from '../provable/wrapped.js';
import { UInt64, UInt32 } from '../provable/int.js';
import { PublicKey } from '../provable/crypto/signature.js';
import { JsonProof, verify } from '../proof-system/zkprogram.js';
import { verifyAccountUpdateSignature } from '../../mina-signer/src/sign-zkapp-command.js';
import { TransactionCost, TransactionLimits } from './constants.js';
import { cloneCircuitValue } from '../provable/types/struct.js';
import { assert } from '../provable/gadgets/common.js';
import { Types, TypesBigint } from '../../bindings/mina-transaction/types.js';
import type { NetworkId } from '../../mina-signer/src/types.js';
import type { Account } from './account.js';
import type { NetworkValue } from './precondition.js';
export {
reportGetAccountError,
defaultNetworkState,
verifyTransactionLimits,
verifyAccountUpdate,
filterGroups,
};
function reportGetAccountError(publicKey: string, tokenId: string) {
if (tokenId === TokenId.toBase58(TokenId.default)) {
return `getAccount: Could not find account for public key ${publicKey}`;
} else {
return `getAccount: Could not find account for public key ${publicKey} with the tokenId ${tokenId}`;
}
}
function defaultNetworkState(): NetworkValue {
let epochData: NetworkValue['stakingEpochData'] = {
ledger: { hash: Field(0), totalCurrency: UInt64.zero },
seed: Field(0),
startCheckpoint: Field(0),
lockCheckpoint: Field(0),
epochLength: UInt32.zero,
};
return {
snarkedLedgerHash: Field(0),
blockchainLength: UInt32.zero,
minWindowDensity: UInt32.zero,
totalCurrency: UInt64.zero,
globalSlotSinceGenesis: UInt32.zero,
stakingEpochData: epochData,
nextEpochData: cloneCircuitValue(epochData),
};
}
function verifyTransactionLimits({ accountUpdates }: ZkappCommand) {
let eventElements = { events: 0, actions: 0 };
let authKinds = accountUpdates.map((update) => {
eventElements.events += countEventElements(update.body.events);
eventElements.actions += countEventElements(update.body.actions);
let { isSigned, isProved, verificationKeyHash } =
update.body.authorizationKind;
return {
isSigned: isSigned.toBoolean(),
isProved: isProved.toBoolean(),
verificationKeyHash: verificationKeyHash.toString(),
};
});
// insert entry for the fee payer
authKinds.unshift({
isSigned: true,
isProved: false,
verificationKeyHash: '',
});
let authTypes = filterGroups(authKinds);
/*
np := proof
n2 := signedPair
n1 := signedSingle
formula used to calculate how expensive a zkapp transaction is
10.26*np + 10.08*n2 + 9.14*n1 < 69.45
*/
let totalTimeRequired =
TransactionCost.PROOF_COST * authTypes.proof +
TransactionCost.SIGNED_PAIR_COST * authTypes.signedPair +
TransactionCost.SIGNED_SINGLE_COST * authTypes.signedSingle;
let isWithinCostLimit = totalTimeRequired < TransactionCost.COST_LIMIT;
let isWithinEventsLimit =
eventElements.events <= TransactionLimits.MAX_EVENT_ELEMENTS;
let isWithinActionsLimit =
eventElements.actions <= TransactionLimits.MAX_ACTION_ELEMENTS;
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 account updates in your transaction are trying to emit too much event data. The maximum allowed number of field elements in events is ${TransactionLimits.MAX_EVENT_ELEMENTS}, but you tried to emit ${eventElements.events}.\n\n`;
}
if (!isWithinActionsLimit) {
error += `Error: The account updates in your transaction are trying to emit too much action data. The maximum allowed number of field elements in actions is ${TransactionLimits.MAX_ACTION_ELEMENTS}, but you tried to emit ${eventElements.actions}.\n\n`;
}
if (error) throw Error('Error during transaction sending:\n\n' + error);
}
function countEventElements({ data }: Events) {
return data.reduce((acc, ev) => acc + ev.length, 0);
}
function filterGroups(xs: AuthorizationKind[]) {
let pairs = filterPairs(xs);
xs = pairs.xs;
let singleCount = 0;
let proofCount = 0;
xs.forEach((t) => {
if (t.isProved) proofCount++;
else singleCount++;
});
return {
signedPair: pairs.pairs,
signedSingle: singleCount,
proof: proofCount,
};
}
async function verifyAccountUpdate(
account: Account,
accountUpdate: AccountUpdate,
publicInput: ZkappPublicInput,
transactionCommitments: { commitment: bigint; fullCommitment: bigint },
proofsEnabled: boolean,
networkId: NetworkId
): Promise<void> {
// check that that top-level updates have mayUseToken = No
// (equivalent check exists in the Mina node)
if (
accountUpdate.body.callDepth === 0 &&
!AccountUpdate.MayUseToken.isNo(accountUpdate).toBoolean()
) {
throw Error(
'Top-level account update can not use or pass on token permissions. Make sure that\n' +
'accountUpdate.body.mayUseToken = AccountUpdate.MayUseToken.No;'
);
}
let perm = account.permissions;
// check if addMissingSignatures failed to include a signature
// due to a missing private key
if (accountUpdate.authorization === dummySignature()) {
let pk = PublicKey.toBase58(accountUpdate.body.publicKey);
throw Error(
`verifyAccountUpdate: Detected a missing signature for (${pk}), private key was missing.`
);
}
// we are essentially only checking if the update is empty or an actual update
function includesChange<T extends {}>(
val: T | string | null | (string | null)[]
): boolean {
if (Array.isArray(val)) {
return !val.every((v) => v === null);
} else {
return val !== null;
}
}
function permissionForUpdate(key: string): Types.AuthRequired {
switch (key) {
case 'appState':
return perm.editState;
case 'delegate':
return perm.setDelegate;
case 'verificationKey':
return perm.setVerificationKey.auth;
case 'permissions':
return perm.setPermissions;
case 'zkappUri':
return perm.setZkappUri;
case 'tokenSymbol':
return perm.setTokenSymbol;
case 'timing':
return perm.setTiming;
case 'votingFor':
return perm.setVotingFor;
case 'actions':
return perm.editActionState;
case 'incrementNonce':
return perm.incrementNonce;
case 'send':
return perm.send;
case 'receive':
return perm.receive;
default:
throw Error(`Invalid permission for field ${key}: does not exist.`);
}
}
let accountUpdateJson = accountUpdate.toJSON();
const update = accountUpdateJson.body.update;
let errorTrace = '';
let isValidProof = false;
let isValidSignature = false;
// we don't check if proofs aren't enabled
if (!proofsEnabled) isValidProof = true;
if (accountUpdate.authorization.proof && proofsEnabled) {
try {
let publicInputFields = ZkappPublicInput.toFields(publicInput);
let proof: JsonProof = {
maxProofsVerified: 2,
proof: accountUpdate.authorization.proof!,
publicInput: publicInputFields.map((f) => f.toString()),
publicOutput: [],
};
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).stack;
isValidProof = false;
}
}
if (accountUpdate.authorization.signature) {
// checking permissions and authorization for each account update individually
try {
isValidSignature = verifyAccountUpdateSignature(
TypesBigint.AccountUpdate.fromJSON(accountUpdateJson),
transactionCommitments,
networkId
);
} catch (error) {
errorTrace += '\n\n' + (error as Error).stack;
isValidSignature = false;
}
}
let verified = false;
function checkPermission(p0: Types.AuthRequired, field: string) {
let p = Types.AuthRequired.toJSON(p0);
if (p === 'None') return;
if (p === 'Impossible') {
throw Error(
`Transaction verification failed: Cannot update field '${field}' because permission for this field is '${p}'`
);
}
if (p === 'Signature' || p === 'Either') {
verified ||= isValidSignature;
}
if (p === 'Proof' || p === 'Either') {
verified ||= isValidProof;
}
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 : ''}\n\n`
);
}
}
// goes through the update field on a transaction
Object.entries(update).forEach(([key, value]) => {
if (includesChange(value)) {
let p = permissionForUpdate(key);
checkPermission(p, key);
}
});
// checks the sequence events (which result in an updated sequence state)
if (accountUpdate.body.actions.data.length > 0) {
let p = permissionForUpdate('actions');
checkPermission(p, 'actions');
}
if (accountUpdate.body.incrementNonce.toBoolean()) {
let p = permissionForUpdate('incrementNonce');
checkPermission(p, 'incrementNonce');
}
// this checks for an edge case where an account update can be approved using proofs but
// a) the proof is invalid (bad verification key)
// and b) there are no state changes initiate so no permissions will be checked
// however, if the verification key changes, the proof should still be invalid
if (errorTrace && !verified) {
throw Error(
`One or more proofs were invalid and no other form of authorization was provided.\n${errorTrace}`
);
}
}
type AuthorizationKind = { isProved: boolean; isSigned: boolean };
const isPair = (a: AuthorizationKind, b: AuthorizationKind) =>
!a.isProved && !b.isProved;
function filterPairs(xs: AuthorizationKind[]): {
xs: { isProved: boolean; isSigned: boolean }[];
pairs: number;
} {
if (xs.length <= 1) return { xs, pairs: 0 };
if (isPair(xs[0], 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 };
}
}