@corpus-core/colibri-stateless 2.0.5 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/runtime.d.ts CHANGED
@@ -65,6 +65,7 @@ export interface C4Runtime {
65
65
  rpcCtxSetWitnessKeys(ctx: CtxHandle, keys: string): void;
66
66
  rpcCtxSetMinLatestBlockTs(ctx: CtxHandle, ts: bigint): void;
67
67
  setCheckpoint(chainId: bigint, checkpoint: string): void;
68
+ resetCaches(): void;
68
69
  reqSetResponse(req: DataRequest, data: Uint8Array, nodeIndex: number): void;
69
70
  reqSetError(req: DataRequest, error: string, nodeIndex: number): void;
70
71
  decodeProof(proof: Uint8Array): any;
package/runtime_node.js CHANGED
@@ -69,6 +69,7 @@ function createNativeRuntime(binding) {
69
69
  rpcCtxSetWitnessKeys: binding.rpcCtxSetWitnessKeys,
70
70
  rpcCtxSetMinLatestBlockTs: binding.rpcCtxSetMinLatestBlockTs,
71
71
  setCheckpoint: binding.setCheckpoint,
72
+ resetCaches: binding.resetCaches,
72
73
  reqSetResponse: (req, data, nodeIndex) => binding.reqSetResponse(String(req.req_ptr), data, nodeIndex),
73
74
  reqSetError: (req, error, nodeIndex) => binding.reqSetError(String(req.req_ptr), error, nodeIndex),
74
75
  decodeProof(proof) {
package/runtime_wasm.js CHANGED
@@ -94,6 +94,9 @@ function createRuntime(c4w) {
94
94
  c4w._c4w_set_checkpoint(chainId, as_char_ptr(checkpoint, c4w, free_buffers));
95
95
  free_buffers.forEach(ptr => c4w._free(ptr));
96
96
  },
97
+ resetCaches() {
98
+ c4w._c4w_reset_caches();
99
+ },
97
100
  reqSetResponse(req, data, nodeIndex) {
98
101
  // ownership of the copied bytes is transferred to the C context
99
102
  c4w._c4w_req_set_response(req.req_ptr, copy_to_c(data, c4w), data.length, nodeIndex);
@@ -22,13 +22,18 @@
22
22
  */
23
23
  import { MethodType as C4MethodType } from './types.js';
24
24
  /**
25
- * Transaction Verifier - Sichere Verifikation von eth_sendTransaction
25
+ * Transaction Verifier — defence against a compromised `eth_signTransaction` provider.
26
26
  *
27
- * Implementiert eine Sicherheitsschicht gegen NPM Supply-Chain-Attacks:
28
- * 1. Transaction über fallbackProvider signieren lassen
29
- * 2. Signierte Transaction mit colibri_decodeTransaction dekodieren
30
- * 3. Dekodierte Parameter mit Original vergleichen
31
- * 4. Bei Übereinstimmung: eth_sendRawTransaction ausführen
27
+ * Flow:
28
+ * 1. Ask `fallback_provider` to sign the intended tx object
29
+ * 2. Decode the returned raw tx with `colibri_decodeTransaction` (local)
30
+ * 3. Compare decoded fields with the original intent (fail-closed)
31
+ * 4. Broadcast only if they match
32
+ *
33
+ * `colibri_decodeTransaction` returns `EthTxData` JSON (`input`, not `data`).
34
+ * Semantic fields omitted by the dapp default to empty/zero; the signer must
35
+ * not fill them in. Wallet-estimated fields (`nonce`, `gas`, fees) are compared
36
+ * only when the dapp set them.
32
37
  */
33
38
  export interface TransactionVerifierConfig {
34
39
  debug?: boolean;
@@ -36,6 +41,15 @@ export interface TransactionVerifierConfig {
36
41
  fallback_provider?: any;
37
42
  rpcs: string[];
38
43
  }
44
+ /**
45
+ * Compare an `eth_sendTransaction` object with the JSON from `colibri_decodeTransaction`.
46
+ *
47
+ * @param original Intended tx object from the dapp
48
+ * @param decoded Decoded signed tx (`EthTxData`: `input`, hex quantities, `to` null if create)
49
+ * @param config Must include the expected `chainId`
50
+ * @return `true` only if the signed tx matches the intent
51
+ */
52
+ export declare function compareTransactionParameters(original: any, decoded: any, config: TransactionVerifierConfig): boolean;
39
53
  export declare class TransactionVerifier {
40
54
  /**
41
55
  * Verifizierte Ausführung von eth_sendTransaction
@@ -45,14 +59,6 @@ export declare class TransactionVerifier {
45
59
  * 4. eth_sendRawTransaction ausführen
46
60
  */
47
61
  static verifyAndSendTransaction(txObject: any, config: TransactionVerifierConfig, rpcMethod: (method: string, args: any[], method_type?: C4MethodType) => Promise<any>, fetchRpc: (urls: string[], payload: any, as_proof?: boolean, fetchFn?: typeof globalThis.fetch) => Promise<any>, fetchFn?: typeof globalThis.fetch): Promise<string>;
48
- /**
49
- * Vergleicht die Parameter einer ursprünglichen Transaction mit einer dekodierten signierten Transaction
50
- */
51
- private static compareTransactionParameters;
52
- /**
53
- * Normalisiert Hex-Werte für Vergleiche (entfernt führende Nullen, stellt 0x-Prefix sicher)
54
- */
55
- private static normalizeHexValue;
56
62
  }
57
63
  /**
58
64
  * Prototype Protection - Schutz vor NPM Supply-Chain-Attacks
@@ -90,5 +96,6 @@ declare const _default: {
90
96
  TransactionVerifier: typeof TransactionVerifier;
91
97
  PrototypeProtection: typeof PrototypeProtection;
92
98
  TransactionUtils: typeof TransactionUtils;
99
+ compareTransactionParameters: typeof compareTransactionParameters;
93
100
  };
94
101
  export default _default;
@@ -21,6 +21,464 @@
21
21
  * SPDX-License-Identifier: MIT
22
22
  */
23
23
  import { ProviderRpcError, MethodType as C4MethodType } from './types.js';
24
+ function getOwn(obj, key) {
25
+ if (obj == null || typeof obj !== 'object')
26
+ return { present: false, value: undefined };
27
+ if (!Object.prototype.hasOwnProperty.call(obj, key))
28
+ return { present: false, value: undefined };
29
+ return { present: true, value: obj[key] };
30
+ }
31
+ /** Resolve RPC aliases. If several names are present they must parse to the same value. */
32
+ function resolveAliased(obj, names, parse) {
33
+ let provided = false;
34
+ let canonical;
35
+ for (const name of names) {
36
+ const field = getOwn(obj, name);
37
+ if (!field.present || field.value === undefined)
38
+ continue;
39
+ const parsed = parse(field.value);
40
+ if (!parsed.ok)
41
+ return { ok: false };
42
+ if (!provided) {
43
+ provided = true;
44
+ canonical = parsed.value;
45
+ }
46
+ else if (canonical !== parsed.value) {
47
+ return { ok: false };
48
+ }
49
+ }
50
+ return { ok: true, value: { provided, value: canonical } };
51
+ }
52
+ function parseQuantity(value) {
53
+ if (value === undefined || value === null || value === '')
54
+ return { ok: true, value: 0n };
55
+ if (typeof value === 'boolean' || typeof value === 'object')
56
+ return { ok: false };
57
+ if (typeof value === 'bigint')
58
+ return value < 0n ? { ok: false } : { ok: true, value };
59
+ if (typeof value === 'number') {
60
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0)
61
+ return { ok: false };
62
+ return { ok: true, value: BigInt(value) };
63
+ }
64
+ const str = String(value).trim();
65
+ if (str === '' || str === '0x' || str === '0X')
66
+ return { ok: true, value: 0n };
67
+ if (/^0x[0-9a-fA-F]+$/.test(str))
68
+ return { ok: true, value: BigInt(str) };
69
+ if (/^[0-9]+$/.test(str))
70
+ return { ok: true, value: BigInt(str) };
71
+ return { ok: false };
72
+ }
73
+ /** 20-byte address, left-padded. `null`/`''`/`0x` → contract creation (`null`). */
74
+ function parseAddress(value) {
75
+ if (value === undefined || value === null || value === '')
76
+ return { ok: true, value: null };
77
+ if (typeof value === 'boolean' || typeof value === 'object')
78
+ return { ok: false };
79
+ let str = String(value).trim();
80
+ if (str === '0x' || str === '0X')
81
+ return { ok: true, value: null };
82
+ if (str.startsWith('0x') || str.startsWith('0X'))
83
+ str = str.slice(2);
84
+ if (!/^[0-9a-fA-F]+$/.test(str) || str.length > 40)
85
+ return { ok: false };
86
+ if (str.length % 2 === 1)
87
+ str = '0' + str;
88
+ return { ok: true, value: '0x' + str.toLowerCase().padStart(40, '0') };
89
+ }
90
+ function parseHexPayload(value) {
91
+ if (value === undefined || value === null || value === '')
92
+ return { ok: true, value: '' };
93
+ if (value instanceof Uint8Array) {
94
+ let hex = '';
95
+ for (let i = 0; i < value.length; i++)
96
+ hex += value[i].toString(16).padStart(2, '0');
97
+ return { ok: true, value: hex };
98
+ }
99
+ if (typeof value === 'boolean' || typeof value === 'object')
100
+ return { ok: false };
101
+ let str = String(value).trim();
102
+ if (str.startsWith('0x') || str.startsWith('0X'))
103
+ str = str.slice(2);
104
+ if (str === '')
105
+ return { ok: true, value: '' };
106
+ if (!/^[0-9a-fA-F]+$/.test(str))
107
+ return { ok: false };
108
+ return { ok: true, value: str.toLowerCase() };
109
+ }
110
+ /** Exact calldata bytes. Leading zeros are significant (`0x` ≠ `0x00`). Odd nibble length is invalid. */
111
+ function parseBytes(value) {
112
+ const hex = parseHexPayload(value);
113
+ if (!hex.ok)
114
+ return hex;
115
+ if (hex.value.length % 2 === 1)
116
+ return { ok: false };
117
+ return { ok: true, value: '0x' + hex.value };
118
+ }
119
+ function parseHash32(value) {
120
+ const hex = parseHexPayload(value);
121
+ if (!hex.ok)
122
+ return hex;
123
+ if (hex.value.length > 64)
124
+ return { ok: false };
125
+ return { ok: true, value: '0x' + hex.value.padStart(64, '0') };
126
+ }
127
+ function parseYParity(value) {
128
+ const q = parseQuantity(value);
129
+ if (!q.ok)
130
+ return q;
131
+ // Accept v = 27/28 as well as yParity = 0/1.
132
+ if (q.value === 27n || q.value === 28n)
133
+ return { ok: true, value: q.value - 27n };
134
+ if (q.value !== 0n && q.value !== 1n)
135
+ return { ok: false };
136
+ return q;
137
+ }
138
+ function warn(config, message) {
139
+ if (config.debug)
140
+ console.warn(`[TransactionVerifier] ${message}`);
141
+ }
142
+ function quantitiesEqual(originalVal, decodedVal, defaultIfOriginalOmitted) {
143
+ const decoded = decodedVal === undefined ? { ok: true, value: 0n } : parseQuantity(decodedVal);
144
+ if (!decoded.ok)
145
+ return 'invalid';
146
+ if (originalVal === undefined) {
147
+ if (defaultIfOriginalOmitted === 'skip')
148
+ return true;
149
+ return decoded.value === 0n;
150
+ }
151
+ const original = parseQuantity(originalVal);
152
+ if (!original.ok)
153
+ return 'invalid';
154
+ return original.value === decoded.value;
155
+ }
156
+ function compareAccessList(original, decoded, config) {
157
+ const origField = getOwn(original, 'accessList');
158
+ const decField = getOwn(decoded, 'accessList');
159
+ const origList = origField.present && origField.value != null ? origField.value : [];
160
+ const decList = decField.present && decField.value != null ? decField.value : [];
161
+ if (!Array.isArray(origList) || !Array.isArray(decList)) {
162
+ warn(config, 'accessList is not an array');
163
+ return false;
164
+ }
165
+ if (origList.length !== decList.length) {
166
+ warn(config, `accessList length mismatch: original=${origList.length}, decoded=${decList.length}`);
167
+ return false;
168
+ }
169
+ for (let i = 0; i < origList.length; i++) {
170
+ const oEntry = origList[i];
171
+ const dEntry = decList[i];
172
+ if (oEntry == null || typeof oEntry !== 'object' || dEntry == null || typeof dEntry !== 'object') {
173
+ warn(config, `accessList[${i}] is not an object`);
174
+ return false;
175
+ }
176
+ const oa = parseAddress(getOwn(oEntry, 'address').value);
177
+ const da = parseAddress(getOwn(dEntry, 'address').value);
178
+ if (!oa.ok || !da.ok || oa.value === null || da.value === null || oa.value !== da.value) {
179
+ warn(config, `accessList[${i}].address mismatch`);
180
+ return false;
181
+ }
182
+ const oKeysField = getOwn(oEntry, 'storageKeys');
183
+ const dKeysField = getOwn(dEntry, 'storageKeys');
184
+ const oKeys = oKeysField.present && oKeysField.value != null ? oKeysField.value : [];
185
+ const dKeys = dKeysField.present && dKeysField.value != null ? dKeysField.value : [];
186
+ if (!Array.isArray(oKeys) || !Array.isArray(dKeys) || oKeys.length !== dKeys.length) {
187
+ warn(config, `accessList[${i}].storageKeys mismatch`);
188
+ return false;
189
+ }
190
+ for (let k = 0; k < oKeys.length; k++) {
191
+ const okh = parseHash32(oKeys[k]);
192
+ const dkh = parseHash32(dKeys[k]);
193
+ if (!okh.ok || !dkh.ok || okh.value !== dkh.value) {
194
+ warn(config, `accessList[${i}].storageKeys[${k}] mismatch`);
195
+ return false;
196
+ }
197
+ }
198
+ }
199
+ return true;
200
+ }
201
+ function compareAuthorizationList(original, decoded, config) {
202
+ const origField = getOwn(original, 'authorizationList');
203
+ const decField = getOwn(decoded, 'authorizationList');
204
+ const origList = origField.present && origField.value != null ? origField.value : [];
205
+ const decList = decField.present && decField.value != null ? decField.value : [];
206
+ if (!Array.isArray(origList) || !Array.isArray(decList)) {
207
+ warn(config, 'authorizationList is not an array');
208
+ return false;
209
+ }
210
+ if (origList.length !== decList.length) {
211
+ warn(config, `authorizationList length mismatch: original=${origList.length}, decoded=${decList.length}`);
212
+ return false;
213
+ }
214
+ for (let i = 0; i < origList.length; i++) {
215
+ const o = origList[i];
216
+ const d = decList[i];
217
+ if (o == null || typeof o !== 'object' || d == null || typeof d !== 'object') {
218
+ warn(config, `authorizationList[${i}] is not an object`);
219
+ return false;
220
+ }
221
+ const oa = parseAddress(getOwn(o, 'address').value);
222
+ const da = parseAddress(getOwn(d, 'address').value);
223
+ if (!oa.ok || !da.ok || oa.value === null || da.value === null || oa.value !== da.value) {
224
+ warn(config, `authorizationList[${i}].address mismatch`);
225
+ return false;
226
+ }
227
+ const origChain = getOwn(o, 'chainId');
228
+ const expectedChain = origChain.present && origChain.value !== undefined ? origChain.value : config.chainId;
229
+ const chainEq = quantitiesEqual(expectedChain, getOwn(d, 'chainId').value, 'skip');
230
+ if (chainEq !== true) {
231
+ warn(config, `authorizationList[${i}].chainId mismatch`);
232
+ return false;
233
+ }
234
+ const origNonce = getOwn(o, 'nonce');
235
+ if (origNonce.present && origNonce.value !== undefined) {
236
+ const nonceEq = quantitiesEqual(origNonce.value, getOwn(d, 'nonce').value, 'skip');
237
+ if (nonceEq !== true) {
238
+ warn(config, `authorizationList[${i}].nonce mismatch`);
239
+ return false;
240
+ }
241
+ }
242
+ const origHasSig = (getOwn(o, 'r').present && getOwn(o, 'r').value !== undefined) ||
243
+ (getOwn(o, 's').present && getOwn(o, 's').value !== undefined) ||
244
+ (getOwn(o, 'yParity').present && getOwn(o, 'yParity').value !== undefined) ||
245
+ (getOwn(o, 'v').present && getOwn(o, 'v').value !== undefined);
246
+ if (origHasSig) {
247
+ const or_ = parseHash32(getOwn(o, 'r').value);
248
+ const dr = parseHash32(getOwn(d, 'r').value);
249
+ const os = parseHash32(getOwn(o, 's').value);
250
+ const ds = parseHash32(getOwn(d, 's').value);
251
+ if (!or_.ok || !dr.ok || or_.value !== dr.value || !os.ok || !ds.ok || os.value !== ds.value) {
252
+ warn(config, `authorizationList[${i}] signature mismatch`);
253
+ return false;
254
+ }
255
+ const origY = getOwn(o, 'yParity').present ? getOwn(o, 'yParity').value : getOwn(o, 'v').value;
256
+ const decY = getOwn(d, 'yParity').present ? getOwn(d, 'yParity').value : getOwn(d, 'v').value;
257
+ const oy = parseYParity(origY);
258
+ const dy = parseYParity(decY);
259
+ if (!oy.ok || !dy.ok || oy.value !== dy.value) {
260
+ warn(config, `authorizationList[${i}].yParity mismatch`);
261
+ return false;
262
+ }
263
+ }
264
+ }
265
+ return true;
266
+ }
267
+ function compareHashList(originalVal, decodedVal, name, config) {
268
+ const origList = originalVal === undefined || originalVal === null ? [] : originalVal;
269
+ const decList = decodedVal === undefined || decodedVal === null ? [] : decodedVal;
270
+ if (!Array.isArray(origList) || !Array.isArray(decList)) {
271
+ warn(config, `${name} is not an array`);
272
+ return false;
273
+ }
274
+ if (origList.length !== decList.length) {
275
+ warn(config, `${name} length mismatch: original=${origList.length}, decoded=${decList.length}`);
276
+ return false;
277
+ }
278
+ for (let i = 0; i < origList.length; i++) {
279
+ const oh = parseHash32(origList[i]);
280
+ const dh = parseHash32(decList[i]);
281
+ if (!oh.ok || !dh.ok || oh.value !== dh.value) {
282
+ warn(config, `${name}[${i}] mismatch`);
283
+ return false;
284
+ }
285
+ }
286
+ return true;
287
+ }
288
+ /**
289
+ * Compare an `eth_sendTransaction` object with the JSON from `colibri_decodeTransaction`.
290
+ *
291
+ * @param original Intended tx object from the dapp
292
+ * @param decoded Decoded signed tx (`EthTxData`: `input`, hex quantities, `to` null if create)
293
+ * @param config Must include the expected `chainId`
294
+ * @return `true` only if the signed tx matches the intent
295
+ */
296
+ export function compareTransactionParameters(original, decoded, config) {
297
+ if (config.debug) {
298
+ console.log('[TransactionVerifier] Comparing transaction parameters:');
299
+ console.log('Original:', original);
300
+ console.log('Decoded:', decoded);
301
+ }
302
+ if (original == null || typeof original !== 'object' || decoded == null || typeof decoded !== 'object') {
303
+ warn(config, 'original and decoded must be objects');
304
+ return false;
305
+ }
306
+ const chainId = parseQuantity(config.chainId);
307
+ if (!chainId.ok) {
308
+ warn(config, 'config.chainId is invalid');
309
+ return false;
310
+ }
311
+ const decodedChainIdField = getOwn(decoded, 'chainId');
312
+ if (!decodedChainIdField.present) {
313
+ warn(config, 'signed transaction has no chainId (pre-EIP-155 replay risk)');
314
+ return false;
315
+ }
316
+ const decodedChainId = parseQuantity(decodedChainIdField.value);
317
+ if (!decodedChainId.ok || decodedChainId.value !== chainId.value) {
318
+ warn(config, `ChainId mismatch: expected=${config.chainId}, decoded=${decodedChainIdField.value}`);
319
+ return false;
320
+ }
321
+ const originalChainId = getOwn(original, 'chainId');
322
+ if (originalChainId.present && originalChainId.value !== undefined) {
323
+ const parsed = parseQuantity(originalChainId.value);
324
+ if (!parsed.ok || parsed.value !== chainId.value) {
325
+ warn(config, 'original.chainId does not match config.chainId');
326
+ return false;
327
+ }
328
+ }
329
+ // Decoder maps pre-EIP-155 legacy v=27/28 to chainId 1, so a missing-chainId
330
+ // check is not enough on mainnet. EIP-155 requires v >= 35.
331
+ const decodedType = getOwn(decoded, 'type');
332
+ const typeVal = decodedType.present ? parseQuantity(decodedType.value) : { ok: true, value: 0n };
333
+ if (!typeVal.ok) {
334
+ warn(config, 'invalid decoded.type');
335
+ return false;
336
+ }
337
+ if (typeVal.value === 0n) {
338
+ const vField = getOwn(decoded, 'v');
339
+ const v = vField.present ? parseQuantity(vField.value) : { ok: false };
340
+ if (!v.ok || v.value < 35n) {
341
+ warn(config, 'unprotected pre-EIP-155 signature');
342
+ return false;
343
+ }
344
+ }
345
+ const origTo = resolveAliased(original, ['to'], parseAddress);
346
+ const decTo = resolveAliased(decoded, ['to'], parseAddress);
347
+ if (!origTo.ok || !decTo.ok) {
348
+ warn(config, 'invalid to address');
349
+ return false;
350
+ }
351
+ const expectedTo = origTo.value.provided ? origTo.value.value : null;
352
+ const actualTo = decTo.value.provided ? decTo.value.value : null;
353
+ if (expectedTo !== actualTo) {
354
+ warn(config, `to mismatch: original='${expectedTo}', decoded='${actualTo}'`);
355
+ return false;
356
+ }
357
+ const origFrom = resolveAliased(original, ['from'], parseAddress);
358
+ const decFrom = resolveAliased(decoded, ['from'], parseAddress);
359
+ if (!origFrom.ok || !decFrom.ok) {
360
+ warn(config, 'invalid from address');
361
+ return false;
362
+ }
363
+ if (origFrom.value.provided && origFrom.value.value !== (decFrom.value.provided ? decFrom.value.value : null)) {
364
+ warn(config, `from mismatch: original='${origFrom.value.value}', decoded='${decFrom.value.value}'`);
365
+ return false;
366
+ }
367
+ const origValue = getOwn(original, 'value');
368
+ const valueEq = quantitiesEqual(origValue.present ? origValue.value : undefined, getOwn(decoded, 'value').value, 'zero');
369
+ if (valueEq !== true) {
370
+ warn(config, `value mismatch: original='${origValue.value}', decoded='${decoded.value}'`);
371
+ return false;
372
+ }
373
+ const origData = resolveAliased(original, ['data', 'input'], parseBytes);
374
+ const decData = resolveAliased(decoded, ['input', 'data'], parseBytes);
375
+ if (!origData.ok || !decData.ok) {
376
+ warn(config, 'invalid calldata (data/input)');
377
+ return false;
378
+ }
379
+ const expectedData = origData.value.provided ? origData.value.value : '0x';
380
+ const actualData = decData.value.provided ? decData.value.value : '0x';
381
+ if (expectedData !== actualData) {
382
+ warn(config, `data/input mismatch: original='${expectedData}', decoded='${actualData}'`);
383
+ return false;
384
+ }
385
+ const origType = getOwn(original, 'type');
386
+ if (origType.present && origType.value !== undefined) {
387
+ const typeEq = quantitiesEqual(origType.value, decoded.type, 'skip');
388
+ if (typeEq !== true) {
389
+ warn(config, `type mismatch: original='${origType.value}', decoded='${decoded.type}'`);
390
+ return false;
391
+ }
392
+ }
393
+ const origNonce = getOwn(original, 'nonce');
394
+ if (origNonce.present && origNonce.value !== undefined) {
395
+ const nonceEq = quantitiesEqual(origNonce.value, decoded.nonce, 'skip');
396
+ if (nonceEq !== true) {
397
+ warn(config, `nonce mismatch: original='${origNonce.value}', decoded='${decoded.nonce}'`);
398
+ return false;
399
+ }
400
+ }
401
+ const origGas = resolveAliased(original, ['gas', 'gasLimit'], parseQuantity);
402
+ if (!origGas.ok) {
403
+ warn(config, 'invalid gas/gasLimit');
404
+ return false;
405
+ }
406
+ if (origGas.value.provided) {
407
+ const gasEq = quantitiesEqual(origGas.value.value, decoded.gas, 'skip');
408
+ if (gasEq !== true) {
409
+ warn(config, `gas mismatch: original='${origGas.value.value}', decoded='${decoded.gas}'`);
410
+ return false;
411
+ }
412
+ }
413
+ const origMaxFee = getOwn(original, 'maxFeePerGas');
414
+ const origPrio = getOwn(original, 'maxPriorityFeePerGas');
415
+ const origGasPrice = getOwn(original, 'gasPrice');
416
+ const isEip1559 = origMaxFee.present || origPrio.present;
417
+ if (origMaxFee.present && origMaxFee.value !== undefined) {
418
+ const eq = quantitiesEqual(origMaxFee.value, decoded.maxFeePerGas, 'skip');
419
+ if (eq !== true) {
420
+ warn(config, `maxFeePerGas mismatch`);
421
+ return false;
422
+ }
423
+ }
424
+ if (origPrio.present && origPrio.value !== undefined) {
425
+ const eq = quantitiesEqual(origPrio.value, decoded.maxPriorityFeePerGas, 'skip');
426
+ if (eq !== true) {
427
+ warn(config, `maxPriorityFeePerGas mismatch`);
428
+ return false;
429
+ }
430
+ }
431
+ // Decoded EIP-1559 txs still carry an *effective* gasPrice; do not compare it.
432
+ if (origGasPrice.present && origGasPrice.value !== undefined && !isEip1559) {
433
+ const eq = quantitiesEqual(origGasPrice.value, decoded.gasPrice, 'skip');
434
+ if (eq !== true) {
435
+ warn(config, `gasPrice mismatch`);
436
+ return false;
437
+ }
438
+ }
439
+ if (!compareAccessList(original, decoded, config))
440
+ return false;
441
+ if (!compareAuthorizationList(original, decoded, config))
442
+ return false;
443
+ const origBlobs = getOwn(original, 'blobVersionedHashes');
444
+ const decBlobs = getOwn(decoded, 'blobVersionedHashes');
445
+ if (!compareHashList(origBlobs.present ? origBlobs.value : [], decBlobs.present ? decBlobs.value : [], 'blobVersionedHashes', config))
446
+ return false;
447
+ const origBlobFee = getOwn(original, 'maxFeePerBlobGas');
448
+ if (origBlobFee.present && origBlobFee.value !== undefined) {
449
+ const eq = quantitiesEqual(origBlobFee.value, decoded.maxFeePerBlobGas, 'skip');
450
+ if (eq !== true) {
451
+ warn(config, `maxFeePerBlobGas mismatch`);
452
+ return false;
453
+ }
454
+ }
455
+ const origMint = getOwn(original, 'mint');
456
+ const mintEq = quantitiesEqual(origMint.present ? origMint.value : undefined, getOwn(decoded, 'mint').present ? decoded.mint : 0n, 'zero');
457
+ if (mintEq !== true) {
458
+ warn(config, 'unexpected mint field on signed transaction');
459
+ return false;
460
+ }
461
+ const origSource = getOwn(original, 'sourceHash');
462
+ const decSource = getOwn(decoded, 'sourceHash');
463
+ if (decSource.present) {
464
+ const expected = origSource.present ? origSource.value : '0x' + '00'.repeat(32);
465
+ const oh = parseHash32(expected);
466
+ const dh = parseHash32(decSource.value);
467
+ if (!oh.ok || !dh.ok || oh.value !== dh.value) {
468
+ warn(config, 'unexpected sourceHash on signed transaction');
469
+ return false;
470
+ }
471
+ }
472
+ const origSys = getOwn(original, 'isSystemTx');
473
+ const decSys = getOwn(decoded, 'isSystemTx');
474
+ const expectedSys = origSys.present ? Boolean(origSys.value) : false;
475
+ const actualSys = decSys.present ? Boolean(decSys.value) : false;
476
+ if (expectedSys !== actualSys) {
477
+ warn(config, 'unexpected isSystemTx on signed transaction');
478
+ return false;
479
+ }
480
+ return true;
481
+ }
24
482
  export class TransactionVerifier {
25
483
  /**
26
484
  * Verifizierte Ausführung von eth_sendTransaction
@@ -44,7 +502,7 @@ export class TransactionVerifier {
44
502
  // 2. Signierte Transaction dekodieren
45
503
  const decodedTx = await rpcMethod('colibri_decodeTransaction', [signedRawTx], C4MethodType.LOCAL);
46
504
  // 3. Dekodierte Parameter mit Original vergleichen
47
- const isValid = this.compareTransactionParameters(txObject, decodedTx, config);
505
+ const isValid = compareTransactionParameters(txObject, decodedTx, config);
48
506
  if (!isValid) {
49
507
  throw new ProviderRpcError(4201, 'Signed transaction does not match original parameters');
50
508
  }
@@ -60,55 +518,6 @@ export class TransactionVerifier {
60
518
  throw new ProviderRpcError(4202, `Transaction verification failed: ${error.message}`);
61
519
  }
62
520
  }
63
- /**
64
- * Vergleicht die Parameter einer ursprünglichen Transaction mit einer dekodierten signierten Transaction
65
- */
66
- static compareTransactionParameters(original, decoded, config) {
67
- if (config.debug) {
68
- console.log('[TransactionVerifier] Comparing transaction parameters:');
69
- console.log('Original:', original);
70
- console.log('Decoded:', decoded);
71
- }
72
- // Kritische Parameter vergleichen
73
- const criticalFields = ['to', 'value', 'data', 'gas', 'gasPrice', 'maxFeePerGas', 'maxPriorityFeePerGas', 'nonce'];
74
- for (const field of criticalFields) {
75
- if (original[field] !== undefined) {
76
- // Normalisierung für Hex-Werte (0x-Prefix, Leading Zeros)
77
- const originalValue = this.normalizeHexValue(original[field]);
78
- const decodedValue = this.normalizeHexValue(decoded[field]);
79
- if (originalValue !== decodedValue) {
80
- if (config.debug) {
81
- console.warn(`[TransactionVerifier] Transaction parameter mismatch in '${field}': original='${originalValue}', decoded='${decodedValue}'`);
82
- }
83
- return false;
84
- }
85
- }
86
- }
87
- // ChainId prüfen
88
- if (decoded.chainId && parseInt(decoded.chainId, 16) !== parseInt(config.chainId)) {
89
- if (config.debug) {
90
- console.warn(`[TransactionVerifier] ChainId mismatch: expected=${config.chainId}, decoded=${parseInt(decoded.chainId, 16)}`);
91
- }
92
- return false;
93
- }
94
- return true;
95
- }
96
- /**
97
- * Normalisiert Hex-Werte für Vergleiche (entfernt führende Nullen, stellt 0x-Prefix sicher)
98
- */
99
- static normalizeHexValue(value) {
100
- if (value === undefined || value === null)
101
- return '';
102
- if (typeof value === 'number')
103
- return '0x' + value.toString(16);
104
- const str = value.toString();
105
- if (!str.startsWith('0x'))
106
- return '0x' + str;
107
- // Führende Nullen entfernen, aber mindestens eine Ziffer behalten
108
- const withoutPrefix = str.slice(2);
109
- const normalized = withoutPrefix.replace(/^0+/, '') || '0';
110
- return '0x' + normalized;
111
- }
112
521
  }
113
522
  /**
114
523
  * Prototype Protection - Schutz vor NPM Supply-Chain-Attacks
@@ -192,5 +601,6 @@ export class TransactionUtils {
192
601
  export default {
193
602
  TransactionVerifier,
194
603
  PrototypeProtection,
195
- TransactionUtils
604
+ TransactionUtils,
605
+ compareTransactionParameters
196
606
  };
package/types.d.ts CHANGED
@@ -102,6 +102,14 @@ export interface Config extends ChainConfig {
102
102
  * the WSP. Default: false.
103
103
  */
104
104
  skip_wsp_check?: boolean;
105
+ /**
106
+ * If true, each `rpc()` reloads the in-process EL header cache and header
107
+ * tags from storage at start and writes them back on cleanup
108
+ * (`VERIFY_FLAG_PERSIST_HEADER_CACHE`, bit `1 << 10`). Intended for
109
+ * integration-test recording/replay so prover `last_block_hash` matches
110
+ * the dumped storage snapshot. Default: false.
111
+ */
112
+ persist_header_cache?: boolean;
105
113
  /**
106
114
  * Maximum age (in seconds) accepted for a proof whose request uses the
107
115
  * `"latest"` block tag. The verifier compares the block timestamp from
package/wasm.d.ts CHANGED
@@ -28,6 +28,7 @@ export declare const loadC4WModule: () => Promise<C4W>;
28
28
  export declare const getC4w: () => Promise<C4W>;
29
29
  export declare const get_prover_config_hex: (chainId: number) => Promise<string>;
30
30
  export declare const set_trusted_checkpoint: (chainId: number, checkpoint: string) => Promise<void>;
31
+ export declare const reset_caches: () => Promise<void>;
31
32
  export declare const decode_proof: (proof: Uint8Array) => Promise<any>;
32
33
  /**
33
34
  * Returns the directory of the compiled module. Used in Node to locate
package/wasm.js CHANGED
@@ -59,6 +59,7 @@ export const loadC4WModule = api.loadC4WModule;
59
59
  export const getC4w = api.getC4w;
60
60
  export const get_prover_config_hex = api.get_prover_config_hex;
61
61
  export const set_trusted_checkpoint = api.set_trusted_checkpoint;
62
+ export const reset_caches = api.reset_caches;
62
63
  export const decode_proof = api.decode_proof;
63
64
  /**
64
65
  * Returns the directory of the compiled module. Used in Node to locate
package/wasm_cjs.d.ts CHANGED
@@ -28,6 +28,7 @@ export declare const loadC4WModule: () => Promise<C4W>;
28
28
  export declare const getC4w: () => Promise<C4W>;
29
29
  export declare const get_prover_config_hex: (chainId: number) => Promise<string>;
30
30
  export declare const set_trusted_checkpoint: (chainId: number, checkpoint: string) => Promise<void>;
31
+ export declare const reset_caches: () => Promise<void>;
31
32
  export declare const decode_proof: (proof: Uint8Array) => Promise<any>;
32
33
  /**
33
34
  * Returns the directory of the compiled module. Used in Node to locate