@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/c4w.js +1 -1
- package/c4w.wasm +0 -0
- package/chains.d.ts +2 -10
- package/chains.generated.d.ts +10 -0
- package/chains.generated.js +97 -0
- package/chains.js +2 -79
- package/cjs/chains.generated.js +100 -0
- package/cjs/chains.js +6 -83
- package/cjs/index.js +11 -1
- package/cjs/runtime_node.js +1 -0
- package/cjs/runtime_wasm.js +3 -0
- package/cjs/transactionVerifier.js +462 -51
- package/cjs/wasm.js +2 -1
- package/cjs/wasm_cjs.js +2 -1
- package/cjs/wasm_shared.js +9 -0
- package/index.d.ts +6 -1
- package/index.js +10 -1
- package/package.json +3 -2
- package/prebuilds/darwin-arm64/colibri_native.node +0 -0
- package/prebuilds/darwin-x64/colibri_native.node +0 -0
- package/prebuilds/linux-arm64/colibri_native.node +0 -0
- package/prebuilds/linux-x64/colibri_native.node +0 -0
- package/prebuilds/win32-x64/colibri_native.node +0 -0
- package/runtime.d.ts +1 -0
- package/runtime_node.js +1 -0
- package/runtime_wasm.js +3 -0
- package/transactionVerifier.d.ts +21 -14
- package/transactionVerifier.js +461 -51
- package/types.d.ts +8 -0
- package/wasm.d.ts +1 -0
- package/wasm.js +1 -0
- package/wasm_cjs.d.ts +1 -0
- package/wasm_cjs.js +1 -0
- package/wasm_shared.d.ts +2 -0
- package/wasm_shared.js +9 -0
|
@@ -23,7 +23,466 @@
|
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
exports.TransactionUtils = exports.PrototypeProtection = exports.TransactionVerifier = void 0;
|
|
26
|
+
exports.compareTransactionParameters = compareTransactionParameters;
|
|
26
27
|
const types_js_1 = require("./types.js");
|
|
28
|
+
function getOwn(obj, key) {
|
|
29
|
+
if (obj == null || typeof obj !== 'object')
|
|
30
|
+
return { present: false, value: undefined };
|
|
31
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key))
|
|
32
|
+
return { present: false, value: undefined };
|
|
33
|
+
return { present: true, value: obj[key] };
|
|
34
|
+
}
|
|
35
|
+
/** Resolve RPC aliases. If several names are present they must parse to the same value. */
|
|
36
|
+
function resolveAliased(obj, names, parse) {
|
|
37
|
+
let provided = false;
|
|
38
|
+
let canonical;
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
const field = getOwn(obj, name);
|
|
41
|
+
if (!field.present || field.value === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
const parsed = parse(field.value);
|
|
44
|
+
if (!parsed.ok)
|
|
45
|
+
return { ok: false };
|
|
46
|
+
if (!provided) {
|
|
47
|
+
provided = true;
|
|
48
|
+
canonical = parsed.value;
|
|
49
|
+
}
|
|
50
|
+
else if (canonical !== parsed.value) {
|
|
51
|
+
return { ok: false };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, value: { provided, value: canonical } };
|
|
55
|
+
}
|
|
56
|
+
function parseQuantity(value) {
|
|
57
|
+
if (value === undefined || value === null || value === '')
|
|
58
|
+
return { ok: true, value: 0n };
|
|
59
|
+
if (typeof value === 'boolean' || typeof value === 'object')
|
|
60
|
+
return { ok: false };
|
|
61
|
+
if (typeof value === 'bigint')
|
|
62
|
+
return value < 0n ? { ok: false } : { ok: true, value };
|
|
63
|
+
if (typeof value === 'number') {
|
|
64
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0)
|
|
65
|
+
return { ok: false };
|
|
66
|
+
return { ok: true, value: BigInt(value) };
|
|
67
|
+
}
|
|
68
|
+
const str = String(value).trim();
|
|
69
|
+
if (str === '' || str === '0x' || str === '0X')
|
|
70
|
+
return { ok: true, value: 0n };
|
|
71
|
+
if (/^0x[0-9a-fA-F]+$/.test(str))
|
|
72
|
+
return { ok: true, value: BigInt(str) };
|
|
73
|
+
if (/^[0-9]+$/.test(str))
|
|
74
|
+
return { ok: true, value: BigInt(str) };
|
|
75
|
+
return { ok: false };
|
|
76
|
+
}
|
|
77
|
+
/** 20-byte address, left-padded. `null`/`''`/`0x` → contract creation (`null`). */
|
|
78
|
+
function parseAddress(value) {
|
|
79
|
+
if (value === undefined || value === null || value === '')
|
|
80
|
+
return { ok: true, value: null };
|
|
81
|
+
if (typeof value === 'boolean' || typeof value === 'object')
|
|
82
|
+
return { ok: false };
|
|
83
|
+
let str = String(value).trim();
|
|
84
|
+
if (str === '0x' || str === '0X')
|
|
85
|
+
return { ok: true, value: null };
|
|
86
|
+
if (str.startsWith('0x') || str.startsWith('0X'))
|
|
87
|
+
str = str.slice(2);
|
|
88
|
+
if (!/^[0-9a-fA-F]+$/.test(str) || str.length > 40)
|
|
89
|
+
return { ok: false };
|
|
90
|
+
if (str.length % 2 === 1)
|
|
91
|
+
str = '0' + str;
|
|
92
|
+
return { ok: true, value: '0x' + str.toLowerCase().padStart(40, '0') };
|
|
93
|
+
}
|
|
94
|
+
function parseHexPayload(value) {
|
|
95
|
+
if (value === undefined || value === null || value === '')
|
|
96
|
+
return { ok: true, value: '' };
|
|
97
|
+
if (value instanceof Uint8Array) {
|
|
98
|
+
let hex = '';
|
|
99
|
+
for (let i = 0; i < value.length; i++)
|
|
100
|
+
hex += value[i].toString(16).padStart(2, '0');
|
|
101
|
+
return { ok: true, value: hex };
|
|
102
|
+
}
|
|
103
|
+
if (typeof value === 'boolean' || typeof value === 'object')
|
|
104
|
+
return { ok: false };
|
|
105
|
+
let str = String(value).trim();
|
|
106
|
+
if (str.startsWith('0x') || str.startsWith('0X'))
|
|
107
|
+
str = str.slice(2);
|
|
108
|
+
if (str === '')
|
|
109
|
+
return { ok: true, value: '' };
|
|
110
|
+
if (!/^[0-9a-fA-F]+$/.test(str))
|
|
111
|
+
return { ok: false };
|
|
112
|
+
return { ok: true, value: str.toLowerCase() };
|
|
113
|
+
}
|
|
114
|
+
/** Exact calldata bytes. Leading zeros are significant (`0x` ≠ `0x00`). Odd nibble length is invalid. */
|
|
115
|
+
function parseBytes(value) {
|
|
116
|
+
const hex = parseHexPayload(value);
|
|
117
|
+
if (!hex.ok)
|
|
118
|
+
return hex;
|
|
119
|
+
if (hex.value.length % 2 === 1)
|
|
120
|
+
return { ok: false };
|
|
121
|
+
return { ok: true, value: '0x' + hex.value };
|
|
122
|
+
}
|
|
123
|
+
function parseHash32(value) {
|
|
124
|
+
const hex = parseHexPayload(value);
|
|
125
|
+
if (!hex.ok)
|
|
126
|
+
return hex;
|
|
127
|
+
if (hex.value.length > 64)
|
|
128
|
+
return { ok: false };
|
|
129
|
+
return { ok: true, value: '0x' + hex.value.padStart(64, '0') };
|
|
130
|
+
}
|
|
131
|
+
function parseYParity(value) {
|
|
132
|
+
const q = parseQuantity(value);
|
|
133
|
+
if (!q.ok)
|
|
134
|
+
return q;
|
|
135
|
+
// Accept v = 27/28 as well as yParity = 0/1.
|
|
136
|
+
if (q.value === 27n || q.value === 28n)
|
|
137
|
+
return { ok: true, value: q.value - 27n };
|
|
138
|
+
if (q.value !== 0n && q.value !== 1n)
|
|
139
|
+
return { ok: false };
|
|
140
|
+
return q;
|
|
141
|
+
}
|
|
142
|
+
function warn(config, message) {
|
|
143
|
+
if (config.debug)
|
|
144
|
+
console.warn(`[TransactionVerifier] ${message}`);
|
|
145
|
+
}
|
|
146
|
+
function quantitiesEqual(originalVal, decodedVal, defaultIfOriginalOmitted) {
|
|
147
|
+
const decoded = decodedVal === undefined ? { ok: true, value: 0n } : parseQuantity(decodedVal);
|
|
148
|
+
if (!decoded.ok)
|
|
149
|
+
return 'invalid';
|
|
150
|
+
if (originalVal === undefined) {
|
|
151
|
+
if (defaultIfOriginalOmitted === 'skip')
|
|
152
|
+
return true;
|
|
153
|
+
return decoded.value === 0n;
|
|
154
|
+
}
|
|
155
|
+
const original = parseQuantity(originalVal);
|
|
156
|
+
if (!original.ok)
|
|
157
|
+
return 'invalid';
|
|
158
|
+
return original.value === decoded.value;
|
|
159
|
+
}
|
|
160
|
+
function compareAccessList(original, decoded, config) {
|
|
161
|
+
const origField = getOwn(original, 'accessList');
|
|
162
|
+
const decField = getOwn(decoded, 'accessList');
|
|
163
|
+
const origList = origField.present && origField.value != null ? origField.value : [];
|
|
164
|
+
const decList = decField.present && decField.value != null ? decField.value : [];
|
|
165
|
+
if (!Array.isArray(origList) || !Array.isArray(decList)) {
|
|
166
|
+
warn(config, 'accessList is not an array');
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (origList.length !== decList.length) {
|
|
170
|
+
warn(config, `accessList length mismatch: original=${origList.length}, decoded=${decList.length}`);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
for (let i = 0; i < origList.length; i++) {
|
|
174
|
+
const oEntry = origList[i];
|
|
175
|
+
const dEntry = decList[i];
|
|
176
|
+
if (oEntry == null || typeof oEntry !== 'object' || dEntry == null || typeof dEntry !== 'object') {
|
|
177
|
+
warn(config, `accessList[${i}] is not an object`);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const oa = parseAddress(getOwn(oEntry, 'address').value);
|
|
181
|
+
const da = parseAddress(getOwn(dEntry, 'address').value);
|
|
182
|
+
if (!oa.ok || !da.ok || oa.value === null || da.value === null || oa.value !== da.value) {
|
|
183
|
+
warn(config, `accessList[${i}].address mismatch`);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const oKeysField = getOwn(oEntry, 'storageKeys');
|
|
187
|
+
const dKeysField = getOwn(dEntry, 'storageKeys');
|
|
188
|
+
const oKeys = oKeysField.present && oKeysField.value != null ? oKeysField.value : [];
|
|
189
|
+
const dKeys = dKeysField.present && dKeysField.value != null ? dKeysField.value : [];
|
|
190
|
+
if (!Array.isArray(oKeys) || !Array.isArray(dKeys) || oKeys.length !== dKeys.length) {
|
|
191
|
+
warn(config, `accessList[${i}].storageKeys mismatch`);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
for (let k = 0; k < oKeys.length; k++) {
|
|
195
|
+
const okh = parseHash32(oKeys[k]);
|
|
196
|
+
const dkh = parseHash32(dKeys[k]);
|
|
197
|
+
if (!okh.ok || !dkh.ok || okh.value !== dkh.value) {
|
|
198
|
+
warn(config, `accessList[${i}].storageKeys[${k}] mismatch`);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
function compareAuthorizationList(original, decoded, config) {
|
|
206
|
+
const origField = getOwn(original, 'authorizationList');
|
|
207
|
+
const decField = getOwn(decoded, 'authorizationList');
|
|
208
|
+
const origList = origField.present && origField.value != null ? origField.value : [];
|
|
209
|
+
const decList = decField.present && decField.value != null ? decField.value : [];
|
|
210
|
+
if (!Array.isArray(origList) || !Array.isArray(decList)) {
|
|
211
|
+
warn(config, 'authorizationList is not an array');
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
if (origList.length !== decList.length) {
|
|
215
|
+
warn(config, `authorizationList length mismatch: original=${origList.length}, decoded=${decList.length}`);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
for (let i = 0; i < origList.length; i++) {
|
|
219
|
+
const o = origList[i];
|
|
220
|
+
const d = decList[i];
|
|
221
|
+
if (o == null || typeof o !== 'object' || d == null || typeof d !== 'object') {
|
|
222
|
+
warn(config, `authorizationList[${i}] is not an object`);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
const oa = parseAddress(getOwn(o, 'address').value);
|
|
226
|
+
const da = parseAddress(getOwn(d, 'address').value);
|
|
227
|
+
if (!oa.ok || !da.ok || oa.value === null || da.value === null || oa.value !== da.value) {
|
|
228
|
+
warn(config, `authorizationList[${i}].address mismatch`);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
const origChain = getOwn(o, 'chainId');
|
|
232
|
+
const expectedChain = origChain.present && origChain.value !== undefined ? origChain.value : config.chainId;
|
|
233
|
+
const chainEq = quantitiesEqual(expectedChain, getOwn(d, 'chainId').value, 'skip');
|
|
234
|
+
if (chainEq !== true) {
|
|
235
|
+
warn(config, `authorizationList[${i}].chainId mismatch`);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
const origNonce = getOwn(o, 'nonce');
|
|
239
|
+
if (origNonce.present && origNonce.value !== undefined) {
|
|
240
|
+
const nonceEq = quantitiesEqual(origNonce.value, getOwn(d, 'nonce').value, 'skip');
|
|
241
|
+
if (nonceEq !== true) {
|
|
242
|
+
warn(config, `authorizationList[${i}].nonce mismatch`);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const origHasSig = (getOwn(o, 'r').present && getOwn(o, 'r').value !== undefined) ||
|
|
247
|
+
(getOwn(o, 's').present && getOwn(o, 's').value !== undefined) ||
|
|
248
|
+
(getOwn(o, 'yParity').present && getOwn(o, 'yParity').value !== undefined) ||
|
|
249
|
+
(getOwn(o, 'v').present && getOwn(o, 'v').value !== undefined);
|
|
250
|
+
if (origHasSig) {
|
|
251
|
+
const or_ = parseHash32(getOwn(o, 'r').value);
|
|
252
|
+
const dr = parseHash32(getOwn(d, 'r').value);
|
|
253
|
+
const os = parseHash32(getOwn(o, 's').value);
|
|
254
|
+
const ds = parseHash32(getOwn(d, 's').value);
|
|
255
|
+
if (!or_.ok || !dr.ok || or_.value !== dr.value || !os.ok || !ds.ok || os.value !== ds.value) {
|
|
256
|
+
warn(config, `authorizationList[${i}] signature mismatch`);
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
const origY = getOwn(o, 'yParity').present ? getOwn(o, 'yParity').value : getOwn(o, 'v').value;
|
|
260
|
+
const decY = getOwn(d, 'yParity').present ? getOwn(d, 'yParity').value : getOwn(d, 'v').value;
|
|
261
|
+
const oy = parseYParity(origY);
|
|
262
|
+
const dy = parseYParity(decY);
|
|
263
|
+
if (!oy.ok || !dy.ok || oy.value !== dy.value) {
|
|
264
|
+
warn(config, `authorizationList[${i}].yParity mismatch`);
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
function compareHashList(originalVal, decodedVal, name, config) {
|
|
272
|
+
const origList = originalVal === undefined || originalVal === null ? [] : originalVal;
|
|
273
|
+
const decList = decodedVal === undefined || decodedVal === null ? [] : decodedVal;
|
|
274
|
+
if (!Array.isArray(origList) || !Array.isArray(decList)) {
|
|
275
|
+
warn(config, `${name} is not an array`);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (origList.length !== decList.length) {
|
|
279
|
+
warn(config, `${name} length mismatch: original=${origList.length}, decoded=${decList.length}`);
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
for (let i = 0; i < origList.length; i++) {
|
|
283
|
+
const oh = parseHash32(origList[i]);
|
|
284
|
+
const dh = parseHash32(decList[i]);
|
|
285
|
+
if (!oh.ok || !dh.ok || oh.value !== dh.value) {
|
|
286
|
+
warn(config, `${name}[${i}] mismatch`);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Compare an `eth_sendTransaction` object with the JSON from `colibri_decodeTransaction`.
|
|
294
|
+
*
|
|
295
|
+
* @param original Intended tx object from the dapp
|
|
296
|
+
* @param decoded Decoded signed tx (`EthTxData`: `input`, hex quantities, `to` null if create)
|
|
297
|
+
* @param config Must include the expected `chainId`
|
|
298
|
+
* @return `true` only if the signed tx matches the intent
|
|
299
|
+
*/
|
|
300
|
+
function compareTransactionParameters(original, decoded, config) {
|
|
301
|
+
if (config.debug) {
|
|
302
|
+
console.log('[TransactionVerifier] Comparing transaction parameters:');
|
|
303
|
+
console.log('Original:', original);
|
|
304
|
+
console.log('Decoded:', decoded);
|
|
305
|
+
}
|
|
306
|
+
if (original == null || typeof original !== 'object' || decoded == null || typeof decoded !== 'object') {
|
|
307
|
+
warn(config, 'original and decoded must be objects');
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const chainId = parseQuantity(config.chainId);
|
|
311
|
+
if (!chainId.ok) {
|
|
312
|
+
warn(config, 'config.chainId is invalid');
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
const decodedChainIdField = getOwn(decoded, 'chainId');
|
|
316
|
+
if (!decodedChainIdField.present) {
|
|
317
|
+
warn(config, 'signed transaction has no chainId (pre-EIP-155 replay risk)');
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
const decodedChainId = parseQuantity(decodedChainIdField.value);
|
|
321
|
+
if (!decodedChainId.ok || decodedChainId.value !== chainId.value) {
|
|
322
|
+
warn(config, `ChainId mismatch: expected=${config.chainId}, decoded=${decodedChainIdField.value}`);
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
const originalChainId = getOwn(original, 'chainId');
|
|
326
|
+
if (originalChainId.present && originalChainId.value !== undefined) {
|
|
327
|
+
const parsed = parseQuantity(originalChainId.value);
|
|
328
|
+
if (!parsed.ok || parsed.value !== chainId.value) {
|
|
329
|
+
warn(config, 'original.chainId does not match config.chainId');
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Decoder maps pre-EIP-155 legacy v=27/28 to chainId 1, so a missing-chainId
|
|
334
|
+
// check is not enough on mainnet. EIP-155 requires v >= 35.
|
|
335
|
+
const decodedType = getOwn(decoded, 'type');
|
|
336
|
+
const typeVal = decodedType.present ? parseQuantity(decodedType.value) : { ok: true, value: 0n };
|
|
337
|
+
if (!typeVal.ok) {
|
|
338
|
+
warn(config, 'invalid decoded.type');
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
if (typeVal.value === 0n) {
|
|
342
|
+
const vField = getOwn(decoded, 'v');
|
|
343
|
+
const v = vField.present ? parseQuantity(vField.value) : { ok: false };
|
|
344
|
+
if (!v.ok || v.value < 35n) {
|
|
345
|
+
warn(config, 'unprotected pre-EIP-155 signature');
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const origTo = resolveAliased(original, ['to'], parseAddress);
|
|
350
|
+
const decTo = resolveAliased(decoded, ['to'], parseAddress);
|
|
351
|
+
if (!origTo.ok || !decTo.ok) {
|
|
352
|
+
warn(config, 'invalid to address');
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
const expectedTo = origTo.value.provided ? origTo.value.value : null;
|
|
356
|
+
const actualTo = decTo.value.provided ? decTo.value.value : null;
|
|
357
|
+
if (expectedTo !== actualTo) {
|
|
358
|
+
warn(config, `to mismatch: original='${expectedTo}', decoded='${actualTo}'`);
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
const origFrom = resolveAliased(original, ['from'], parseAddress);
|
|
362
|
+
const decFrom = resolveAliased(decoded, ['from'], parseAddress);
|
|
363
|
+
if (!origFrom.ok || !decFrom.ok) {
|
|
364
|
+
warn(config, 'invalid from address');
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (origFrom.value.provided && origFrom.value.value !== (decFrom.value.provided ? decFrom.value.value : null)) {
|
|
368
|
+
warn(config, `from mismatch: original='${origFrom.value.value}', decoded='${decFrom.value.value}'`);
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
const origValue = getOwn(original, 'value');
|
|
372
|
+
const valueEq = quantitiesEqual(origValue.present ? origValue.value : undefined, getOwn(decoded, 'value').value, 'zero');
|
|
373
|
+
if (valueEq !== true) {
|
|
374
|
+
warn(config, `value mismatch: original='${origValue.value}', decoded='${decoded.value}'`);
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
const origData = resolveAliased(original, ['data', 'input'], parseBytes);
|
|
378
|
+
const decData = resolveAliased(decoded, ['input', 'data'], parseBytes);
|
|
379
|
+
if (!origData.ok || !decData.ok) {
|
|
380
|
+
warn(config, 'invalid calldata (data/input)');
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
const expectedData = origData.value.provided ? origData.value.value : '0x';
|
|
384
|
+
const actualData = decData.value.provided ? decData.value.value : '0x';
|
|
385
|
+
if (expectedData !== actualData) {
|
|
386
|
+
warn(config, `data/input mismatch: original='${expectedData}', decoded='${actualData}'`);
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
const origType = getOwn(original, 'type');
|
|
390
|
+
if (origType.present && origType.value !== undefined) {
|
|
391
|
+
const typeEq = quantitiesEqual(origType.value, decoded.type, 'skip');
|
|
392
|
+
if (typeEq !== true) {
|
|
393
|
+
warn(config, `type mismatch: original='${origType.value}', decoded='${decoded.type}'`);
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const origNonce = getOwn(original, 'nonce');
|
|
398
|
+
if (origNonce.present && origNonce.value !== undefined) {
|
|
399
|
+
const nonceEq = quantitiesEqual(origNonce.value, decoded.nonce, 'skip');
|
|
400
|
+
if (nonceEq !== true) {
|
|
401
|
+
warn(config, `nonce mismatch: original='${origNonce.value}', decoded='${decoded.nonce}'`);
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const origGas = resolveAliased(original, ['gas', 'gasLimit'], parseQuantity);
|
|
406
|
+
if (!origGas.ok) {
|
|
407
|
+
warn(config, 'invalid gas/gasLimit');
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
if (origGas.value.provided) {
|
|
411
|
+
const gasEq = quantitiesEqual(origGas.value.value, decoded.gas, 'skip');
|
|
412
|
+
if (gasEq !== true) {
|
|
413
|
+
warn(config, `gas mismatch: original='${origGas.value.value}', decoded='${decoded.gas}'`);
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const origMaxFee = getOwn(original, 'maxFeePerGas');
|
|
418
|
+
const origPrio = getOwn(original, 'maxPriorityFeePerGas');
|
|
419
|
+
const origGasPrice = getOwn(original, 'gasPrice');
|
|
420
|
+
const isEip1559 = origMaxFee.present || origPrio.present;
|
|
421
|
+
if (origMaxFee.present && origMaxFee.value !== undefined) {
|
|
422
|
+
const eq = quantitiesEqual(origMaxFee.value, decoded.maxFeePerGas, 'skip');
|
|
423
|
+
if (eq !== true) {
|
|
424
|
+
warn(config, `maxFeePerGas mismatch`);
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (origPrio.present && origPrio.value !== undefined) {
|
|
429
|
+
const eq = quantitiesEqual(origPrio.value, decoded.maxPriorityFeePerGas, 'skip');
|
|
430
|
+
if (eq !== true) {
|
|
431
|
+
warn(config, `maxPriorityFeePerGas mismatch`);
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Decoded EIP-1559 txs still carry an *effective* gasPrice; do not compare it.
|
|
436
|
+
if (origGasPrice.present && origGasPrice.value !== undefined && !isEip1559) {
|
|
437
|
+
const eq = quantitiesEqual(origGasPrice.value, decoded.gasPrice, 'skip');
|
|
438
|
+
if (eq !== true) {
|
|
439
|
+
warn(config, `gasPrice mismatch`);
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (!compareAccessList(original, decoded, config))
|
|
444
|
+
return false;
|
|
445
|
+
if (!compareAuthorizationList(original, decoded, config))
|
|
446
|
+
return false;
|
|
447
|
+
const origBlobs = getOwn(original, 'blobVersionedHashes');
|
|
448
|
+
const decBlobs = getOwn(decoded, 'blobVersionedHashes');
|
|
449
|
+
if (!compareHashList(origBlobs.present ? origBlobs.value : [], decBlobs.present ? decBlobs.value : [], 'blobVersionedHashes', config))
|
|
450
|
+
return false;
|
|
451
|
+
const origBlobFee = getOwn(original, 'maxFeePerBlobGas');
|
|
452
|
+
if (origBlobFee.present && origBlobFee.value !== undefined) {
|
|
453
|
+
const eq = quantitiesEqual(origBlobFee.value, decoded.maxFeePerBlobGas, 'skip');
|
|
454
|
+
if (eq !== true) {
|
|
455
|
+
warn(config, `maxFeePerBlobGas mismatch`);
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const origMint = getOwn(original, 'mint');
|
|
460
|
+
const mintEq = quantitiesEqual(origMint.present ? origMint.value : undefined, getOwn(decoded, 'mint').present ? decoded.mint : 0n, 'zero');
|
|
461
|
+
if (mintEq !== true) {
|
|
462
|
+
warn(config, 'unexpected mint field on signed transaction');
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
const origSource = getOwn(original, 'sourceHash');
|
|
466
|
+
const decSource = getOwn(decoded, 'sourceHash');
|
|
467
|
+
if (decSource.present) {
|
|
468
|
+
const expected = origSource.present ? origSource.value : '0x' + '00'.repeat(32);
|
|
469
|
+
const oh = parseHash32(expected);
|
|
470
|
+
const dh = parseHash32(decSource.value);
|
|
471
|
+
if (!oh.ok || !dh.ok || oh.value !== dh.value) {
|
|
472
|
+
warn(config, 'unexpected sourceHash on signed transaction');
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const origSys = getOwn(original, 'isSystemTx');
|
|
477
|
+
const decSys = getOwn(decoded, 'isSystemTx');
|
|
478
|
+
const expectedSys = origSys.present ? Boolean(origSys.value) : false;
|
|
479
|
+
const actualSys = decSys.present ? Boolean(decSys.value) : false;
|
|
480
|
+
if (expectedSys !== actualSys) {
|
|
481
|
+
warn(config, 'unexpected isSystemTx on signed transaction');
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
27
486
|
class TransactionVerifier {
|
|
28
487
|
/**
|
|
29
488
|
* Verifizierte Ausführung von eth_sendTransaction
|
|
@@ -47,7 +506,7 @@ class TransactionVerifier {
|
|
|
47
506
|
// 2. Signierte Transaction dekodieren
|
|
48
507
|
const decodedTx = await rpcMethod('colibri_decodeTransaction', [signedRawTx], types_js_1.MethodType.LOCAL);
|
|
49
508
|
// 3. Dekodierte Parameter mit Original vergleichen
|
|
50
|
-
const isValid =
|
|
509
|
+
const isValid = compareTransactionParameters(txObject, decodedTx, config);
|
|
51
510
|
if (!isValid) {
|
|
52
511
|
throw new types_js_1.ProviderRpcError(4201, 'Signed transaction does not match original parameters');
|
|
53
512
|
}
|
|
@@ -63,55 +522,6 @@ class TransactionVerifier {
|
|
|
63
522
|
throw new types_js_1.ProviderRpcError(4202, `Transaction verification failed: ${error.message}`);
|
|
64
523
|
}
|
|
65
524
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Vergleicht die Parameter einer ursprünglichen Transaction mit einer dekodierten signierten Transaction
|
|
68
|
-
*/
|
|
69
|
-
static compareTransactionParameters(original, decoded, config) {
|
|
70
|
-
if (config.debug) {
|
|
71
|
-
console.log('[TransactionVerifier] Comparing transaction parameters:');
|
|
72
|
-
console.log('Original:', original);
|
|
73
|
-
console.log('Decoded:', decoded);
|
|
74
|
-
}
|
|
75
|
-
// Kritische Parameter vergleichen
|
|
76
|
-
const criticalFields = ['to', 'value', 'data', 'gas', 'gasPrice', 'maxFeePerGas', 'maxPriorityFeePerGas', 'nonce'];
|
|
77
|
-
for (const field of criticalFields) {
|
|
78
|
-
if (original[field] !== undefined) {
|
|
79
|
-
// Normalisierung für Hex-Werte (0x-Prefix, Leading Zeros)
|
|
80
|
-
const originalValue = this.normalizeHexValue(original[field]);
|
|
81
|
-
const decodedValue = this.normalizeHexValue(decoded[field]);
|
|
82
|
-
if (originalValue !== decodedValue) {
|
|
83
|
-
if (config.debug) {
|
|
84
|
-
console.warn(`[TransactionVerifier] Transaction parameter mismatch in '${field}': original='${originalValue}', decoded='${decodedValue}'`);
|
|
85
|
-
}
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
// ChainId prüfen
|
|
91
|
-
if (decoded.chainId && parseInt(decoded.chainId, 16) !== parseInt(config.chainId)) {
|
|
92
|
-
if (config.debug) {
|
|
93
|
-
console.warn(`[TransactionVerifier] ChainId mismatch: expected=${config.chainId}, decoded=${parseInt(decoded.chainId, 16)}`);
|
|
94
|
-
}
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Normalisiert Hex-Werte für Vergleiche (entfernt führende Nullen, stellt 0x-Prefix sicher)
|
|
101
|
-
*/
|
|
102
|
-
static normalizeHexValue(value) {
|
|
103
|
-
if (value === undefined || value === null)
|
|
104
|
-
return '';
|
|
105
|
-
if (typeof value === 'number')
|
|
106
|
-
return '0x' + value.toString(16);
|
|
107
|
-
const str = value.toString();
|
|
108
|
-
if (!str.startsWith('0x'))
|
|
109
|
-
return '0x' + str;
|
|
110
|
-
// Führende Nullen entfernen, aber mindestens eine Ziffer behalten
|
|
111
|
-
const withoutPrefix = str.slice(2);
|
|
112
|
-
const normalized = withoutPrefix.replace(/^0+/, '') || '0';
|
|
113
|
-
return '0x' + normalized;
|
|
114
|
-
}
|
|
115
525
|
}
|
|
116
526
|
exports.TransactionVerifier = TransactionVerifier;
|
|
117
527
|
/**
|
|
@@ -198,5 +608,6 @@ exports.TransactionUtils = TransactionUtils;
|
|
|
198
608
|
exports.default = {
|
|
199
609
|
TransactionVerifier,
|
|
200
610
|
PrototypeProtection,
|
|
201
|
-
TransactionUtils
|
|
611
|
+
TransactionUtils,
|
|
612
|
+
compareTransactionParameters
|
|
202
613
|
};
|
package/cjs/wasm.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* SPDX-License-Identifier: MIT
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.decode_proof = exports.set_trusted_checkpoint = exports.get_prover_config_hex = exports.getC4w = exports.loadC4WModule = exports.set_wasm_url = exports.get_default_storage = exports.copy_to_c = exports.as_json = exports.as_char_ptr = exports.as_bytes = void 0;
|
|
25
|
+
exports.decode_proof = exports.reset_caches = exports.set_trusted_checkpoint = exports.get_prover_config_hex = exports.getC4w = exports.loadC4WModule = exports.set_wasm_url = exports.get_default_storage = exports.copy_to_c = exports.as_json = exports.as_char_ptr = exports.as_bytes = void 0;
|
|
26
26
|
exports.module_dir = module_dir;
|
|
27
27
|
// This file is used only for the CommonJS build output. It must not use
|
|
28
28
|
// `import.meta` because that is a parse error in CommonJS environments (e.g. Jest).
|
|
@@ -64,6 +64,7 @@ exports.loadC4WModule = api.loadC4WModule;
|
|
|
64
64
|
exports.getC4w = api.getC4w;
|
|
65
65
|
exports.get_prover_config_hex = api.get_prover_config_hex;
|
|
66
66
|
exports.set_trusted_checkpoint = api.set_trusted_checkpoint;
|
|
67
|
+
exports.reset_caches = api.reset_caches;
|
|
67
68
|
exports.decode_proof = api.decode_proof;
|
|
68
69
|
/**
|
|
69
70
|
* Returns the directory of the compiled module. Used in Node to locate
|
package/cjs/wasm_cjs.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* SPDX-License-Identifier: MIT
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.decode_proof = exports.set_trusted_checkpoint = exports.get_prover_config_hex = exports.getC4w = exports.loadC4WModule = exports.set_wasm_url = exports.get_default_storage = exports.copy_to_c = exports.as_json = exports.as_char_ptr = exports.as_bytes = void 0;
|
|
25
|
+
exports.decode_proof = exports.reset_caches = exports.set_trusted_checkpoint = exports.get_prover_config_hex = exports.getC4w = exports.loadC4WModule = exports.set_wasm_url = exports.get_default_storage = exports.copy_to_c = exports.as_json = exports.as_char_ptr = exports.as_bytes = void 0;
|
|
26
26
|
exports.module_dir = module_dir;
|
|
27
27
|
// This file is used only for the CommonJS build output. It must not use
|
|
28
28
|
// `import.meta` because that is a parse error in CommonJS environments (e.g. Jest).
|
|
@@ -64,6 +64,7 @@ exports.loadC4WModule = api.loadC4WModule;
|
|
|
64
64
|
exports.getC4w = api.getC4w;
|
|
65
65
|
exports.get_prover_config_hex = api.get_prover_config_hex;
|
|
66
66
|
exports.set_trusted_checkpoint = api.set_trusted_checkpoint;
|
|
67
|
+
exports.reset_caches = api.reset_caches;
|
|
67
68
|
exports.decode_proof = api.decode_proof;
|
|
68
69
|
/**
|
|
69
70
|
* Returns the directory of the compiled module. Used in Node to locate
|
package/cjs/wasm_shared.js
CHANGED
|
@@ -256,6 +256,14 @@ function createC4wApi(options) {
|
|
|
256
256
|
c4w._c4w_set_checkpoint(BigInt(chainId), as_char_ptr(checkpoint, c4w, free_buffers));
|
|
257
257
|
free_buffers.forEach(ptr => c4w._free(ptr));
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Clears in-process prover/verifier caches (header tags, EL headers, PAP
|
|
261
|
+
* tx cache, prover cache). Persistent storage is left to the host.
|
|
262
|
+
*/
|
|
263
|
+
async function reset_caches() {
|
|
264
|
+
const c4w = await getC4w();
|
|
265
|
+
c4w._c4w_reset_caches();
|
|
266
|
+
}
|
|
259
267
|
async function decode_proof(proof) {
|
|
260
268
|
const c4w = await getC4w();
|
|
261
269
|
const ptr = copy_to_c(proof, c4w);
|
|
@@ -271,6 +279,7 @@ function createC4wApi(options) {
|
|
|
271
279
|
getC4w,
|
|
272
280
|
get_prover_config_hex,
|
|
273
281
|
set_trusted_checkpoint,
|
|
282
|
+
reset_caches,
|
|
274
283
|
decode_proof,
|
|
275
284
|
};
|
|
276
285
|
}
|
package/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export { getRuntime, type C4Runtime, type RuntimeStatus } from './runtime.js';
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function decode_proof(proof: Uint8Array): Promise<any>;
|
|
36
36
|
export { ProviderRpcError, RequestArguments, Cache, C4Config, DataRequest, C4MethodType as MethodType, ProviderMessage, EthSubscribeSubscriptionType, EthNewFilterType, ProverMode };
|
|
37
|
-
export { TransactionVerifier, PrototypeProtection } from './transactionVerifier.js';
|
|
37
|
+
export { TransactionVerifier, PrototypeProtection, compareTransactionParameters } from './transactionVerifier.js';
|
|
38
38
|
export default class C4Client {
|
|
39
39
|
config: C4Config;
|
|
40
40
|
private eventEmitter;
|
|
@@ -100,6 +100,11 @@ export default class C4Client {
|
|
|
100
100
|
*/
|
|
101
101
|
rpc(method: string, args: any[], method_type?: C4MethodType): Promise<any>;
|
|
102
102
|
static register_storage(storage: C4Storage): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Clears in-process prover/verifier caches. Call between fixture-backed
|
|
105
|
+
* tests that share one WASM/native process.
|
|
106
|
+
*/
|
|
107
|
+
static reset_caches(): Promise<void>;
|
|
103
108
|
request(args: RequestArguments): Promise<unknown>;
|
|
104
109
|
/**
|
|
105
110
|
* Registers an event listener on the Colibri client.
|
package/index.js
CHANGED
|
@@ -46,7 +46,7 @@ export async function decode_proof(proof) {
|
|
|
46
46
|
// Re-export types needed by consumers of the C4Client module
|
|
47
47
|
export { ProviderRpcError, C4MethodType as MethodType };
|
|
48
48
|
// Re-export transaction verification utilities
|
|
49
|
-
export { TransactionVerifier, PrototypeProtection } from './transactionVerifier.js';
|
|
49
|
+
export { TransactionVerifier, PrototypeProtection, compareTransactionParameters } from './transactionVerifier.js';
|
|
50
50
|
// EIP-3668 / CCIP-Read: the EVM ran to completion but reverted. Map the
|
|
51
51
|
// verifier's `revert` status to a structured EIP-1193 error (code 3 + data)
|
|
52
52
|
// so clients like ethers can decode OffchainLookup payloads from
|
|
@@ -102,6 +102,8 @@ class C4Client {
|
|
|
102
102
|
this.flags |= (1 << 12);
|
|
103
103
|
this.verify_flags |= (1 << 9);
|
|
104
104
|
}
|
|
105
|
+
if (this.config.persist_header_cache)
|
|
106
|
+
this.verify_flags |= (1 << 10);
|
|
105
107
|
if (!this.config.warningHandler)
|
|
106
108
|
this.config.warningHandler = async (req, message) => console.warn(message);
|
|
107
109
|
if (!this.config.proofStrategy)
|
|
@@ -267,6 +269,13 @@ class C4Client {
|
|
|
267
269
|
static async register_storage(storage) {
|
|
268
270
|
(await getRuntime()).registerStorage(storage);
|
|
269
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Clears in-process prover/verifier caches. Call between fixture-backed
|
|
274
|
+
* tests that share one WASM/native process.
|
|
275
|
+
*/
|
|
276
|
+
static async reset_caches() {
|
|
277
|
+
(await getRuntime()).resetCaches();
|
|
278
|
+
}
|
|
270
279
|
async request(args) {
|
|
271
280
|
if (!this.connectionState.initialConnectionAttempted) {
|
|
272
281
|
await this.connectionState.attemptInitialConnection();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@corpus-core/colibri-stateless",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Colibri is a stateless and trustless ethereum client, which is optimized for the mobile apps or embedded devices, because it does not hols the state, but verifies on demand.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"test": "npm run test:node && npm run test:webpack",
|
|
23
|
-
"test:node": "node --test test/rpc.test.mjs test/cjs_bridge.test.mjs test/runtime_selection.test.mjs",
|
|
23
|
+
"test:node": "node --test test/rpc.test.mjs test/cjs_bridge.test.mjs test/runtime_selection.test.mjs test/transactionVerifier.test.mjs",
|
|
24
24
|
"test:node:native": "node --test test/native.test.mjs",
|
|
25
25
|
"test:integration": "C4_RUN_INTEGRATION=1 node --test test/integration.test.mjs",
|
|
26
|
+
"test:rpc-integration": "C4_RUN_RPC_INTEGRATION=1 node --test --test-concurrency=1 --test-timeout=3600000 test/rpc_integration.test.mjs",
|
|
26
27
|
"test:ccip": "C4_RUN_INTEGRATION=1 node --test test/ens_ccip.test.mjs",
|
|
27
28
|
"test:webpack": "npm run webpack:install && npm run webpack:build",
|
|
28
29
|
"webpack:install": "cd test/webpack && npm install --legacy-peer-deps",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|