@corpus-core/colibri-stateless 2.0.6 → 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
|
Binary file
|
|
Binary file
|
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);
|
package/transactionVerifier.d.ts
CHANGED
|
@@ -22,13 +22,18 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { MethodType as C4MethodType } from './types.js';
|
|
24
24
|
/**
|
|
25
|
-
* Transaction Verifier
|
|
25
|
+
* Transaction Verifier — defence against a compromised `eth_signTransaction` provider.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
* 1.
|
|
29
|
-
* 2.
|
|
30
|
-
* 3.
|
|
31
|
-
* 4.
|
|
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;
|
package/transactionVerifier.js
CHANGED
|
@@ -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 =
|
|
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
|