@callcium/sdk 0.6.0 → 0.8.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/README.md +77 -2
- package/dist/index.d.ts +114 -43
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +838 -1440
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
- package/src/bytes.ts +100 -0
- package/src/calldata-reader.ts +38 -0
- package/src/constants.ts +395 -0
- package/src/constraint.ts +313 -0
- package/src/descriptor-coder.ts +461 -0
- package/src/descriptor.ts +399 -0
- package/src/errors.ts +138 -0
- package/src/index.ts +105 -0
- package/src/operators.ts +204 -0
- package/src/policy-builder.ts +250 -0
- package/src/policy-coder.ts +668 -0
- package/src/policy-enforcer.ts +600 -0
- package/src/policy-validator.ts +1112 -0
- package/src/signature.ts +97 -0
- package/src/type-desc.ts +205 -0
- package/src/types.ts +280 -0
- package/src/validation-issue.ts +569 -0
package/dist/index.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { keccak_256 } from "@noble/hashes/sha3";
|
|
1
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
2
2
|
//#region src/errors.ts
|
|
3
|
-
/**
|
|
4
|
-
* Thrown when a policy or descriptor blob is structurally malformed.
|
|
5
|
-
* The optional `offset` indicates the byte position in the source blob where the error was detected.
|
|
6
|
-
*/
|
|
7
3
|
var CallciumError = class CallciumError extends Error {
|
|
8
4
|
code;
|
|
9
|
-
/** Byte position in the source blob where the error was detected, if applicable. */
|
|
10
5
|
offset;
|
|
11
6
|
constructor(code, message, offset) {
|
|
12
7
|
const prefix = offset !== void 0 ? `[offset ${offset}] ` : "";
|
|
@@ -16,16 +11,15 @@ var CallciumError = class CallciumError extends Error {
|
|
|
16
11
|
Object.setPrototypeOf(this, CallciumError.prototype);
|
|
17
12
|
}
|
|
18
13
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*/
|
|
14
|
+
var ValidationError = class ValidationError extends CallciumError {
|
|
15
|
+
issues;
|
|
16
|
+
constructor(issues) {
|
|
17
|
+
super("VALIDATION_ERROR", issues[0]?.message ?? "Policy validation failed");
|
|
18
|
+
this.name = "ValidationError";
|
|
19
|
+
this.issues = issues;
|
|
20
|
+
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
29
23
|
var PolicyViolationError = class PolicyViolationError extends Error {
|
|
30
24
|
violations;
|
|
31
25
|
constructor(violations) {
|
|
@@ -35,7 +29,6 @@ var PolicyViolationError = class PolicyViolationError extends Error {
|
|
|
35
29
|
Object.setPrototypeOf(this, PolicyViolationError.prototype);
|
|
36
30
|
}
|
|
37
31
|
};
|
|
38
|
-
/** Build a stack-trace-friendly diagnostic line from a violation's code and coordinates. */
|
|
39
32
|
function formatDiagnostic(violation) {
|
|
40
33
|
if (!violation) return "Policy violation";
|
|
41
34
|
const location = "group" in violation ? ` at group ${violation.group} rule ${violation.rule}` : "";
|
|
@@ -43,56 +36,61 @@ function formatDiagnostic(violation) {
|
|
|
43
36
|
}
|
|
44
37
|
//#endregion
|
|
45
38
|
//#region src/bytes.ts
|
|
46
|
-
/** Regex that matches a valid hex body (even number of hex chars). */
|
|
47
39
|
const HEX_BODY_RE = /^[0-9a-fA-F]*$/;
|
|
48
|
-
|
|
40
|
+
const HEX_DIGITS = "0123456789abcdef";
|
|
41
|
+
const NIBBLE = new Int8Array(128).fill(-1);
|
|
42
|
+
for (let value = 0; value < 16; value++) {
|
|
43
|
+
NIBBLE[HEX_DIGITS.charCodeAt(value)] = value;
|
|
44
|
+
NIBBLE[HEX_DIGITS.toUpperCase().charCodeAt(value)] = value;
|
|
45
|
+
}
|
|
49
46
|
function hexToBytes(hex) {
|
|
50
47
|
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
51
48
|
if (clean.length % 2 !== 0) throw new CallciumError("INVALID_HEX", "Odd-length hex string");
|
|
52
|
-
if (!HEX_BODY_RE.test(clean)) throw new CallciumError("INVALID_HEX", "Invalid hex characters");
|
|
53
49
|
const bytes = new Uint8Array(clean.length / 2);
|
|
54
|
-
for (let i = 0; i <
|
|
50
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
51
|
+
const high = NIBBLE[clean.charCodeAt(i * 2)] ?? -1;
|
|
52
|
+
const low = NIBBLE[clean.charCodeAt(i * 2 + 1)] ?? -1;
|
|
53
|
+
if (high < 0 || low < 0) throw new CallciumError("INVALID_HEX", "Invalid hex characters");
|
|
54
|
+
bytes[i] = high << 4 | low;
|
|
55
|
+
}
|
|
55
56
|
return bytes;
|
|
56
57
|
}
|
|
57
58
|
const HEX_LUT = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
58
|
-
/** Convert a byte array to a 0x-prefixed lowercase hex string. */
|
|
59
59
|
function bytesToHex(bytes) {
|
|
60
60
|
let body = "";
|
|
61
61
|
for (let i = 0; i < bytes.length; i++) body += HEX_LUT[bytes[i]];
|
|
62
62
|
return `0x${body}`;
|
|
63
63
|
}
|
|
64
|
-
/** Extract a subarray and return it as a 0x-prefixed hex string. */
|
|
65
64
|
function toHex(data, start, end) {
|
|
66
65
|
return bytesToHex(data.subarray(start, end));
|
|
67
66
|
}
|
|
68
|
-
/** Read a big-endian uint16 from a byte array at the given offset. */
|
|
69
67
|
function readU16(data, offset) {
|
|
70
68
|
return data[offset] << 8 | data[offset + 1];
|
|
71
69
|
}
|
|
72
|
-
/** Read a big-endian uint24 from a byte array at the given offset. */
|
|
73
70
|
function readU24(data, offset) {
|
|
74
71
|
return data[offset] << 16 | data[offset + 1] << 8 | data[offset + 2];
|
|
75
72
|
}
|
|
76
|
-
/** Read a big-endian uint32 from a byte array at the given offset. */
|
|
77
73
|
function readU32(data, offset) {
|
|
78
74
|
return (data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]) >>> 0;
|
|
79
75
|
}
|
|
80
|
-
/** Write a 16-bit big-endian value into a Uint8Array at offset. */
|
|
81
76
|
function writeBE16(buffer, offset, value) {
|
|
82
77
|
buffer[offset] = value >>> 8 & 255;
|
|
83
78
|
buffer[offset + 1] = value & 255;
|
|
84
79
|
}
|
|
85
|
-
/** Write a 24-bit big-endian value into a Uint8Array at offset. */
|
|
86
80
|
function writeBE24(buffer, offset, value) {
|
|
87
81
|
buffer[offset] = value >>> 16 & 255;
|
|
88
82
|
buffer[offset + 1] = value >>> 8 & 255;
|
|
89
83
|
buffer[offset + 2] = value & 255;
|
|
90
84
|
}
|
|
91
|
-
|
|
85
|
+
function writeBE32(buffer, offset, value) {
|
|
86
|
+
buffer[offset] = value >>> 24 & 255;
|
|
87
|
+
buffer[offset + 1] = value >>> 16 & 255;
|
|
88
|
+
buffer[offset + 2] = value >>> 8 & 255;
|
|
89
|
+
buffer[offset + 3] = value & 255;
|
|
90
|
+
}
|
|
92
91
|
function bigintToHex(value) {
|
|
93
92
|
return `0x${value.toString(16).padStart(64, "0")}`;
|
|
94
93
|
}
|
|
95
|
-
/** Validate and brand a hex string as an address. Throws if the runtime length is not exactly 20 bytes. */
|
|
96
94
|
function toAddress(hex) {
|
|
97
95
|
const body = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
98
96
|
if (body.length !== 40) throw new CallciumError("INVALID_HEX", `Invalid address length: expected 40 hex chars, got ${body.length}`);
|
|
@@ -101,9 +99,8 @@ function toAddress(hex) {
|
|
|
101
99
|
}
|
|
102
100
|
//#endregion
|
|
103
101
|
//#region src/constants.ts
|
|
104
|
-
/** Binary layout constants for the Callcium descriptor format. */
|
|
105
102
|
const DescriptorFormat = {
|
|
106
|
-
VERSION:
|
|
103
|
+
VERSION: 2,
|
|
107
104
|
HEADER_SIZE: 2,
|
|
108
105
|
TYPECODE_SIZE: 1,
|
|
109
106
|
COMPOSITE_META_SIZE: 3,
|
|
@@ -116,12 +113,12 @@ const DescriptorFormat = {
|
|
|
116
113
|
MAX_NODE_LENGTH: 4095,
|
|
117
114
|
MAX_STATIC_ARRAY_LENGTH: 4095,
|
|
118
115
|
MAX_TUPLE_FIELDS: 4089,
|
|
116
|
+
MAX_STATIC_WORDS: 4095,
|
|
119
117
|
MAX_PARAMS: 255,
|
|
120
118
|
MAX_NESTING_DEPTH: 64
|
|
121
119
|
};
|
|
122
|
-
/** Binary layout constants for the Callcium policy format. */
|
|
123
120
|
const PolicyFormat = {
|
|
124
|
-
VERSION:
|
|
121
|
+
VERSION: 2,
|
|
125
122
|
VERSION_MASK: 15,
|
|
126
123
|
FLAG_NO_SELECTOR: 16,
|
|
127
124
|
RESERVED_MASK: 224,
|
|
@@ -143,9 +140,31 @@ const PolicyFormat = {
|
|
|
143
140
|
RULE_OPCODE_SIZE: 1,
|
|
144
141
|
RULE_DATALENGTH_SIZE: 2,
|
|
145
142
|
RULE_FIXED_OVERHEAD: 7,
|
|
146
|
-
RULE_MIN_SIZE: 9
|
|
143
|
+
RULE_MIN_SIZE: 9,
|
|
144
|
+
HINT_HEADER_SIZE: 1,
|
|
145
|
+
HINT_HOP_SIZE: 8,
|
|
146
|
+
HINT_HOP_INDEX_OFFSET: 4,
|
|
147
|
+
HINT_HOP_META_OFFSET: 6,
|
|
148
|
+
HINT_FRAME_PREFIX_SIZE: 8,
|
|
149
|
+
HINT_FRAME_COUNT_OFFSET: 4,
|
|
150
|
+
HINT_FRAME_META_OFFSET: 6,
|
|
151
|
+
HINT_TARGET_SIZE: 7,
|
|
152
|
+
HINT_TARGET_META_OFFSET: 4,
|
|
153
|
+
HINT_TARGET_TYPECODE_OFFSET: 6,
|
|
154
|
+
HINT_KIND_NONE: 0,
|
|
155
|
+
HINT_KIND_ALL: 1,
|
|
156
|
+
HINT_KIND_ANY: 2,
|
|
157
|
+
HINT_KIND_MAX: 2,
|
|
158
|
+
HINT_KIND_SHIFT: 6,
|
|
159
|
+
HINT_HOP_COUNT_MASK: 63,
|
|
160
|
+
HINT_SUFFIX_RESERVED_MASK: 192,
|
|
161
|
+
HINT_NO_INDEX: 65535,
|
|
162
|
+
HINT_INDEX_RESERVED: 65534,
|
|
163
|
+
HINT_META_ELEM_DYNAMIC: 32768,
|
|
164
|
+
HINT_META_DYNAMIC_ARRAY: 16384,
|
|
165
|
+
HINT_META_RESERVED_MASK: 12288,
|
|
166
|
+
HINT_META_STRIDE_MASK: 4095
|
|
147
167
|
};
|
|
148
|
-
/** Build a plain `{ KEY: code }` object from a table at runtime. */
|
|
149
168
|
function buildCodeMap(table) {
|
|
150
169
|
return Object.fromEntries(table.map((e) => [e.key, e.code]));
|
|
151
170
|
}
|
|
@@ -158,29 +177,21 @@ const SCOPE_TABLE = [{
|
|
|
158
177
|
code: 1,
|
|
159
178
|
label: "calldata"
|
|
160
179
|
}];
|
|
161
|
-
/** Rule scope discriminant: context (EVM environment) vs. calldata (ABI payload). */
|
|
162
180
|
const Scope = buildCodeMap(SCOPE_TABLE);
|
|
163
181
|
const scopeByCode = new Map(SCOPE_TABLE.map((e) => [e.code, { label: e.label }]));
|
|
164
|
-
/**
|
|
165
|
-
* Map a scope code to its display label.
|
|
166
|
-
* @param code - Scope byte value.
|
|
167
|
-
* @returns Display metadata for the scope.
|
|
168
|
-
* @throws {CallciumError} If the code is not a recognised scope.
|
|
169
|
-
*/
|
|
170
182
|
function lookupScope(code) {
|
|
171
183
|
const info = scopeByCode.get(code);
|
|
172
184
|
if (!info) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${code}`);
|
|
173
185
|
return info;
|
|
174
186
|
}
|
|
175
|
-
/** ABI type code ranges and sentinel values for the descriptor format. */
|
|
176
187
|
const TypeCode = {
|
|
177
|
-
UINT_MIN:
|
|
178
|
-
UINT_MAX:
|
|
179
|
-
INT_MIN:
|
|
180
|
-
INT_MAX:
|
|
181
|
-
ADDRESS:
|
|
182
|
-
BOOL:
|
|
183
|
-
FUNCTION:
|
|
188
|
+
UINT_MIN: 1,
|
|
189
|
+
UINT_MAX: 32,
|
|
190
|
+
INT_MIN: 33,
|
|
191
|
+
INT_MAX: 64,
|
|
192
|
+
ADDRESS: 65,
|
|
193
|
+
BOOL: 66,
|
|
194
|
+
FUNCTION: 67,
|
|
184
195
|
FIXED_BYTES_MIN: 80,
|
|
185
196
|
FIXED_BYTES_MAX: 111,
|
|
186
197
|
BYTES: 112,
|
|
@@ -239,11 +250,8 @@ const CTX_PROP_TABLE = [
|
|
|
239
250
|
typeCode: TypeCode.UINT_MAX
|
|
240
251
|
}
|
|
241
252
|
];
|
|
242
|
-
/** Well-known context property IDs for context-scope rules. */
|
|
243
253
|
const ContextProperty = buildCodeMap(CTX_PROP_TABLE);
|
|
244
|
-
/** Maximum valid context property ID. */
|
|
245
254
|
const MAX_CONTEXT_PROPERTY_ID = Math.max(...CTX_PROP_TABLE.map((e) => e.code));
|
|
246
|
-
/** Maps each context property table key to its camelCase Context key. */
|
|
247
255
|
const CTX_KEY_MAP = {
|
|
248
256
|
MSG_SENDER: "msgSender",
|
|
249
257
|
MSG_VALUE: "msgValue",
|
|
@@ -259,23 +267,16 @@ const ctxPropByCode = new Map(CTX_PROP_TABLE.map((e) => [e.code, {
|
|
|
259
267
|
contextKey: CTX_KEY_MAP[e.key],
|
|
260
268
|
typeCode: e.typeCode
|
|
261
269
|
}]));
|
|
262
|
-
/**
|
|
263
|
-
* Map a context property code to its display label and ABI type code.
|
|
264
|
-
* @param code - Context property ID.
|
|
265
|
-
* @returns Display metadata including the Solidity type code for the property value.
|
|
266
|
-
* @throws {CallciumError} If the code is not a recognised context property.
|
|
267
|
-
*/
|
|
268
270
|
function lookupContextProperty(code) {
|
|
269
271
|
const info = ctxPropByCode.get(code);
|
|
270
|
-
if (!info) throw new CallciumError("
|
|
272
|
+
if (!info) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ${code}`);
|
|
271
273
|
return info;
|
|
272
274
|
}
|
|
273
|
-
/** Protocol-imposed safety limits for path depth and quantifier array size. */
|
|
274
275
|
const Limits = {
|
|
275
276
|
MAX_PATH_DEPTH: 32,
|
|
276
|
-
MAX_QUANTIFIED_ARRAY_LENGTH: 256
|
|
277
|
+
MAX_QUANTIFIED_ARRAY_LENGTH: 256,
|
|
278
|
+
MAX_SET_MEMBERS: Math.floor((256 ** PolicyFormat.RULE_DATALENGTH_SIZE - 1) / 32)
|
|
277
279
|
};
|
|
278
|
-
/** @internal */
|
|
279
280
|
const OP_TABLE = [
|
|
280
281
|
{
|
|
281
282
|
key: "EQ",
|
|
@@ -374,7 +375,6 @@ const OP_TABLE = [
|
|
|
374
375
|
operands: "range"
|
|
375
376
|
}
|
|
376
377
|
];
|
|
377
|
-
/** Callcium policy operator codes. */
|
|
378
378
|
const Op = {
|
|
379
379
|
...buildCodeMap(OP_TABLE),
|
|
380
380
|
NOT: 128
|
|
@@ -383,24 +383,12 @@ const opByCode = new Map(OP_TABLE.map((e) => [e.code, {
|
|
|
383
383
|
label: e.label,
|
|
384
384
|
operands: e.operands
|
|
385
385
|
}]));
|
|
386
|
-
/**
|
|
387
|
-
* Map an operator code to its display label. Strips the NOT flag automatically.
|
|
388
|
-
* @param code - Operator byte value (may include the NOT flag).
|
|
389
|
-
* @returns Display metadata for the base operator.
|
|
390
|
-
* @throws {CallciumError} If the base code is not a recognised operator.
|
|
391
|
-
*/
|
|
392
386
|
function lookupOp(code) {
|
|
393
387
|
const base = code & ~Op.NOT;
|
|
394
388
|
const info = opByCode.get(base);
|
|
395
|
-
if (!info) throw new CallciumError("
|
|
389
|
+
if (!info) throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}`);
|
|
396
390
|
return info;
|
|
397
391
|
}
|
|
398
|
-
/**
|
|
399
|
-
* Check whether an operator's data payload has the correct length.
|
|
400
|
-
* @param opBase - Base operator code with the NOT flag stripped.
|
|
401
|
-
* @param dataLength - Byte length of the operator's data payload.
|
|
402
|
-
* @returns True if the data length is valid for the given operator.
|
|
403
|
-
*/
|
|
404
392
|
function isValidOperatorData(opBase, dataLength) {
|
|
405
393
|
const operands = opByCode.get(opBase)?.operands;
|
|
406
394
|
if (operands === "single") return dataLength === 32;
|
|
@@ -408,42 +396,25 @@ function isValidOperatorData(opBase, dataLength) {
|
|
|
408
396
|
if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
|
|
409
397
|
return false;
|
|
410
398
|
}
|
|
411
|
-
const QUANTIFIER_TABLE = [
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
label: "all"
|
|
421
|
-
},
|
|
422
|
-
{
|
|
423
|
-
key: "ANY",
|
|
424
|
-
code: 65533,
|
|
425
|
-
label: "any"
|
|
426
|
-
}
|
|
427
|
-
];
|
|
428
|
-
/** Reserved path step values that trigger quantified evaluation over array elements. */
|
|
399
|
+
const QUANTIFIER_TABLE = [{
|
|
400
|
+
key: "ALL",
|
|
401
|
+
code: 65535,
|
|
402
|
+
label: "all"
|
|
403
|
+
}, {
|
|
404
|
+
key: "ANY",
|
|
405
|
+
code: 65534,
|
|
406
|
+
label: "any"
|
|
407
|
+
}];
|
|
429
408
|
const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
|
|
430
409
|
const quantifierByCode = new Map(QUANTIFIER_TABLE.map((e) => [e.code, { label: e.label }]));
|
|
431
|
-
/** Check whether a path step is a quantifier (ALL_OR_EMPTY, ALL, or ANY). */
|
|
432
410
|
function isQuantifier(step) {
|
|
433
411
|
return step >= Quantifier.ANY;
|
|
434
412
|
}
|
|
435
|
-
/**
|
|
436
|
-
* Map a quantifier path step to its display label.
|
|
437
|
-
* @param code - Quantifier step value.
|
|
438
|
-
* @returns Display metadata for the quantifier.
|
|
439
|
-
* @throws {CallciumError} If the code is not a recognised quantifier.
|
|
440
|
-
*/
|
|
441
413
|
function lookupQuantifier(code) {
|
|
442
414
|
const info = quantifierByCode.get(code);
|
|
443
|
-
if (!info) throw new CallciumError("
|
|
415
|
+
if (!info) throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
|
|
444
416
|
return info;
|
|
445
417
|
}
|
|
446
|
-
/** Throw an UNKNOWN_TYPE_CODE error. */
|
|
447
418
|
function unknownTypeCode(code) {
|
|
448
419
|
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code 0x${code.toString(16).padStart(2, "0")}`);
|
|
449
420
|
}
|
|
@@ -467,18 +438,11 @@ const TUPLE = {
|
|
|
467
438
|
typeClass: "tuple",
|
|
468
439
|
isDynamic: false
|
|
469
440
|
};
|
|
470
|
-
/**
|
|
471
|
-
* Classify a type code into its structural category and dynamism.
|
|
472
|
-
* Lightweight variant of `lookupTypeCode` that skips label computation.
|
|
473
|
-
* @param code - A single-byte descriptor type code.
|
|
474
|
-
* @returns Type class and whether the type is ABI-dynamic.
|
|
475
|
-
* @throws {CallciumError} If the code is not a recognised type code.
|
|
476
|
-
*/
|
|
477
441
|
function classifyTypeCode(code) {
|
|
478
442
|
if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return ELEMENTARY;
|
|
479
443
|
if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return ELEMENTARY;
|
|
480
444
|
if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
|
|
481
|
-
if (code >=
|
|
445
|
+
if (code >= 68 && code <= 79) unknownTypeCode(code);
|
|
482
446
|
if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
|
|
483
447
|
if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYN;
|
|
484
448
|
if (code >= 114 && code <= 127) unknownTypeCode(code);
|
|
@@ -488,7 +452,15 @@ function classifyTypeCode(code) {
|
|
|
488
452
|
if (code === TypeCode.TUPLE) return TUPLE;
|
|
489
453
|
unknownTypeCode(code);
|
|
490
454
|
}
|
|
491
|
-
|
|
455
|
+
function isAddressableTarget(code) {
|
|
456
|
+
let info;
|
|
457
|
+
try {
|
|
458
|
+
info = classifyTypeCode(code);
|
|
459
|
+
} catch {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
return info.typeClass === "elementary" || info.typeClass === "dynamicArray";
|
|
463
|
+
}
|
|
492
464
|
function typeCodeLabel(code) {
|
|
493
465
|
if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return `uint${(code - TypeCode.UINT_MIN + 1) * 8}`;
|
|
494
466
|
if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return `int${(code - TypeCode.INT_MIN + 1) * 8}`;
|
|
@@ -503,12 +475,6 @@ function typeCodeLabel(code) {
|
|
|
503
475
|
if (code === TypeCode.TUPLE) return "tuple";
|
|
504
476
|
return `0x${code.toString(16).padStart(2, "0")}`;
|
|
505
477
|
}
|
|
506
|
-
/**
|
|
507
|
-
* Map a raw type code byte to its Solidity label, structural category, and dynamism.
|
|
508
|
-
* @param code - A single-byte descriptor type code.
|
|
509
|
-
* @returns Label, type class, and whether the type is ABI-dynamic.
|
|
510
|
-
* @throws {CallciumError} If the code is not a recognised type code.
|
|
511
|
-
*/
|
|
512
478
|
function lookupTypeCode(code) {
|
|
513
479
|
return {
|
|
514
480
|
label: typeCodeLabel(code),
|
|
@@ -517,42 +483,25 @@ function lookupTypeCode(code) {
|
|
|
517
483
|
}
|
|
518
484
|
//#endregion
|
|
519
485
|
//#region src/descriptor.ts
|
|
520
|
-
/**
|
|
521
|
-
* Return the byte length of the node at `offset`.
|
|
522
|
-
* Elementary nodes occupy exactly one byte; composite nodes encode their
|
|
523
|
-
* length in the lower 12 bits of the 24-bit meta field.
|
|
524
|
-
*/
|
|
525
486
|
function nodeLength(desc, offset) {
|
|
526
487
|
if (desc[offset] < TypeCode.STATIC_ARRAY) return DescriptorFormat.TYPECODE_SIZE;
|
|
527
488
|
return readU24(desc, offset + 1) & DescriptorFormat.META_NODE_LENGTH_MASK;
|
|
528
489
|
}
|
|
529
|
-
/**
|
|
530
|
-
* Return the byte offset of the `fieldIndex`-th field inside a tuple node.
|
|
531
|
-
* Starts at the first field and skips `fieldIndex` nodes using `nodeLength`.
|
|
532
|
-
*/
|
|
533
490
|
function tupleFieldOffset(desc, tupleOffset, fieldIndex) {
|
|
534
491
|
let cursor = tupleOffset + DescriptorFormat.TUPLE_HEADER_SIZE;
|
|
535
492
|
for (let i = 0; i < fieldIndex; i++) cursor += nodeLength(desc, cursor);
|
|
536
493
|
return cursor;
|
|
537
494
|
}
|
|
538
|
-
/** Return paramCount from header byte 1. */
|
|
539
495
|
function paramCount(desc) {
|
|
540
496
|
return desc[1];
|
|
541
497
|
}
|
|
542
|
-
/** Return byte offset of the N-th top-level param (0-indexed). */
|
|
543
498
|
function paramOffset(desc, index) {
|
|
544
499
|
const count = desc[1];
|
|
545
|
-
if (index >= count) throw new CallciumError("
|
|
500
|
+
if (index >= count) throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count}).`);
|
|
546
501
|
let cursor = DescriptorFormat.HEADER_SIZE;
|
|
547
502
|
for (let i = 0; i < index; i++) cursor += nodeLength(desc, cursor);
|
|
548
503
|
return cursor;
|
|
549
504
|
}
|
|
550
|
-
/**
|
|
551
|
-
* Inspect the type node at a given byte offset.
|
|
552
|
-
* @param desc - Raw descriptor bytes.
|
|
553
|
-
* @param offset - Byte position of the node.
|
|
554
|
-
* @returns typeCode, isDynamic, and staticSize in bytes.
|
|
555
|
-
*/
|
|
556
505
|
function inspect$1(desc, offset) {
|
|
557
506
|
const typeCode = desc[offset];
|
|
558
507
|
let staticWords;
|
|
@@ -564,35 +513,14 @@ function inspect$1(desc, offset) {
|
|
|
564
513
|
staticSize: staticWords * 32
|
|
565
514
|
};
|
|
566
515
|
}
|
|
567
|
-
/**
|
|
568
|
-
* Resolve the type at a calldata path (array of step indices).
|
|
569
|
-
*
|
|
570
|
-
* The first step selects a top-level param. Each subsequent step descends
|
|
571
|
-
* into the current node: for tuples it selects a field, for arrays it
|
|
572
|
-
* advances to the element descriptor.
|
|
573
|
-
*
|
|
574
|
-
* @param desc - Raw descriptor bytes.
|
|
575
|
-
* @param steps - Path steps, length >= 1.
|
|
576
|
-
* @returns TypeInfo for the node at the resolved path.
|
|
577
|
-
* @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent into elementary type.
|
|
578
|
-
*/
|
|
579
516
|
function typeAt(desc, steps) {
|
|
580
517
|
return walkPath(desc, steps).typeInfo;
|
|
581
518
|
}
|
|
582
|
-
/**
|
|
583
|
-
* Resolve the type at the path and the declared length of the static array a
|
|
584
|
-
* quantifier step descends into (zero when the path holds no quantifier step
|
|
585
|
-
* over a static array).
|
|
586
|
-
* @param desc - Raw descriptor bytes.
|
|
587
|
-
* @param steps - Path steps, length >= 1.
|
|
588
|
-
* @returns TypeInfo at the resolved path and the quantified static array length.
|
|
589
|
-
* @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent into elementary type.
|
|
590
|
-
*/
|
|
591
519
|
function walkPath(desc, steps) {
|
|
592
|
-
if (steps.length === 0) throw new CallciumError("
|
|
520
|
+
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
|
|
593
521
|
const paramIndex = steps[0];
|
|
594
522
|
const count = desc[1];
|
|
595
|
-
if (paramIndex >= count) throw new CallciumError("
|
|
523
|
+
if (paramIndex >= count) throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${paramIndex} out of range (paramCount=${count}).`);
|
|
596
524
|
let cursor = paramOffset(desc, paramIndex);
|
|
597
525
|
let quantifiedStaticLength = 0;
|
|
598
526
|
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
|
|
@@ -600,47 +528,162 @@ function walkPath(desc, steps) {
|
|
|
600
528
|
const step = steps[stepIndex];
|
|
601
529
|
if (typeCode === TypeCode.TUPLE) {
|
|
602
530
|
const fields = tupleFieldCount(desc, cursor);
|
|
603
|
-
if (step >= fields) throw new CallciumError("
|
|
531
|
+
if (step >= fields) throw new CallciumError("TUPLE_FIELD_OUT_OF_BOUNDS", `Tuple field index ${step} out of range (tuple has ${fields} fields).`);
|
|
604
532
|
cursor = tupleFieldOffset(desc, cursor, step);
|
|
605
533
|
} else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) {
|
|
606
534
|
if (typeCode === TypeCode.STATIC_ARRAY) if (isQuantifier(step)) quantifiedStaticLength = staticArrayLength(desc, cursor);
|
|
607
535
|
else {
|
|
608
536
|
const length = staticArrayLength(desc, cursor);
|
|
609
|
-
if (step >= length) throw new CallciumError("
|
|
537
|
+
if (step >= length) throw new CallciumError("STATIC_ARRAY_INDEX_OUT_OF_BOUNDS", `Array index ${step} out of range (length=${length}).`);
|
|
610
538
|
}
|
|
611
539
|
cursor = arrayElementOffset(cursor);
|
|
612
|
-
} else throw new CallciumError("
|
|
540
|
+
} else throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}.`);
|
|
613
541
|
}
|
|
614
542
|
return {
|
|
615
543
|
typeInfo: inspect$1(desc, cursor),
|
|
616
544
|
quantifiedStaticLength
|
|
617
545
|
};
|
|
618
546
|
}
|
|
619
|
-
/**
|
|
620
|
-
* Return tuple field count at a tuple node offset.
|
|
621
|
-
* @param desc - Raw descriptor bytes.
|
|
622
|
-
* @param offset - Byte position of the tuple node.
|
|
623
|
-
*/
|
|
624
547
|
function tupleFieldCount(desc, offset) {
|
|
625
548
|
return readU16(desc, offset + 4);
|
|
626
549
|
}
|
|
627
|
-
/**
|
|
628
|
-
* Return static array length at a static array node offset.
|
|
629
|
-
* @param desc - Raw descriptor bytes.
|
|
630
|
-
* @param offset - Byte position of the static array node.
|
|
631
|
-
*/
|
|
632
550
|
function staticArrayLength(desc, offset) {
|
|
633
551
|
const elemOffset = arrayElementOffset(offset);
|
|
634
552
|
return readU16(desc, elemOffset + nodeLength(desc, elemOffset));
|
|
635
553
|
}
|
|
636
|
-
/**
|
|
637
|
-
* Return the byte offset of the array element descriptor.
|
|
638
|
-
* @param offset - Byte position of the array node (static or dynamic).
|
|
639
|
-
*/
|
|
640
554
|
function arrayElementOffset(offset) {
|
|
641
555
|
return offset + DescriptorFormat.ARRAY_HEADER_SIZE;
|
|
642
556
|
}
|
|
643
|
-
|
|
557
|
+
function uncompilablePath(stepIndex) {
|
|
558
|
+
return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target.`);
|
|
559
|
+
}
|
|
560
|
+
function headSpan(desc, offset, count) {
|
|
561
|
+
let span = 0;
|
|
562
|
+
let nodeOffset = offset;
|
|
563
|
+
for (let i = 0; i < count; i++) {
|
|
564
|
+
const node = inspect$1(desc, nodeOffset);
|
|
565
|
+
span += node.isDynamic ? 32 : node.staticSize;
|
|
566
|
+
nodeOffset += nodeLength(desc, nodeOffset);
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
span,
|
|
570
|
+
nodeOffset
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function arrayMeta(desc, arrayOffset, code) {
|
|
574
|
+
const element = inspect$1(desc, arrayElementOffset(arrayOffset));
|
|
575
|
+
let meta = element.isDynamic ? 1 : element.staticSize >> 5;
|
|
576
|
+
if (element.isDynamic) meta |= PolicyFormat.HINT_META_ELEM_DYNAMIC;
|
|
577
|
+
if (code === TypeCode.DYNAMIC_ARRAY) meta |= PolicyFormat.HINT_META_DYNAMIC_ARRAY;
|
|
578
|
+
return {
|
|
579
|
+
meta,
|
|
580
|
+
elemIsDynamic: element.isDynamic
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function enterNode(walk, needed) {
|
|
584
|
+
if (!needed) return;
|
|
585
|
+
walk.chain.push([
|
|
586
|
+
walk.delta,
|
|
587
|
+
PolicyFormat.HINT_NO_INDEX,
|
|
588
|
+
0
|
|
589
|
+
]);
|
|
590
|
+
walk.delta = 0;
|
|
591
|
+
}
|
|
592
|
+
function walkStep(desc, walk, descOffset, step, stepIndex) {
|
|
593
|
+
const node = inspect$1(desc, descOffset);
|
|
594
|
+
if (node.typeCode === TypeCode.TUPLE) {
|
|
595
|
+
if (step >= tupleFieldCount(desc, descOffset)) throw uncompilablePath(stepIndex);
|
|
596
|
+
enterNode(walk, node.isDynamic && !walk.entered);
|
|
597
|
+
const field = headSpan(desc, descOffset + DescriptorFormat.TUPLE_HEADER_SIZE, step);
|
|
598
|
+
walk.delta += field.span;
|
|
599
|
+
walk.entered = false;
|
|
600
|
+
return field.nodeOffset;
|
|
601
|
+
}
|
|
602
|
+
if (node.typeCode !== TypeCode.STATIC_ARRAY && node.typeCode !== TypeCode.DYNAMIC_ARRAY) throw uncompilablePath(stepIndex);
|
|
603
|
+
if (node.typeCode === TypeCode.STATIC_ARRAY && !isQuantifier(step) && step >= staticArrayLength(desc, descOffset)) throw uncompilablePath(stepIndex);
|
|
604
|
+
const { meta, elemIsDynamic } = arrayMeta(desc, descOffset, node.typeCode);
|
|
605
|
+
if (isQuantifier(step)) {
|
|
606
|
+
if (walk.kind !== PolicyFormat.HINT_KIND_NONE) throw uncompilablePath(stepIndex);
|
|
607
|
+
enterNode(walk, node.isDynamic && !walk.entered);
|
|
608
|
+
const count = node.typeCode === TypeCode.DYNAMIC_ARRAY ? 0 : staticArrayLength(desc, descOffset);
|
|
609
|
+
walk.frame = {
|
|
610
|
+
arrayDelta: walk.delta,
|
|
611
|
+
count,
|
|
612
|
+
meta
|
|
613
|
+
};
|
|
614
|
+
walk.kind = step === Quantifier.ALL ? PolicyFormat.HINT_KIND_ALL : PolicyFormat.HINT_KIND_ANY;
|
|
615
|
+
walk.mainHops = walk.chain;
|
|
616
|
+
walk.chain = [];
|
|
617
|
+
walk.delta = 0;
|
|
618
|
+
} else if (node.isDynamic) {
|
|
619
|
+
enterNode(walk, !walk.entered);
|
|
620
|
+
walk.chain.push([
|
|
621
|
+
0,
|
|
622
|
+
step,
|
|
623
|
+
meta
|
|
624
|
+
]);
|
|
625
|
+
walk.delta = 0;
|
|
626
|
+
} else walk.delta += step * (meta & PolicyFormat.HINT_META_STRIDE_MASK) * 32;
|
|
627
|
+
walk.entered = elemIsDynamic;
|
|
628
|
+
return arrayElementOffset(descOffset);
|
|
629
|
+
}
|
|
630
|
+
function writeHops(out, offset, hops) {
|
|
631
|
+
for (const [delta, index, meta] of hops) {
|
|
632
|
+
writeBE32(out, offset, delta);
|
|
633
|
+
writeBE16(out, offset + PolicyFormat.HINT_HOP_INDEX_OFFSET, index);
|
|
634
|
+
writeBE16(out, offset + PolicyFormat.HINT_HOP_META_OFFSET, meta);
|
|
635
|
+
offset += PolicyFormat.HINT_HOP_SIZE;
|
|
636
|
+
}
|
|
637
|
+
return offset;
|
|
638
|
+
}
|
|
639
|
+
function encodeHint(walk, targetMeta, typeCode, depth) {
|
|
640
|
+
let suffixHops = [];
|
|
641
|
+
if (walk.kind === PolicyFormat.HINT_KIND_NONE) walk.mainHops = walk.chain;
|
|
642
|
+
else suffixHops = walk.chain;
|
|
643
|
+
const mainHopCount = walk.mainHops.length;
|
|
644
|
+
const suffixHopCount = suffixHops.length;
|
|
645
|
+
if (mainHopCount > PolicyFormat.HINT_HOP_COUNT_MASK || suffixHopCount > PolicyFormat.HINT_HOP_COUNT_MASK) throw uncompilablePath(depth);
|
|
646
|
+
const quantified = walk.kind !== PolicyFormat.HINT_KIND_NONE;
|
|
647
|
+
const frameSize = quantified ? PolicyFormat.HINT_FRAME_PREFIX_SIZE + PolicyFormat.HINT_HEADER_SIZE + suffixHopCount * PolicyFormat.HINT_HOP_SIZE : 0;
|
|
648
|
+
const out = new Uint8Array(PolicyFormat.HINT_HEADER_SIZE + mainHopCount * PolicyFormat.HINT_HOP_SIZE + frameSize + PolicyFormat.HINT_TARGET_SIZE);
|
|
649
|
+
let offset = 0;
|
|
650
|
+
out[offset] = walk.kind << PolicyFormat.HINT_KIND_SHIFT | mainHopCount;
|
|
651
|
+
offset = writeHops(out, offset + PolicyFormat.HINT_HEADER_SIZE, walk.mainHops);
|
|
652
|
+
if (quantified) {
|
|
653
|
+
writeBE32(out, offset, walk.frame.arrayDelta);
|
|
654
|
+
writeBE16(out, offset + PolicyFormat.HINT_FRAME_COUNT_OFFSET, walk.frame.count);
|
|
655
|
+
writeBE16(out, offset + PolicyFormat.HINT_FRAME_META_OFFSET, walk.frame.meta);
|
|
656
|
+
out[offset + PolicyFormat.HINT_FRAME_PREFIX_SIZE] = suffixHopCount;
|
|
657
|
+
offset = writeHops(out, offset + PolicyFormat.HINT_FRAME_PREFIX_SIZE + PolicyFormat.HINT_HEADER_SIZE, suffixHops);
|
|
658
|
+
}
|
|
659
|
+
writeBE32(out, offset, walk.delta);
|
|
660
|
+
writeBE16(out, offset + PolicyFormat.HINT_TARGET_META_OFFSET, targetMeta);
|
|
661
|
+
out[offset + PolicyFormat.HINT_TARGET_TYPECODE_OFFSET] = typeCode;
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
664
|
+
function compileHint(desc, steps) {
|
|
665
|
+
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
|
|
666
|
+
const argIndex = steps[0];
|
|
667
|
+
if (argIndex >= paramCount(desc)) throw uncompilablePath(0);
|
|
668
|
+
const arg = headSpan(desc, DescriptorFormat.HEADER_SIZE, argIndex);
|
|
669
|
+
const walk = {
|
|
670
|
+
chain: [],
|
|
671
|
+
mainHops: [],
|
|
672
|
+
delta: arg.span,
|
|
673
|
+
frame: {
|
|
674
|
+
arrayDelta: 0,
|
|
675
|
+
count: 0,
|
|
676
|
+
meta: 0
|
|
677
|
+
},
|
|
678
|
+
kind: PolicyFormat.HINT_KIND_NONE,
|
|
679
|
+
entered: false
|
|
680
|
+
};
|
|
681
|
+
let descOffset = arg.nodeOffset;
|
|
682
|
+
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) descOffset = walkStep(desc, walk, descOffset, steps[stepIndex], stepIndex);
|
|
683
|
+
const target = inspect$1(desc, descOffset);
|
|
684
|
+
enterNode(walk, target.isDynamic && !walk.entered);
|
|
685
|
+
return encodeHint(walk, target.typeCode === TypeCode.DYNAMIC_ARRAY ? arrayMeta(desc, descOffset, target.typeCode).meta : 0, target.typeCode, steps.length);
|
|
686
|
+
}
|
|
644
687
|
const Descriptor = {
|
|
645
688
|
paramCount,
|
|
646
689
|
paramOffset,
|
|
@@ -651,88 +694,47 @@ const Descriptor = {
|
|
|
651
694
|
staticArrayLength,
|
|
652
695
|
nodeLength,
|
|
653
696
|
arrayElementOffset,
|
|
654
|
-
walkPath
|
|
697
|
+
walkPath,
|
|
698
|
+
compileHint
|
|
655
699
|
};
|
|
656
700
|
//#endregion
|
|
657
701
|
//#region src/type-desc.ts
|
|
658
|
-
/**
|
|
659
|
-
* Extract the staticWords count from a descriptor node.
|
|
660
|
-
*
|
|
661
|
-
* For elementary types (typeCode < STATIC_ARRAY), returns 1 unless the type is
|
|
662
|
-
* BYTES or STRING, which are ABI-dynamic and return 0. For composite types,
|
|
663
|
-
* reads the 24-bit meta field at offset 1 and shifts right by
|
|
664
|
-
* META_STATIC_WORDS_SHIFT.
|
|
665
|
-
*/
|
|
666
702
|
function extractStaticWords(desc) {
|
|
667
703
|
const typeCode = desc[0];
|
|
668
704
|
if (typeCode < TypeCode.STATIC_ARRAY) return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING ? 0 : 1;
|
|
669
705
|
return readU24(desc, 1) >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
|
|
670
706
|
}
|
|
671
|
-
/** Build a descriptor node for `address`. */
|
|
672
707
|
function address() {
|
|
673
708
|
return new Uint8Array([TypeCode.ADDRESS]);
|
|
674
709
|
}
|
|
675
|
-
/** Build a descriptor node for `bool`. */
|
|
676
710
|
function bool() {
|
|
677
711
|
return new Uint8Array([TypeCode.BOOL]);
|
|
678
712
|
}
|
|
679
|
-
/** Build a descriptor node for `function`. */
|
|
680
713
|
function function_() {
|
|
681
714
|
return new Uint8Array([TypeCode.FUNCTION]);
|
|
682
715
|
}
|
|
683
|
-
/** Build a descriptor node for `uint256`. */
|
|
684
|
-
function uint256() {
|
|
685
|
-
return new Uint8Array([TypeCode.UINT_MAX]);
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* Build a descriptor node for `uintN`.
|
|
689
|
-
* @param bits - Bit width, 8–256 in steps of 8.
|
|
690
|
-
* @throws {CallciumError} If bits is out of range or not a multiple of 8.
|
|
691
|
-
*/
|
|
692
716
|
function uintN(bits) {
|
|
693
|
-
if (bits < 8 || bits > 256 || bits % 8 !== 0) throw new CallciumError("
|
|
717
|
+
if (bits < 8 || bits > 256 || bits % 8 !== 0) throw new CallciumError("UNKNOWN_TYPE", `Invalid uintN bits: ${bits}. Must be 8–256 in steps of 8.`);
|
|
694
718
|
return new Uint8Array([TypeCode.UINT_MIN + (bits / 8 - 1)]);
|
|
695
719
|
}
|
|
696
|
-
/**
|
|
697
|
-
* Build a descriptor node for `intN`.
|
|
698
|
-
* @param bits - Bit width, 8–256 in steps of 8.
|
|
699
|
-
* @throws {CallciumError} If bits is out of range or not a multiple of 8.
|
|
700
|
-
*/
|
|
701
720
|
function intN(bits) {
|
|
702
|
-
if (bits < 8 || bits > 256 || bits % 8 !== 0) throw new CallciumError("
|
|
721
|
+
if (bits < 8 || bits > 256 || bits % 8 !== 0) throw new CallciumError("UNKNOWN_TYPE", `Invalid intN bits: ${bits}. Must be 8–256 in steps of 8.`);
|
|
703
722
|
return new Uint8Array([TypeCode.INT_MIN + (bits / 8 - 1)]);
|
|
704
723
|
}
|
|
705
|
-
/** Build a descriptor node for `bytes` (dynamic). */
|
|
706
724
|
function bytes() {
|
|
707
725
|
return new Uint8Array([TypeCode.BYTES]);
|
|
708
726
|
}
|
|
709
|
-
/** Build a descriptor node for `string`. */
|
|
710
727
|
function string_() {
|
|
711
728
|
return new Uint8Array([TypeCode.STRING]);
|
|
712
729
|
}
|
|
713
|
-
/**
|
|
714
|
-
* Build a descriptor node for `bytesN`.
|
|
715
|
-
* @param n - Byte width, 1–32.
|
|
716
|
-
* @throws {CallciumError} If n is out of range.
|
|
717
|
-
*/
|
|
718
730
|
function bytesN(n) {
|
|
719
|
-
if (n < 1 || n > 32) throw new CallciumError("
|
|
731
|
+
if (n < 1 || n > 32) throw new CallciumError("UNKNOWN_TYPE", `Invalid bytesN size: ${n}. Must be 1–32.`);
|
|
720
732
|
return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (n - 1)]);
|
|
721
733
|
}
|
|
722
|
-
/**
|
|
723
|
-
* Build a descriptor node for a dynamic array `T[]` or static array `T[length]`.
|
|
724
|
-
*
|
|
725
|
-
* Dynamic form: `[0x81][meta:3][elemDesc]`
|
|
726
|
-
* Static form: `[0x80][meta:3][elemDesc][length:be16]`
|
|
727
|
-
*
|
|
728
|
-
* @param elemDesc - Descriptor bytes for the element type.
|
|
729
|
-
* @param length - Fixed array length. When omitted, produces a dynamic array.
|
|
730
|
-
* @throws {CallciumError} If length is 0 or exceeds MAX_STATIC_ARRAY_LENGTH.
|
|
731
|
-
*/
|
|
732
734
|
function array(elemDesc, length) {
|
|
733
735
|
if (length === void 0) {
|
|
734
736
|
const nodeLength = DescriptorFormat.ARRAY_HEADER_SIZE + elemDesc.length;
|
|
735
|
-
if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("
|
|
737
|
+
if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("NODE_LENGTH_TOO_LARGE", `Dynamic array node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}.`);
|
|
736
738
|
const meta24 = 0 << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
|
|
737
739
|
const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + elemDesc.length);
|
|
738
740
|
buf[0] = TypeCode.DYNAMIC_ARRAY;
|
|
@@ -742,10 +744,10 @@ function array(elemDesc, length) {
|
|
|
742
744
|
}
|
|
743
745
|
if (length <= 0 || length > DescriptorFormat.MAX_STATIC_ARRAY_LENGTH) throw new CallciumError("INVALID_ARRAY_LENGTH", `Static array length ${length} out of range. Must be 1–${DescriptorFormat.MAX_STATIC_ARRAY_LENGTH}.`);
|
|
744
746
|
const nodeLength = DescriptorFormat.ARRAY_HEADER_SIZE + elemDesc.length + DescriptorFormat.ARRAY_LENGTH_SIZE;
|
|
745
|
-
if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("
|
|
747
|
+
if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("NODE_LENGTH_TOO_LARGE", `Static array node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}.`);
|
|
746
748
|
const elemStaticWords = extractStaticWords(elemDesc);
|
|
747
749
|
const staticWords = elemStaticWords === 0 ? 0 : length * elemStaticWords;
|
|
748
|
-
if (staticWords > DescriptorFormat.
|
|
750
|
+
if (staticWords > DescriptorFormat.MAX_STATIC_WORDS) throw new CallciumError("STATIC_WORDS_TOO_LARGE", `Static array static words ${staticWords} exceeds maximum ${DescriptorFormat.MAX_STATIC_WORDS}.`);
|
|
749
751
|
const meta24 = staticWords << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
|
|
750
752
|
const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + elemDesc.length + DescriptorFormat.ARRAY_LENGTH_SIZE);
|
|
751
753
|
buf[0] = TypeCode.STATIC_ARRAY;
|
|
@@ -754,19 +756,11 @@ function array(elemDesc, length) {
|
|
|
754
756
|
writeBE16(buf, 4 + elemDesc.length, length);
|
|
755
757
|
return buf;
|
|
756
758
|
}
|
|
757
|
-
/**
|
|
758
|
-
* Build a descriptor node for a tuple.
|
|
759
|
-
*
|
|
760
|
-
* Format: `[0x90][meta:3][fieldCount:be16][field0][field1]...`
|
|
761
|
-
*
|
|
762
|
-
* @param fieldDescs - Descriptor bytes for each field, in order.
|
|
763
|
-
* @throws {CallciumError} If fieldDescs is empty, exceeds MAX_TUPLE_FIELDS, or nodeLength overflows MAX_NODE_LENGTH.
|
|
764
|
-
*/
|
|
765
759
|
function tuple(fieldDescs) {
|
|
766
760
|
if (fieldDescs.length === 0 || fieldDescs.length > DescriptorFormat.MAX_TUPLE_FIELDS) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", `Tuple field count ${fieldDescs.length} out of range. Must be 1–${DescriptorFormat.MAX_TUPLE_FIELDS}.`);
|
|
767
761
|
const totalFieldBytes = fieldDescs.reduce((sum, f) => sum + f.length, 0);
|
|
768
762
|
const nodeLength = DescriptorFormat.TUPLE_HEADER_SIZE + totalFieldBytes;
|
|
769
|
-
if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("
|
|
763
|
+
if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("NODE_LENGTH_TOO_LARGE", `Tuple node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}.`);
|
|
770
764
|
let anyDynamic = false;
|
|
771
765
|
let staticWordsSum = 0;
|
|
772
766
|
for (const field of fieldDescs) {
|
|
@@ -775,7 +769,7 @@ function tuple(fieldDescs) {
|
|
|
775
769
|
staticWordsSum += staticWords;
|
|
776
770
|
}
|
|
777
771
|
const staticWords = anyDynamic ? 0 : staticWordsSum;
|
|
778
|
-
if (staticWords > DescriptorFormat.
|
|
772
|
+
if (staticWords > DescriptorFormat.MAX_STATIC_WORDS) throw new CallciumError("STATIC_WORDS_TOO_LARGE", `Tuple static words ${staticWords} exceeds maximum ${DescriptorFormat.MAX_STATIC_WORDS}.`);
|
|
779
773
|
const meta24 = staticWords << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
|
|
780
774
|
const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + DescriptorFormat.TUPLE_FIELDCOUNT_SIZE + totalFieldBytes);
|
|
781
775
|
buf[0] = TypeCode.TUPLE;
|
|
@@ -790,10 +784,6 @@ function tuple(fieldDescs) {
|
|
|
790
784
|
}
|
|
791
785
|
//#endregion
|
|
792
786
|
//#region src/descriptor-coder.ts
|
|
793
|
-
/**
|
|
794
|
-
* Collect the positions of depth-0 commas in input[start..end).
|
|
795
|
-
* Commas inside `()` or `[]` are skipped.
|
|
796
|
-
*/
|
|
797
787
|
function commaPositions(input, start, end) {
|
|
798
788
|
const positions = [];
|
|
799
789
|
let depth = 0;
|
|
@@ -805,27 +795,21 @@ function commaPositions(input, start, end) {
|
|
|
805
795
|
}
|
|
806
796
|
return positions;
|
|
807
797
|
}
|
|
808
|
-
|
|
809
|
-
* Parse a decimal integer from input[start..end).
|
|
810
|
-
* Returns -1 if the substring is empty or contains non-digit characters.
|
|
811
|
-
*/
|
|
798
|
+
const MAX_TYPE_STRING_NUMBER = 256 ** DescriptorFormat.ARRAY_LENGTH_SIZE - 1;
|
|
812
799
|
function parseUint(input, start, end) {
|
|
813
|
-
if (start >= end)
|
|
814
|
-
if (end - start > 1 && input.charCodeAt(start) === 48)
|
|
800
|
+
if (start >= end) throw new CallciumError("MALFORMED_TYPE_STRING", `Missing number at position ${start}`);
|
|
801
|
+
if (end - start > 1 && input.charCodeAt(start) === 48) throw new CallciumError("MALFORMED_TYPE_STRING", `Zero-padded number at position ${start}`);
|
|
815
802
|
let value = 0;
|
|
816
803
|
for (let i = start; i < end; i++) {
|
|
817
804
|
const code = input.charCodeAt(i);
|
|
818
|
-
if (code < 48 || code > 57)
|
|
805
|
+
if (code < 48 || code > 57) throw new CallciumError("MALFORMED_TYPE_STRING", `Non-digit character at position ${i}`);
|
|
819
806
|
value = value * 10 + (code - 48);
|
|
820
807
|
}
|
|
808
|
+
if (value > MAX_TYPE_STRING_NUMBER) throw new CallciumError("MALFORMED_TYPE_STRING", `Number at position ${start} is too wide`);
|
|
821
809
|
return value;
|
|
822
810
|
}
|
|
823
|
-
/**
|
|
824
|
-
* Parse a tuple literal `(field0,field1,...)` where the opening `(` is at
|
|
825
|
-
* `start` and the substring to parse ends before `end`.
|
|
826
|
-
*/
|
|
827
811
|
function parseTuple(input, start, end) {
|
|
828
|
-
if (input[start] !== "(") throw new CallciumError("
|
|
812
|
+
if (input[start] !== "(") throw new CallciumError("MALFORMED_TYPE_STRING", `Expected '(' at position ${start}`);
|
|
829
813
|
let depth = 0;
|
|
830
814
|
let closePos = -1;
|
|
831
815
|
for (let i = start; i < end; i++) if (input[i] === "(") depth++;
|
|
@@ -836,7 +820,8 @@ function parseTuple(input, start, end) {
|
|
|
836
820
|
break;
|
|
837
821
|
}
|
|
838
822
|
}
|
|
839
|
-
if (closePos === -1) throw new CallciumError("
|
|
823
|
+
if (closePos === -1) throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '(' at position ${start}`);
|
|
824
|
+
if (closePos !== end - 1) throw new CallciumError("MALFORMED_TYPE_STRING", `Unexpected characters after ')' at position ${closePos + 1}`);
|
|
840
825
|
const innerStart = start + 1;
|
|
841
826
|
const innerEnd = closePos;
|
|
842
827
|
const commas = commaPositions(input, innerStart, innerEnd);
|
|
@@ -847,10 +832,9 @@ function parseTuple(input, start, end) {
|
|
|
847
832
|
segStart = comma + 1;
|
|
848
833
|
}
|
|
849
834
|
segments.push([segStart, innerEnd]);
|
|
850
|
-
if (segments.length === 1 && segments[0][0] === segments[0][1])
|
|
835
|
+
if (segments.length === 1 && segments[0][0] === segments[0][1]) return tuple([]);
|
|
851
836
|
return tuple(segments.map(([fieldStart, fieldEnd]) => parseType(input, fieldStart, fieldEnd)));
|
|
852
837
|
}
|
|
853
|
-
/** Parse the base type (no array suffixes) from input[start..end). */
|
|
854
838
|
function parseBaseType(input, start, end) {
|
|
855
839
|
const segment = input.slice(start, end);
|
|
856
840
|
if (segment === "address") return address();
|
|
@@ -858,63 +842,31 @@ function parseBaseType(input, start, end) {
|
|
|
858
842
|
if (segment === "function") return function_();
|
|
859
843
|
if (segment === "bytes") return bytes();
|
|
860
844
|
if (segment === "string") return string_();
|
|
861
|
-
if (segment === "uint256") return uint256();
|
|
862
|
-
if (segment === "int256") return intN(256);
|
|
863
845
|
if (segment.startsWith("(")) return parseTuple(input, start, end);
|
|
864
|
-
if (segment.startsWith("uint"))
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
return uintN(bits);
|
|
868
|
-
}
|
|
869
|
-
if (segment.startsWith("int")) {
|
|
870
|
-
const bits = parseUint(input, start + 3, end);
|
|
871
|
-
if (bits === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
|
|
872
|
-
return intN(bits);
|
|
873
|
-
}
|
|
874
|
-
if (segment.startsWith("bytes")) {
|
|
875
|
-
const n = parseUint(input, start + 5, end);
|
|
876
|
-
if (n === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
|
|
877
|
-
return bytesN(n);
|
|
878
|
-
}
|
|
846
|
+
if (segment.startsWith("uint")) return uintN(parseUint(input, start + 4, end));
|
|
847
|
+
if (segment.startsWith("int")) return intN(parseUint(input, start + 3, end));
|
|
848
|
+
if (segment.startsWith("bytes")) return bytesN(parseUint(input, start + 5, end));
|
|
879
849
|
throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
|
|
880
850
|
}
|
|
881
|
-
/**
|
|
882
|
-
* Parse a type segment from input[start..end).
|
|
883
|
-
*
|
|
884
|
-
* Scans backward from end to collect all `[...]` suffixes, identifies the
|
|
885
|
-
* base type extent, parses the base, then applies suffixes left-to-right.
|
|
886
|
-
*/
|
|
887
851
|
function parseType(input, start, end) {
|
|
888
|
-
if (start >= end) throw new CallciumError("
|
|
852
|
+
if (start >= end) throw new CallciumError("MALFORMED_TYPE_STRING", "Empty type segment");
|
|
889
853
|
const suffixes = [];
|
|
890
854
|
let baseEnd = end;
|
|
891
855
|
while (baseEnd > start && input[baseEnd - 1] === "]") {
|
|
892
856
|
const closePos = baseEnd - 1;
|
|
893
857
|
let openPos = closePos - 1;
|
|
894
858
|
while (openPos > start && input[openPos] !== "[") openPos--;
|
|
895
|
-
if (input[openPos] !== "[") throw new CallciumError("
|
|
859
|
+
if (input[openPos] !== "[") throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched ']' at position ${closePos}`);
|
|
896
860
|
const innerStart = openPos + 1;
|
|
897
861
|
const innerEnd = closePos;
|
|
898
|
-
|
|
899
|
-
else {
|
|
900
|
-
const length = parseUint(input, innerStart, innerEnd);
|
|
901
|
-
if (length === -1) throw new CallciumError("INVALID_TYPE_STRING", `Invalid array length at position ${innerStart}`);
|
|
902
|
-
suffixes.unshift(length);
|
|
903
|
-
}
|
|
862
|
+
suffixes.unshift(innerStart === innerEnd ? void 0 : parseUint(input, innerStart, innerEnd));
|
|
904
863
|
baseEnd = openPos;
|
|
905
864
|
}
|
|
865
|
+
if (input[start] !== "(" && input.slice(start, baseEnd).includes("[")) throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '[' in '${input.slice(start, baseEnd)}'`);
|
|
906
866
|
let desc = parseBaseType(input, start, baseEnd);
|
|
907
867
|
for (const length of suffixes) desc = array(desc, length);
|
|
908
868
|
return desc;
|
|
909
869
|
}
|
|
910
|
-
/**
|
|
911
|
-
* Encode a comma-separated list of ABI type strings into a binary descriptor.
|
|
912
|
-
*
|
|
913
|
-
* @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[],string"`.
|
|
914
|
-
* @returns Binary descriptor bytes starting with the version+paramCount header.
|
|
915
|
-
* @throws {CallciumError} With code `INVALID_TYPE_STRING` for malformed input.
|
|
916
|
-
* @throws {CallciumError} With code `UNKNOWN_TYPE` for unrecognised type names.
|
|
917
|
-
*/
|
|
918
870
|
function fromTypes(typesCsv) {
|
|
919
871
|
if (typesCsv === "") return new Uint8Array([DescriptorFormat.VERSION, 0]);
|
|
920
872
|
const commas = commaPositions(typesCsv, 0, typesCsv.length);
|
|
@@ -926,7 +878,7 @@ function fromTypes(typesCsv) {
|
|
|
926
878
|
}
|
|
927
879
|
segments.push([segStart, typesCsv.length]);
|
|
928
880
|
const paramDescs = segments.map(([start, end]) => parseType(typesCsv, start, end));
|
|
929
|
-
if (paramDescs.length > DescriptorFormat.MAX_PARAMS) throw new CallciumError("
|
|
881
|
+
if (paramDescs.length > DescriptorFormat.MAX_PARAMS) throw new CallciumError("TOO_MANY_PARAMS", `Parameter count ${paramDescs.length} exceeds maximum ${DescriptorFormat.MAX_PARAMS}.`);
|
|
930
882
|
const totalBytes = paramDescs.reduce((sum, d) => sum + d.length, 0);
|
|
931
883
|
const result = new Uint8Array(DescriptorFormat.HEADER_SIZE + totalBytes);
|
|
932
884
|
result[0] = DescriptorFormat.VERSION;
|
|
@@ -938,7 +890,6 @@ function fromTypes(typesCsv) {
|
|
|
938
890
|
}
|
|
939
891
|
return result;
|
|
940
892
|
}
|
|
941
|
-
/** Reconstruct a Solidity type string from a descriptor node at the given offset. */
|
|
942
893
|
function nodeToTypeString(desc, offset) {
|
|
943
894
|
const typeCode = desc[offset];
|
|
944
895
|
if (typeCode === TypeCode.TUPLE) {
|
|
@@ -958,15 +909,6 @@ function nodeToTypeString(desc, offset) {
|
|
|
958
909
|
if (typeCode === TypeCode.DYNAMIC_ARRAY) return `${nodeToTypeString(desc, Descriptor.arrayElementOffset(offset))}[]`;
|
|
959
910
|
return lookupTypeCode(typeCode).label;
|
|
960
911
|
}
|
|
961
|
-
/**
|
|
962
|
-
* Reconstruct a comma-separated list of ABI type strings from a binary descriptor.
|
|
963
|
-
*
|
|
964
|
-
* Inverse of `fromTypes`: `toTypes(fromTypes(s))` returns `s` for any valid input.
|
|
965
|
-
*
|
|
966
|
-
* @param desc - Binary descriptor bytes (with version+paramCount header).
|
|
967
|
-
* @returns Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[]"`.
|
|
968
|
-
* @throws {CallciumError} If the descriptor is malformed.
|
|
969
|
-
*/
|
|
970
912
|
function toTypes(desc) {
|
|
971
913
|
const count = Descriptor.paramCount(desc);
|
|
972
914
|
const types = [];
|
|
@@ -977,23 +919,15 @@ function toTypes(desc) {
|
|
|
977
919
|
}
|
|
978
920
|
return types.join(",");
|
|
979
921
|
}
|
|
980
|
-
/** Recursively parse a single descriptor node starting at offset. */
|
|
981
922
|
function parseNode(data, offset, depth) {
|
|
982
923
|
if (offset >= data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
|
|
983
924
|
const code = data[offset];
|
|
984
925
|
const info = classifyTypeCode(code);
|
|
985
926
|
const metaOffset = offset + DescriptorFormat.TYPECODE_SIZE;
|
|
986
927
|
if (info.typeClass === "elementary") return {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
isDynamic: info.isDynamic,
|
|
991
|
-
staticSize: info.isDynamic ? 0 : 32,
|
|
992
|
-
span: {
|
|
993
|
-
start: offset,
|
|
994
|
-
end: metaOffset
|
|
995
|
-
}
|
|
996
|
-
},
|
|
928
|
+
typeCode: code,
|
|
929
|
+
isDynamic: info.isDynamic,
|
|
930
|
+
staticSize: info.isDynamic ? 0 : 32,
|
|
997
931
|
next: metaOffset
|
|
998
932
|
};
|
|
999
933
|
if (depth > DescriptorFormat.MAX_NESTING_DEPTH) throw new CallciumError("NESTING_TOO_DEEP", `Composite nesting exceeds maximum depth ${DescriptorFormat.MAX_NESTING_DEPTH}`, offset);
|
|
@@ -1003,7 +937,7 @@ function parseNode(data, offset, depth) {
|
|
|
1003
937
|
const staticWords = meta >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
|
|
1004
938
|
const nodeLength = meta & DescriptorFormat.META_NODE_LENGTH_MASK;
|
|
1005
939
|
const minHeader = info.typeClass === "tuple" ? DescriptorFormat.TUPLE_HEADER_SIZE : DescriptorFormat.ARRAY_HEADER_SIZE;
|
|
1006
|
-
if (nodeLength < minHeader) throw new CallciumError("
|
|
940
|
+
if (nodeLength < minHeader) throw new CallciumError("NODE_LENGTH_TOO_SMALL", `Composite node length ${nodeLength} is smaller than minimum header ${minHeader}`, offset);
|
|
1007
941
|
if (offset + nodeLength > data.length) throw new CallciumError("NODE_OVERFLOW", "Composite node extends beyond descriptor", offset);
|
|
1008
942
|
const isDynamic = staticWords === 0;
|
|
1009
943
|
const staticSize = isDynamic ? 0 : staticWords * 32;
|
|
@@ -1014,89 +948,54 @@ function parseNode(data, offset, depth) {
|
|
|
1014
948
|
const fieldCount = readU16(data, fieldCountOffset);
|
|
1015
949
|
if (fieldCount === 0) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", "Tuple must have at least one field", offset);
|
|
1016
950
|
if (fieldCount > DescriptorFormat.MAX_TUPLE_FIELDS) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", `Tuple field count ${fieldCount} exceeds maximum ${DescriptorFormat.MAX_TUPLE_FIELDS}`, offset);
|
|
1017
|
-
const fields = [];
|
|
1018
951
|
let cursor = offset + DescriptorFormat.TUPLE_HEADER_SIZE;
|
|
1019
|
-
for (let i = 0; i < fieldCount; i++)
|
|
1020
|
-
const result = parseNode(data, cursor, depth + 1);
|
|
1021
|
-
fields.push(result.node);
|
|
1022
|
-
cursor = result.next;
|
|
1023
|
-
}
|
|
952
|
+
for (let i = 0; i < fieldCount; i++) cursor = parseNode(data, cursor, depth + 1).next;
|
|
1024
953
|
return {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
isDynamic,
|
|
1029
|
-
staticSize,
|
|
1030
|
-
fields,
|
|
1031
|
-
span: {
|
|
1032
|
-
start: offset,
|
|
1033
|
-
end: nodeEnd
|
|
1034
|
-
}
|
|
1035
|
-
},
|
|
954
|
+
typeCode: code,
|
|
955
|
+
isDynamic,
|
|
956
|
+
staticSize,
|
|
1036
957
|
next: nodeEnd
|
|
1037
958
|
};
|
|
1038
959
|
}
|
|
1039
960
|
if (info.typeClass === "staticArray") {
|
|
1040
|
-
const
|
|
1041
|
-
const lengthOffset = elemResult.next;
|
|
961
|
+
const lengthOffset = parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1).next;
|
|
1042
962
|
if (lengthOffset + DescriptorFormat.ARRAY_LENGTH_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Missing static array length suffix", offset);
|
|
1043
963
|
const length = readU16(data, lengthOffset);
|
|
1044
964
|
if (length === 0) throw new CallciumError("INVALID_ARRAY_LENGTH", "Static array length must be greater than zero", offset);
|
|
1045
965
|
if (length > DescriptorFormat.MAX_STATIC_ARRAY_LENGTH) throw new CallciumError("INVALID_ARRAY_LENGTH", `Static array length ${length} exceeds maximum ${DescriptorFormat.MAX_STATIC_ARRAY_LENGTH}`, offset);
|
|
1046
966
|
return {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
isDynamic,
|
|
1051
|
-
staticSize,
|
|
1052
|
-
element: elemResult.node,
|
|
1053
|
-
length,
|
|
1054
|
-
span: {
|
|
1055
|
-
start: offset,
|
|
1056
|
-
end: nodeEnd
|
|
1057
|
-
}
|
|
1058
|
-
},
|
|
967
|
+
typeCode: code,
|
|
968
|
+
isDynamic,
|
|
969
|
+
staticSize,
|
|
1059
970
|
next: nodeEnd
|
|
1060
971
|
};
|
|
1061
972
|
}
|
|
973
|
+
parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
|
|
1062
974
|
return {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
isDynamic: true,
|
|
1067
|
-
staticSize: 0,
|
|
1068
|
-
element: parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1).node,
|
|
1069
|
-
span: {
|
|
1070
|
-
start: offset,
|
|
1071
|
-
end: nodeEnd
|
|
1072
|
-
}
|
|
1073
|
-
},
|
|
975
|
+
typeCode: code,
|
|
976
|
+
isDynamic: true,
|
|
977
|
+
staticSize: 0,
|
|
1074
978
|
next: nodeEnd
|
|
1075
979
|
};
|
|
1076
980
|
}
|
|
1077
|
-
/** Convert a zero-based param index to a BE16 hex path step. */
|
|
1078
981
|
function indexToPath(index) {
|
|
1079
982
|
return `0x${index.toString(16).padStart(4, "0")}`;
|
|
1080
983
|
}
|
|
1081
|
-
/** Decode a binary descriptor blob, returning the structural representation and AST. */
|
|
1082
984
|
function decodeDescriptor(data) {
|
|
1083
985
|
if (data.length < 1) throw new CallciumError("MALFORMED_HEADER", "Descriptor is empty");
|
|
1084
986
|
const version = data[0];
|
|
1085
987
|
if (version !== DescriptorFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${DescriptorFormat.VERSION})`);
|
|
1086
988
|
if (data.length < DescriptorFormat.HEADER_SIZE) throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
|
|
1087
|
-
const declaredCount = data[1];
|
|
1088
989
|
const params = [];
|
|
1089
|
-
const tree = [];
|
|
1090
990
|
let cursor = DescriptorFormat.HEADER_SIZE;
|
|
1091
991
|
while (cursor < data.length) {
|
|
1092
|
-
if (params.length >= DescriptorFormat.MAX_PARAMS) throw new CallciumError("
|
|
1093
|
-
const {
|
|
1094
|
-
tree.push(node);
|
|
992
|
+
if (params.length >= DescriptorFormat.MAX_PARAMS) throw new CallciumError("TOO_MANY_PARAMS", `Descriptor exceeds the maximum of ${DescriptorFormat.MAX_PARAMS} top-level params`);
|
|
993
|
+
const { typeCode, isDynamic, staticSize, next } = parseNode(data, cursor, 1);
|
|
1095
994
|
params.push({
|
|
1096
995
|
index: params.length,
|
|
1097
|
-
typeCode
|
|
1098
|
-
isDynamic
|
|
1099
|
-
staticSize
|
|
996
|
+
typeCode,
|
|
997
|
+
isDynamic,
|
|
998
|
+
staticSize,
|
|
1100
999
|
path: indexToPath(params.length),
|
|
1101
1000
|
span: {
|
|
1102
1001
|
start: cursor,
|
|
@@ -1105,34 +1004,146 @@ function decodeDescriptor(data) {
|
|
|
1105
1004
|
});
|
|
1106
1005
|
cursor = next;
|
|
1107
1006
|
}
|
|
1007
|
+
const declaredCount = data[1];
|
|
1108
1008
|
if (params.length !== declaredCount) throw new CallciumError("PARAM_COUNT_MISMATCH", `Header declares ${declaredCount} params but ${params.length} were parsed`);
|
|
1109
|
-
return {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
},
|
|
1114
|
-
tree
|
|
1115
|
-
};
|
|
1009
|
+
return { descriptor: {
|
|
1010
|
+
version,
|
|
1011
|
+
params
|
|
1012
|
+
} };
|
|
1116
1013
|
}
|
|
1117
|
-
/** Encode and decode descriptors. */
|
|
1118
1014
|
const DescriptorCoder = {
|
|
1119
1015
|
fromTypes,
|
|
1120
1016
|
toTypes
|
|
1121
1017
|
};
|
|
1122
1018
|
//#endregion
|
|
1019
|
+
//#region src/operators.ts
|
|
1020
|
+
function toBigInt(bytes, offset = 0) {
|
|
1021
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
|
|
1022
|
+
return view.getBigUint64(0, false) << 192n | view.getBigUint64(8, false) << 128n | view.getBigUint64(16, false) << 64n | view.getBigUint64(24, false);
|
|
1023
|
+
}
|
|
1024
|
+
function isSigned(typeCode) {
|
|
1025
|
+
return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
|
|
1026
|
+
}
|
|
1027
|
+
function isLeftAligned(typeCode) {
|
|
1028
|
+
return typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX || typeCode === TypeCode.FUNCTION;
|
|
1029
|
+
}
|
|
1030
|
+
function canonicalize(value, typeCode) {
|
|
1031
|
+
if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
|
|
1032
|
+
const bits = BigInt(typeCode * 8);
|
|
1033
|
+
return bits === 256n ? value : value & (1n << bits) - 1n;
|
|
1034
|
+
}
|
|
1035
|
+
if (typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX) {
|
|
1036
|
+
const bits = (typeCode - TypeCode.INT_MIN + 1) * 8;
|
|
1037
|
+
return bits === 256 ? value : BigInt.asUintN(256, BigInt.asIntN(bits, value));
|
|
1038
|
+
}
|
|
1039
|
+
if (typeCode === TypeCode.ADDRESS) return value & (1n << 160n) - 1n;
|
|
1040
|
+
if (typeCode === TypeCode.BOOL) return value & 1n;
|
|
1041
|
+
if (typeCode === TypeCode.FUNCTION) return value >> 64n << 64n;
|
|
1042
|
+
if (typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX) {
|
|
1043
|
+
const n = typeCode - TypeCode.FIXED_BYTES_MIN + 1;
|
|
1044
|
+
if (n === 32) return value;
|
|
1045
|
+
const padBits = BigInt((32 - n) * 8);
|
|
1046
|
+
return value >> padBits << padBits;
|
|
1047
|
+
}
|
|
1048
|
+
return value;
|
|
1049
|
+
}
|
|
1050
|
+
function isLengthOp(opCode) {
|
|
1051
|
+
const base = opCode & ~Op.NOT;
|
|
1052
|
+
return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
|
|
1053
|
+
}
|
|
1054
|
+
function lengthToValueOp(base) {
|
|
1055
|
+
if (base === Op.LENGTH_EQ) return Op.EQ;
|
|
1056
|
+
if (base === Op.LENGTH_GT) return Op.GT;
|
|
1057
|
+
if (base === Op.LENGTH_LT) return Op.LT;
|
|
1058
|
+
if (base === Op.LENGTH_GTE) return Op.GTE;
|
|
1059
|
+
if (base === Op.LENGTH_LTE) return Op.LTE;
|
|
1060
|
+
return Op.BETWEEN;
|
|
1061
|
+
}
|
|
1062
|
+
function isLengthValidType(typeCode) {
|
|
1063
|
+
return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
|
|
1064
|
+
}
|
|
1065
|
+
function compareTyped(a, b, typeCode) {
|
|
1066
|
+
if (isSigned(typeCode)) {
|
|
1067
|
+
const sa = BigInt.asIntN(256, a);
|
|
1068
|
+
const sb = BigInt.asIntN(256, b);
|
|
1069
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
1070
|
+
}
|
|
1071
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
1072
|
+
}
|
|
1073
|
+
function isIn(value, data) {
|
|
1074
|
+
const count = data.length / 32;
|
|
1075
|
+
let lo = 0;
|
|
1076
|
+
let hi = count - 1;
|
|
1077
|
+
while (lo <= hi) {
|
|
1078
|
+
const mid = lo + hi >>> 1;
|
|
1079
|
+
const elem = toBigInt(data, mid * 32);
|
|
1080
|
+
if (value === elem) return true;
|
|
1081
|
+
if (value < elem) hi = mid - 1;
|
|
1082
|
+
else lo = mid + 1;
|
|
1083
|
+
}
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
1087
|
+
let base = opCode & ~Op.NOT;
|
|
1088
|
+
if (isLengthOp(base)) {
|
|
1089
|
+
value = BigInt(valueLength);
|
|
1090
|
+
typeCode = TypeCode.UINT_MAX;
|
|
1091
|
+
base = lengthToValueOp(base);
|
|
1092
|
+
}
|
|
1093
|
+
let result;
|
|
1094
|
+
switch (base) {
|
|
1095
|
+
case Op.EQ:
|
|
1096
|
+
result = value === toBigInt(operandData, 0);
|
|
1097
|
+
break;
|
|
1098
|
+
case Op.GT:
|
|
1099
|
+
result = compareTyped(value, toBigInt(operandData, 0), typeCode) > 0;
|
|
1100
|
+
break;
|
|
1101
|
+
case Op.LT:
|
|
1102
|
+
result = compareTyped(value, toBigInt(operandData, 0), typeCode) < 0;
|
|
1103
|
+
break;
|
|
1104
|
+
case Op.GTE:
|
|
1105
|
+
result = compareTyped(value, toBigInt(operandData, 0), typeCode) >= 0;
|
|
1106
|
+
break;
|
|
1107
|
+
case Op.LTE:
|
|
1108
|
+
result = compareTyped(value, toBigInt(operandData, 0), typeCode) <= 0;
|
|
1109
|
+
break;
|
|
1110
|
+
case Op.BETWEEN: {
|
|
1111
|
+
const min = toBigInt(operandData, 0);
|
|
1112
|
+
const max = toBigInt(operandData, 32);
|
|
1113
|
+
result = compareTyped(value, min, typeCode) >= 0 && compareTyped(value, max, typeCode) <= 0;
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
case Op.IN:
|
|
1117
|
+
result = isIn(value, operandData);
|
|
1118
|
+
break;
|
|
1119
|
+
case Op.BITMASK_ALL: {
|
|
1120
|
+
const mask = toBigInt(operandData, 0);
|
|
1121
|
+
result = (value & mask) === mask;
|
|
1122
|
+
break;
|
|
1123
|
+
}
|
|
1124
|
+
case Op.BITMASK_ANY: {
|
|
1125
|
+
const mask = toBigInt(operandData, 0);
|
|
1126
|
+
result = (value & mask) !== 0n;
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
case Op.BITMASK_NONE: {
|
|
1130
|
+
const mask = toBigInt(operandData, 0);
|
|
1131
|
+
result = (value & mask) === 0n;
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
default: throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
|
|
1135
|
+
}
|
|
1136
|
+
return (opCode & Op.NOT) !== 0 ? !result : result;
|
|
1137
|
+
}
|
|
1138
|
+
//#endregion
|
|
1123
1139
|
//#region src/policy-coder.ts
|
|
1124
|
-
/**
|
|
1125
|
-
* Parse a BE16-encoded hex path into an array of step values.
|
|
1126
|
-
* @param path - 0x-prefixed hex string containing BE16-encoded path steps.
|
|
1127
|
-
* @returns Array of numeric step values.
|
|
1128
|
-
*/
|
|
1129
1140
|
function parsePathSteps(path) {
|
|
1130
|
-
const
|
|
1141
|
+
const bytes = hexToBytes(path);
|
|
1142
|
+
if (bytes.length % PolicyFormat.PATH_STEP_SIZE !== 0) throw new CallciumError("MALFORMED_PATH", `Path byte length ${bytes.length} is not a whole number of steps`);
|
|
1131
1143
|
const steps = [];
|
|
1132
|
-
for (let
|
|
1144
|
+
for (let offset = 0; offset < bytes.length; offset += PolicyFormat.PATH_STEP_SIZE) steps.push(readU16(bytes, offset));
|
|
1133
1145
|
return steps;
|
|
1134
1146
|
}
|
|
1135
|
-
/** Wrap a value with its byte span for positional tracking. */
|
|
1136
1147
|
function field(value, start, end) {
|
|
1137
1148
|
return {
|
|
1138
1149
|
value,
|
|
@@ -1142,7 +1153,60 @@ function field(value, start, end) {
|
|
|
1142
1153
|
}
|
|
1143
1154
|
};
|
|
1144
1155
|
}
|
|
1145
|
-
|
|
1156
|
+
function resolveHintSize(data, hintStart, ruleEnd, ruleOffset) {
|
|
1157
|
+
const overrun = () => {
|
|
1158
|
+
throw new CallciumError("RULE_FIELD_OUT_OF_BOUNDS", "Rule is too small to hold a hint block", ruleOffset);
|
|
1159
|
+
};
|
|
1160
|
+
if (hintStart + PolicyFormat.HINT_HEADER_SIZE > ruleEnd) overrun();
|
|
1161
|
+
const header = data[hintStart];
|
|
1162
|
+
const hopsEnd = hintStart + PolicyFormat.HINT_HEADER_SIZE + (header & PolicyFormat.HINT_HOP_COUNT_MASK) * PolicyFormat.HINT_HOP_SIZE;
|
|
1163
|
+
let hintSize = hopsEnd - hintStart + PolicyFormat.HINT_TARGET_SIZE;
|
|
1164
|
+
if (header >> PolicyFormat.HINT_KIND_SHIFT !== PolicyFormat.HINT_KIND_NONE) {
|
|
1165
|
+
const suffixHeaderOffset = hopsEnd + PolicyFormat.HINT_FRAME_PREFIX_SIZE;
|
|
1166
|
+
if (suffixHeaderOffset >= ruleEnd) overrun();
|
|
1167
|
+
const suffixHeader = data[suffixHeaderOffset];
|
|
1168
|
+
hintSize += PolicyFormat.HINT_FRAME_PREFIX_SIZE + PolicyFormat.HINT_HEADER_SIZE + (suffixHeader & PolicyFormat.HINT_HOP_COUNT_MASK) * PolicyFormat.HINT_HOP_SIZE;
|
|
1169
|
+
}
|
|
1170
|
+
if (hintStart + hintSize > ruleEnd) overrun();
|
|
1171
|
+
return hintSize;
|
|
1172
|
+
}
|
|
1173
|
+
function malformedHint(detail, ruleOffset) {
|
|
1174
|
+
throw new CallciumError("MALFORMED_HINT", detail, ruleOffset);
|
|
1175
|
+
}
|
|
1176
|
+
function validateHops(data, start, end, ruleOffset) {
|
|
1177
|
+
for (let offset = start; offset < end; offset += PolicyFormat.HINT_HOP_SIZE) {
|
|
1178
|
+
const index = readU16(data, offset + PolicyFormat.HINT_HOP_INDEX_OFFSET);
|
|
1179
|
+
if (index === PolicyFormat.HINT_INDEX_RESERVED) malformedHint("Hop index is a reserved value", ruleOffset);
|
|
1180
|
+
const meta = readU16(data, offset + PolicyFormat.HINT_HOP_META_OFFSET);
|
|
1181
|
+
if (index === PolicyFormat.HINT_NO_INDEX) {
|
|
1182
|
+
if (meta !== 0) malformedHint("Plain hop carries a meta word", ruleOffset);
|
|
1183
|
+
} else {
|
|
1184
|
+
if (readU32(data, offset) !== 0) malformedHint("Element hop carries a delta", ruleOffset);
|
|
1185
|
+
if ((meta & PolicyFormat.HINT_META_RESERVED_MASK) !== 0) malformedHint("Hop meta carries reserved bits", ruleOffset);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
function validateHint(data, hintStart, hintSize, ruleOffset) {
|
|
1190
|
+
const header = data[hintStart];
|
|
1191
|
+
const kind = header >> PolicyFormat.HINT_KIND_SHIFT;
|
|
1192
|
+
if (kind > PolicyFormat.HINT_KIND_MAX) malformedHint("Header kind is a reserved value", ruleOffset);
|
|
1193
|
+
const hopsStart = hintStart + PolicyFormat.HINT_HEADER_SIZE;
|
|
1194
|
+
const hopsEnd = hopsStart + (header & PolicyFormat.HINT_HOP_COUNT_MASK) * PolicyFormat.HINT_HOP_SIZE;
|
|
1195
|
+
validateHops(data, hopsStart, hopsEnd, ruleOffset);
|
|
1196
|
+
const targetOffset = hintStart + hintSize - PolicyFormat.HINT_TARGET_SIZE;
|
|
1197
|
+
if (kind !== PolicyFormat.HINT_KIND_NONE) {
|
|
1198
|
+
if ((readU16(data, hopsEnd + PolicyFormat.HINT_FRAME_META_OFFSET) & PolicyFormat.HINT_META_RESERVED_MASK) !== 0) malformedHint("Frame meta carries reserved bits", ruleOffset);
|
|
1199
|
+
if ((data[hopsEnd + PolicyFormat.HINT_FRAME_PREFIX_SIZE] & PolicyFormat.HINT_SUFFIX_RESERVED_MASK) !== 0) malformedHint("Suffix header carries reserved bits", ruleOffset);
|
|
1200
|
+
validateHops(data, hopsEnd + PolicyFormat.HINT_FRAME_PREFIX_SIZE + PolicyFormat.HINT_HEADER_SIZE, targetOffset, ruleOffset);
|
|
1201
|
+
}
|
|
1202
|
+
const targetMeta = readU16(data, targetOffset + PolicyFormat.HINT_TARGET_META_OFFSET);
|
|
1203
|
+
const targetTypeCode = data[targetOffset + PolicyFormat.HINT_TARGET_TYPECODE_OFFSET];
|
|
1204
|
+
if (!(targetTypeCode === TypeCode.DYNAMIC_ARRAY ? (targetMeta & PolicyFormat.HINT_META_RESERVED_MASK) === 0 : targetMeta === 0)) malformedHint("Target meta carries reserved or unused state", ruleOffset);
|
|
1205
|
+
if (!isAddressableTarget(targetTypeCode)) malformedHint("Target type code addresses no value", ruleOffset);
|
|
1206
|
+
}
|
|
1207
|
+
function hintTargetTypeCode(data, hintStart, hintSize) {
|
|
1208
|
+
return data[hintStart + hintSize - PolicyFormat.HINT_TARGET_SIZE + PolicyFormat.HINT_TARGET_TYPECODE_OFFSET];
|
|
1209
|
+
}
|
|
1146
1210
|
function decodePolicy(blob) {
|
|
1147
1211
|
const data = hexToBytes(blob);
|
|
1148
1212
|
if (data.length < PolicyFormat.DESC_OFFSET + 1) throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short");
|
|
@@ -1150,10 +1214,10 @@ function decodePolicy(blob) {
|
|
|
1150
1214
|
const version = headerByte & PolicyFormat.VERSION_MASK;
|
|
1151
1215
|
if (version !== PolicyFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${PolicyFormat.VERSION})`);
|
|
1152
1216
|
if ((headerByte & PolicyFormat.RESERVED_MASK) !== 0) throw new CallciumError("MALFORMED_HEADER", "Reserved header bits must be zero");
|
|
1153
|
-
const isSelectorless = (headerByte & PolicyFormat.FLAG_NO_SELECTOR) !== 0;
|
|
1154
1217
|
const selectorStart = PolicyFormat.SELECTOR_OFFSET;
|
|
1155
1218
|
const selectorEnd = selectorStart + PolicyFormat.SELECTOR_SIZE;
|
|
1156
1219
|
const selectorHex = toHex(data, selectorStart, selectorEnd);
|
|
1220
|
+
const isSelectorless = (headerByte & PolicyFormat.FLAG_NO_SELECTOR) !== 0;
|
|
1157
1221
|
if (isSelectorless && selectorHex !== "0x00000000") throw new CallciumError("MALFORMED_HEADER", "Selectorless policy must have a zeroed selector slot");
|
|
1158
1222
|
const descLengthStart = PolicyFormat.DESC_LENGTH_OFFSET;
|
|
1159
1223
|
const descLengthValue = readU16(data, descLengthStart);
|
|
@@ -1162,7 +1226,7 @@ function decodePolicy(blob) {
|
|
|
1162
1226
|
const descEnd = descStart + descLengthValue;
|
|
1163
1227
|
const groupCountOffset = descEnd;
|
|
1164
1228
|
if (data.length < groupCountOffset + PolicyFormat.GROUP_COUNT_SIZE) throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short for descriptor and group count");
|
|
1165
|
-
const { descriptor: desc
|
|
1229
|
+
const { descriptor: desc } = decodeDescriptor(data.subarray(descStart, descEnd));
|
|
1166
1230
|
const params = desc.params.map((param) => ({
|
|
1167
1231
|
...param,
|
|
1168
1232
|
span: {
|
|
@@ -1179,55 +1243,66 @@ function decodePolicy(blob) {
|
|
|
1179
1243
|
const groups = [];
|
|
1180
1244
|
for (let groupIndex = 0; groupIndex < groupCountValue; groupIndex++) {
|
|
1181
1245
|
if (offset + PolicyFormat.GROUP_HEADER_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading group header", offset);
|
|
1182
|
-
const ruleCountStart = offset;
|
|
1183
|
-
const ruleCountValue = readU16(data, ruleCountStart);
|
|
1184
1246
|
const groupSizeStart = offset + PolicyFormat.GROUP_RULECOUNT_SIZE;
|
|
1185
1247
|
const groupSizeValue = readU32(data, groupSizeStart);
|
|
1186
1248
|
const groupBodyStart = offset + PolicyFormat.GROUP_HEADER_SIZE;
|
|
1187
1249
|
const groupEnd = groupBodyStart + groupSizeValue;
|
|
1250
|
+
const ruleCountStart = offset;
|
|
1251
|
+
const ruleCountValue = readU16(data, ruleCountStart);
|
|
1188
1252
|
if (ruleCountValue === 0) throw new CallciumError("EMPTY_GROUP", "Group must contain at least one rule", offset);
|
|
1189
|
-
if (groupSizeValue < ruleCountValue * PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("
|
|
1253
|
+
if (groupSizeValue < ruleCountValue * PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("GROUP_TOO_SMALL", "Declared group size is too small for its rule count", offset);
|
|
1190
1254
|
if (groupEnd > data.length) throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
|
|
1191
1255
|
const rules = [];
|
|
1192
1256
|
let ruleOffset = groupBodyStart;
|
|
1193
1257
|
for (let ruleIndex = 0; ruleIndex < ruleCountValue; ruleIndex++) {
|
|
1194
1258
|
if (ruleOffset + PolicyFormat.RULE_SIZE_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading rule size", ruleOffset);
|
|
1195
1259
|
const ruleSizeValue = readU16(data, ruleOffset);
|
|
1196
|
-
if (ruleSizeValue < PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("
|
|
1260
|
+
if (ruleSizeValue < PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("RULE_TOO_SMALL", `Rule size ${ruleSizeValue} is below minimum ${PolicyFormat.RULE_MIN_SIZE}`, ruleOffset);
|
|
1197
1261
|
if (ruleOffset + ruleSizeValue > groupEnd) throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
|
|
1198
1262
|
const scopeOffset = ruleOffset + PolicyFormat.RULE_SCOPE_OFFSET;
|
|
1199
1263
|
const scopeValue = data[scopeOffset];
|
|
1200
1264
|
if (scopeValue !== Scope.CONTEXT && scopeValue !== Scope.CALLDATA) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${scopeValue}`, ruleOffset);
|
|
1201
1265
|
const depthOffset = ruleOffset + PolicyFormat.RULE_DEPTH_OFFSET;
|
|
1202
1266
|
const depthValue = data[depthOffset];
|
|
1203
|
-
if (depthValue === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
|
|
1204
|
-
if (depthValue > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depthValue} exceeds maximum ${Limits.MAX_PATH_DEPTH}`, ruleOffset);
|
|
1205
|
-
if (scopeValue === Scope.CONTEXT && depthValue !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step", ruleOffset);
|
|
1206
1267
|
const pathStart = ruleOffset + PolicyFormat.RULE_PATH_OFFSET;
|
|
1207
1268
|
const pathLength = depthValue * PolicyFormat.PATH_STEP_SIZE;
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1269
|
+
const ruleEnd = ruleOffset + ruleSizeValue;
|
|
1270
|
+
const hintStart = pathStart + pathLength;
|
|
1271
|
+
let hintSize = 0;
|
|
1272
|
+
if (scopeValue === Scope.CALLDATA) {
|
|
1273
|
+
hintSize = resolveHintSize(data, hintStart, ruleEnd, ruleOffset);
|
|
1274
|
+
validateHint(data, hintStart, hintSize, ruleOffset);
|
|
1212
1275
|
}
|
|
1213
|
-
const opCodeOffset =
|
|
1214
|
-
const opCodeValue = data[opCodeOffset];
|
|
1276
|
+
const opCodeOffset = hintStart + hintSize;
|
|
1215
1277
|
const dataLengthOffset = opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE;
|
|
1216
1278
|
const dataLengthValue = readU16(data, dataLengthOffset);
|
|
1217
|
-
const
|
|
1218
|
-
const expectedRuleSize = PolicyFormat.RULE_FIXED_OVERHEAD + pathLength + dataLengthValue;
|
|
1279
|
+
const expectedRuleSize = PolicyFormat.RULE_FIXED_OVERHEAD + pathLength + hintSize + dataLengthValue;
|
|
1219
1280
|
if (ruleSizeValue !== expectedRuleSize) throw new CallciumError("RULE_SIZE_MISMATCH", `Declared rule size ${ruleSizeValue} does not match computed size ${expectedRuleSize}`, ruleOffset);
|
|
1281
|
+
const opCodeValue = data[opCodeOffset];
|
|
1220
1282
|
const opBase = opCodeValue & ~Op.NOT;
|
|
1221
|
-
if (opBase === 0 || !isValidOperatorData(opBase, dataLengthValue)) throw new CallciumError("
|
|
1283
|
+
if (opBase === 0 || !isValidOperatorData(opBase, dataLengthValue)) throw new CallciumError("UNKNOWN_OPERATOR", "Unrecognized or malformed operator", ruleOffset);
|
|
1284
|
+
if (hintSize !== 0) {
|
|
1285
|
+
if (isLengthValidType(hintTargetTypeCode(data, hintStart, hintSize)) !== isLengthOp(opBase)) throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Operator family does not match the type the target declares", ruleOffset);
|
|
1286
|
+
}
|
|
1287
|
+
const dataStart = dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE;
|
|
1222
1288
|
if (opBase === Op.IN) {
|
|
1223
1289
|
const wordCount = dataLengthValue / 32;
|
|
1224
1290
|
for (let word = 1; word < wordCount; word++) if (compareBytes(data.subarray(dataStart + (word - 1) * 32, dataStart + word * 32), data.subarray(dataStart + word * 32, dataStart + (word + 1) * 32)) >= 0) throw new CallciumError("UNSORTED_IN_SET", "IN operands must be strictly ascending", ruleOffset);
|
|
1225
1291
|
}
|
|
1292
|
+
if (depthValue === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
|
|
1293
|
+
if (depthValue > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depthValue} exceeds maximum ${Limits.MAX_PATH_DEPTH}`, ruleOffset);
|
|
1294
|
+
if (scopeValue === Scope.CONTEXT) {
|
|
1295
|
+
if (depthValue !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step", ruleOffset);
|
|
1296
|
+
const contextPropertyId = readU16(data, pathStart);
|
|
1297
|
+
if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Context rule references undefined context property 0x${contextPropertyId.toString(16).padStart(4, "0")}`, ruleOffset);
|
|
1298
|
+
}
|
|
1299
|
+
const pathHex = toHex(data, pathStart, pathStart + pathLength);
|
|
1226
1300
|
rules.push({
|
|
1227
1301
|
ruleSize: field(ruleSizeValue, ruleOffset, ruleOffset + PolicyFormat.RULE_SIZE_SIZE),
|
|
1228
1302
|
scope: field(scopeValue, scopeOffset, scopeOffset + 1),
|
|
1229
1303
|
pathDepth: field(depthValue, depthOffset, depthOffset + 1),
|
|
1230
1304
|
path: field(pathHex, pathStart, pathStart + pathLength),
|
|
1305
|
+
...hintSize > 0 && { hint: field(toHex(data, hintStart, hintStart + hintSize), hintStart, hintStart + hintSize) },
|
|
1231
1306
|
opCode: field(opCodeValue, opCodeOffset, opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE),
|
|
1232
1307
|
dataLength: field(dataLengthValue, dataLengthOffset, dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE),
|
|
1233
1308
|
data: field(toHex(data, dataStart, dataStart + dataLengthValue), dataStart, dataStart + dataLengthValue),
|
|
@@ -1277,25 +1352,30 @@ function decodePolicy(blob) {
|
|
|
1277
1352
|
version,
|
|
1278
1353
|
isSelectorless
|
|
1279
1354
|
},
|
|
1280
|
-
|
|
1355
|
+
data
|
|
1281
1356
|
};
|
|
1282
1357
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1358
|
+
function flattenConstraint(constraint, desc) {
|
|
1359
|
+
const operators = constraint.operators.map(hexToBytes);
|
|
1360
|
+
for (const operator of operators) if (operator.length < 1) throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode).");
|
|
1285
1361
|
const path = hexToBytes(constraint.path);
|
|
1286
|
-
|
|
1362
|
+
if (path.length === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step.");
|
|
1363
|
+
if ((path.length & 1) !== 0) throw new CallciumError("MALFORMED_PATH", "Path byte length must be even.");
|
|
1364
|
+
const depth = path.length / 2;
|
|
1365
|
+
if (depth > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${Limits.MAX_PATH_DEPTH}.`);
|
|
1366
|
+
const hint = constraint.scope === Scope.CALLDATA ? Descriptor.compileHint(desc, parsePathSteps(constraint.path)) : new Uint8Array(0);
|
|
1367
|
+
return operators.map((operator) => ({
|
|
1287
1368
|
scope: constraint.scope,
|
|
1288
1369
|
path,
|
|
1289
|
-
operator
|
|
1370
|
+
operator,
|
|
1371
|
+
hint
|
|
1290
1372
|
}));
|
|
1291
1373
|
}
|
|
1292
|
-
/** Compare two byte arrays lexicographically. */
|
|
1293
1374
|
function compareBytes(a, b) {
|
|
1294
1375
|
const length = Math.min(a.length, b.length);
|
|
1295
1376
|
for (let i = 0; i < length; i++) if (a[i] !== b[i]) return a[i] - b[i];
|
|
1296
1377
|
return a.length - b.length;
|
|
1297
1378
|
}
|
|
1298
|
-
/** Sort rules by (scope, pathDepth, pathBytes, operatorBytes). */
|
|
1299
1379
|
function sortRules(rules) {
|
|
1300
1380
|
rules.sort((a, b) => {
|
|
1301
1381
|
if (a.scope !== b.scope) return a.scope - b.scope;
|
|
@@ -1307,30 +1387,24 @@ function sortRules(rules) {
|
|
|
1307
1387
|
return compareBytes(a.operator, b.operator);
|
|
1308
1388
|
});
|
|
1309
1389
|
}
|
|
1310
|
-
/** Serialize a single rule to its wire format bytes. */
|
|
1311
1390
|
function encodeRule(rule) {
|
|
1312
|
-
if (rule.path.length === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step.");
|
|
1313
|
-
if ((rule.path.length & 1) !== 0) throw new CallciumError("INVALID_PATH", "Path byte length must be even.");
|
|
1314
|
-
if (rule.operator.length < 1) throw new CallciumError("INVALID_OPERATOR", "Operator must have at least one byte (opcode).");
|
|
1315
1391
|
const depth = rule.path.length / 2;
|
|
1316
|
-
if (depth > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${Limits.MAX_PATH_DEPTH}.`);
|
|
1317
1392
|
if (rule.scope === Scope.CONTEXT && depth !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step.");
|
|
1318
|
-
const opCode = rule.operator[0];
|
|
1319
1393
|
const data = rule.operator.subarray(1);
|
|
1320
|
-
const ruleSize = PolicyFormat.RULE_FIXED_OVERHEAD + rule.path.length + data.length;
|
|
1394
|
+
const ruleSize = PolicyFormat.RULE_FIXED_OVERHEAD + rule.path.length + rule.hint.length + data.length;
|
|
1321
1395
|
if (ruleSize > 65535) throw new CallciumError("RULE_SIZE_OVERFLOW", `Rule size ${ruleSize} exceeds maximum 65535`);
|
|
1322
1396
|
const buf = new Uint8Array(ruleSize);
|
|
1323
1397
|
writeBE16(buf, 0, ruleSize);
|
|
1324
1398
|
buf[2] = rule.scope;
|
|
1325
1399
|
buf[3] = depth;
|
|
1326
1400
|
buf.set(rule.path, 4);
|
|
1327
|
-
|
|
1328
|
-
|
|
1401
|
+
buf.set(rule.hint, 4 + rule.path.length);
|
|
1402
|
+
const opOffset = 4 + rule.path.length + rule.hint.length;
|
|
1403
|
+
buf[opOffset] = rule.operator[0];
|
|
1329
1404
|
writeBE16(buf, opOffset + 1, data.length);
|
|
1330
1405
|
buf.set(data, opOffset + 3);
|
|
1331
1406
|
return buf;
|
|
1332
1407
|
}
|
|
1333
|
-
/** Serialize all rules in a group to a single byte array. */
|
|
1334
1408
|
function encodeGroupRules(rules) {
|
|
1335
1409
|
const parts = rules.map(encodeRule);
|
|
1336
1410
|
const totalSize = parts.reduce((sum, p) => sum + p.length, 0);
|
|
@@ -1342,40 +1416,35 @@ function encodeGroupRules(rules) {
|
|
|
1342
1416
|
}
|
|
1343
1417
|
return buf;
|
|
1344
1418
|
}
|
|
1345
|
-
/** Build a single operator hex string from a decoded rule's opCode and data. */
|
|
1346
1419
|
function buildOperatorHex(rule) {
|
|
1347
1420
|
return `0x${rule.opCode.value.toString(16).padStart(2, "0")}${rule.data.value.slice(2)}`;
|
|
1348
1421
|
}
|
|
1349
|
-
/**
|
|
1350
|
-
* Encode a PolicyData structure into the canonical binary format.
|
|
1351
|
-
* @param data - The policy data to encode.
|
|
1352
|
-
* @returns The encoded policy as a 0x-prefixed hex string.
|
|
1353
|
-
*/
|
|
1354
1422
|
function encode(data) {
|
|
1423
|
+
const descBytes = hexToBytes(data.descriptor);
|
|
1424
|
+
if (data.groups.length === 0) throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
|
|
1425
|
+
if (data.groups.length > 255) throw new CallciumError("GROUP_COUNT_OVERFLOW", `Group count ${data.groups.length} exceeds maximum 255`);
|
|
1355
1426
|
const sortedGroups = data.groups.map((group) => {
|
|
1356
|
-
const rules = group.flatMap(flattenConstraint);
|
|
1427
|
+
const rules = group.flatMap((constraint) => flattenConstraint(constraint, descBytes));
|
|
1357
1428
|
sortRules(rules);
|
|
1358
1429
|
return rules;
|
|
1359
1430
|
});
|
|
1360
|
-
if (
|
|
1361
|
-
|
|
1362
|
-
const groupsWithHash = sortedGroups.map((rules, groupIndex) => {
|
|
1431
|
+
if (descBytes.length > 65535) throw new CallciumError("DESC_LENGTH_OVERFLOW", `Descriptor length ${descBytes.length} exceeds maximum 65535`);
|
|
1432
|
+
const encodedGroups = sortedGroups.map((rules, groupIndex) => {
|
|
1363
1433
|
if (rules.length === 0) throw new CallciumError("EMPTY_GROUP", `Group ${groupIndex} is empty`);
|
|
1364
1434
|
if (rules.length > 65535) throw new CallciumError("RULE_COUNT_OVERFLOW", `Group ${groupIndex} rule count ${rules.length} exceeds maximum 65535`);
|
|
1365
|
-
const wireBytes = encodeGroupRules(rules);
|
|
1366
1435
|
return {
|
|
1367
|
-
wireBytes,
|
|
1368
|
-
hash: keccak_256(wireBytes),
|
|
1436
|
+
wireBytes: encodeGroupRules(rules),
|
|
1369
1437
|
ruleCount: rules.length
|
|
1370
1438
|
};
|
|
1371
1439
|
});
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1440
|
+
if (encodedGroups.length > 1) {
|
|
1441
|
+
const hashes = new Map(encodedGroups.map((group) => [group, keccak_256(group.wireBytes)]));
|
|
1442
|
+
encodedGroups.sort((a, b) => compareBytes(hashes.get(a), hashes.get(b)));
|
|
1443
|
+
}
|
|
1375
1444
|
const selectorBytes = data.isSelectorless ? new Uint8Array(4) : hexToBytes(data.selector);
|
|
1376
1445
|
const headerByte = PolicyFormat.VERSION | (data.isSelectorless ? PolicyFormat.FLAG_NO_SELECTOR : 0);
|
|
1377
1446
|
let totalSize = PolicyFormat.HEADER_SIZE + PolicyFormat.SELECTOR_SIZE + PolicyFormat.DESC_LENGTH_SIZE + descBytes.length + PolicyFormat.GROUP_COUNT_SIZE;
|
|
1378
|
-
for (const g of
|
|
1447
|
+
for (const g of encodedGroups) totalSize += PolicyFormat.GROUP_HEADER_SIZE + g.wireBytes.length;
|
|
1379
1448
|
const out = new Uint8Array(totalSize);
|
|
1380
1449
|
let offset = 0;
|
|
1381
1450
|
out[offset++] = headerByte;
|
|
@@ -1385,226 +1454,61 @@ function encode(data) {
|
|
|
1385
1454
|
offset += PolicyFormat.DESC_LENGTH_SIZE;
|
|
1386
1455
|
out.set(descBytes, offset);
|
|
1387
1456
|
offset += descBytes.length;
|
|
1388
|
-
out[offset++] =
|
|
1389
|
-
for (const g of
|
|
1457
|
+
out[offset++] = encodedGroups.length;
|
|
1458
|
+
for (const g of encodedGroups) {
|
|
1390
1459
|
writeBE16(out, offset, g.ruleCount);
|
|
1391
1460
|
offset += PolicyFormat.GROUP_RULECOUNT_SIZE;
|
|
1392
|
-
out
|
|
1393
|
-
|
|
1394
|
-
out[offset++] = g.wireBytes.length >>> 8 & 255;
|
|
1395
|
-
out[offset++] = g.wireBytes.length & 255;
|
|
1461
|
+
writeBE32(out, offset, g.wireBytes.length);
|
|
1462
|
+
offset += PolicyFormat.GROUP_SIZE_SIZE;
|
|
1396
1463
|
out.set(g.wireBytes, offset);
|
|
1397
1464
|
offset += g.wireBytes.length;
|
|
1398
1465
|
}
|
|
1399
1466
|
return bytesToHex(out);
|
|
1400
1467
|
}
|
|
1401
|
-
/**
|
|
1402
|
-
* Decode a binary policy blob into a PolicyData structure.
|
|
1403
|
-
* @param blob - Binary policy as 0x-prefixed hex string.
|
|
1404
|
-
* @returns The decoded policy data with constraints grouped by scope and path.
|
|
1405
|
-
* @throws {CallciumError} If the blob is structurally malformed.
|
|
1406
|
-
*/
|
|
1407
1468
|
function decode(blob) {
|
|
1408
1469
|
const { policy } = decodePolicy(blob);
|
|
1409
1470
|
const groups = policy.groups.map((group) => {
|
|
1410
1471
|
const constraintMap = /* @__PURE__ */ new Map();
|
|
1411
1472
|
const constraintOrder = [];
|
|
1412
1473
|
for (const rule of group.rules) {
|
|
1413
|
-
const
|
|
1414
|
-
const
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
});
|
|
1430
|
-
return {
|
|
1431
|
-
isSelectorless: policy.isSelectorless,
|
|
1432
|
-
selector: policy.selector.value,
|
|
1433
|
-
descriptor: policy.descriptor.raw,
|
|
1434
|
-
groups,
|
|
1435
|
-
span: policy.span
|
|
1436
|
-
};
|
|
1437
|
-
}
|
|
1438
|
-
/**
|
|
1439
|
-
* Inspect a binary policy blob, returning the structural representation with full byte-level spans.
|
|
1440
|
-
* @param blob - Binary policy as 0x-prefixed hex string.
|
|
1441
|
-
* @returns The inspected policy with per-field spans for every structural element.
|
|
1442
|
-
* @throws {CallciumError} If the blob is structurally malformed.
|
|
1443
|
-
*/
|
|
1444
|
-
function inspect(blob) {
|
|
1445
|
-
return decodePolicy(blob).policy;
|
|
1446
|
-
}
|
|
1447
|
-
/** Encode, decode, and inspect policies in the canonical binary format. */
|
|
1448
|
-
const PolicyCoder = {
|
|
1449
|
-
encode,
|
|
1450
|
-
decode,
|
|
1451
|
-
inspect
|
|
1452
|
-
};
|
|
1453
|
-
//#endregion
|
|
1454
|
-
//#region src/operators.ts
|
|
1455
|
-
/** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
|
|
1456
|
-
function toBigInt(bytes, offset = 0) {
|
|
1457
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
|
|
1458
|
-
return view.getBigUint64(0, false) << 192n | view.getBigUint64(8, false) << 128n | view.getBigUint64(16, false) << 64n | view.getBigUint64(24, false);
|
|
1459
|
-
}
|
|
1460
|
-
/** Return true when the type code represents a signed integer (int8 through int256). */
|
|
1461
|
-
function isSigned(typeCode) {
|
|
1462
|
-
return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
|
|
1463
|
-
}
|
|
1464
|
-
/**
|
|
1465
|
-
* Return true when the type code represents a left-aligned type (fixed bytes or function),
|
|
1466
|
-
* whose value occupies the high bytes of the word with zero padding below.
|
|
1467
|
-
*/
|
|
1468
|
-
function isLeftAligned(typeCode) {
|
|
1469
|
-
return typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX || typeCode === TypeCode.FUNCTION;
|
|
1470
|
-
}
|
|
1471
|
-
/**
|
|
1472
|
-
* Canonicalize a raw 256-bit calldata word to its ABI value for the declared type (Policy Spec §7.8).
|
|
1473
|
-
* A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
|
|
1474
|
-
* unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
|
|
1475
|
-
* comparison use the canonical ABI value rather than the raw bytes.
|
|
1476
|
-
* @param value - The raw 256-bit word loaded from calldata.
|
|
1477
|
-
* @param typeCode - The declared type code of the value.
|
|
1478
|
-
* @returns The canonicalized 256-bit value.
|
|
1479
|
-
*/
|
|
1480
|
-
function canonicalize(value, typeCode) {
|
|
1481
|
-
if (typeCode <= TypeCode.UINT_MAX) {
|
|
1482
|
-
const bits = BigInt((typeCode + 1) * 8);
|
|
1483
|
-
return bits === 256n ? value : value & (1n << bits) - 1n;
|
|
1484
|
-
}
|
|
1485
|
-
if (typeCode <= TypeCode.INT_MAX) {
|
|
1486
|
-
const bits = (typeCode - TypeCode.INT_MIN + 1) * 8;
|
|
1487
|
-
return bits === 256 ? value : BigInt.asUintN(256, BigInt.asIntN(bits, value));
|
|
1488
|
-
}
|
|
1489
|
-
if (typeCode === TypeCode.ADDRESS) return value & (1n << 160n) - 1n;
|
|
1490
|
-
if (typeCode === TypeCode.BOOL) return value & 1n;
|
|
1491
|
-
if (typeCode === TypeCode.FUNCTION) return value >> 64n << 64n;
|
|
1492
|
-
if (typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX) {
|
|
1493
|
-
const n = typeCode - TypeCode.FIXED_BYTES_MIN + 1;
|
|
1494
|
-
if (n === 32) return value;
|
|
1495
|
-
const padBits = BigInt((32 - n) * 8);
|
|
1496
|
-
return value >> padBits << padBits;
|
|
1497
|
-
}
|
|
1498
|
-
return value;
|
|
1499
|
-
}
|
|
1500
|
-
/** Return true when the operator code (with or without NOT flag) is a LENGTH_* variant. */
|
|
1501
|
-
function isLengthOp(opCode) {
|
|
1502
|
-
const base = opCode & ~Op.NOT;
|
|
1503
|
-
return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
|
|
1504
|
-
}
|
|
1505
|
-
/** Map a LENGTH_* opcode base to its core value-comparison twin. */
|
|
1506
|
-
function lengthToValueOp(base) {
|
|
1507
|
-
if (base === Op.LENGTH_EQ) return Op.EQ;
|
|
1508
|
-
if (base === Op.LENGTH_GT) return Op.GT;
|
|
1509
|
-
if (base === Op.LENGTH_LT) return Op.LT;
|
|
1510
|
-
if (base === Op.LENGTH_GTE) return Op.GTE;
|
|
1511
|
-
if (base === Op.LENGTH_LTE) return Op.LTE;
|
|
1512
|
-
return Op.BETWEEN;
|
|
1513
|
-
}
|
|
1514
|
-
/** Return true when LENGTH_* operators are valid for the given type code (bytes, string, or dynamic array). */
|
|
1515
|
-
function isLengthValidType(typeCode) {
|
|
1516
|
-
return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
|
|
1517
|
-
}
|
|
1518
|
-
/** Compare two 256-bit values, using two's complement interpretation for signed type codes. */
|
|
1519
|
-
function compareTyped(a, b, typeCode) {
|
|
1520
|
-
if (isSigned(typeCode)) {
|
|
1521
|
-
const sa = BigInt.asIntN(256, a);
|
|
1522
|
-
const sb = BigInt.asIntN(256, b);
|
|
1523
|
-
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
1524
|
-
}
|
|
1525
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
1526
|
-
}
|
|
1527
|
-
/** Binary search over a sorted array of 32-byte words. Comparison is always unsigned. */
|
|
1528
|
-
function isIn(value, data) {
|
|
1529
|
-
const count = data.length / 32;
|
|
1530
|
-
let lo = 0;
|
|
1531
|
-
let hi = count - 1;
|
|
1532
|
-
while (lo <= hi) {
|
|
1533
|
-
const mid = lo + hi >>> 1;
|
|
1534
|
-
const elem = toBigInt(data, mid * 32);
|
|
1535
|
-
if (value === elem) return true;
|
|
1536
|
-
if (value < elem) hi = mid - 1;
|
|
1537
|
-
else lo = mid + 1;
|
|
1538
|
-
}
|
|
1539
|
-
return false;
|
|
1540
|
-
}
|
|
1541
|
-
/**
|
|
1542
|
-
* Apply a single operator against a value and operand data.
|
|
1543
|
-
* Handles the NOT flag internally by inverting the base operator result.
|
|
1544
|
-
* @param opCode - Operator code, potentially OR'd with the NOT flag.
|
|
1545
|
-
* @param value - The 256-bit value to test (ignored for LENGTH_* operators).
|
|
1546
|
-
* @param valueLength - Runtime byte or element count, only meaningful for LENGTH_* operators.
|
|
1547
|
-
* @param operandData - Raw operand bytes (32 per operand, 64 for range operators).
|
|
1548
|
-
* @param typeCode - ABI type code, used to select signed vs. unsigned comparison.
|
|
1549
|
-
* @returns Whether the value satisfies the operator.
|
|
1550
|
-
*/
|
|
1551
|
-
function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
1552
|
-
const negate = (opCode & Op.NOT) !== 0;
|
|
1553
|
-
let base = opCode & ~Op.NOT;
|
|
1554
|
-
if (isLengthOp(base)) {
|
|
1555
|
-
value = BigInt(valueLength);
|
|
1556
|
-
typeCode = TypeCode.UINT_MAX;
|
|
1557
|
-
base = lengthToValueOp(base);
|
|
1558
|
-
}
|
|
1559
|
-
let result;
|
|
1560
|
-
switch (base) {
|
|
1561
|
-
case Op.EQ:
|
|
1562
|
-
result = value === toBigInt(operandData, 0);
|
|
1563
|
-
break;
|
|
1564
|
-
case Op.GT:
|
|
1565
|
-
result = compareTyped(value, toBigInt(operandData, 0), typeCode) > 0;
|
|
1566
|
-
break;
|
|
1567
|
-
case Op.LT:
|
|
1568
|
-
result = compareTyped(value, toBigInt(operandData, 0), typeCode) < 0;
|
|
1569
|
-
break;
|
|
1570
|
-
case Op.GTE:
|
|
1571
|
-
result = compareTyped(value, toBigInt(operandData, 0), typeCode) >= 0;
|
|
1572
|
-
break;
|
|
1573
|
-
case Op.LTE:
|
|
1574
|
-
result = compareTyped(value, toBigInt(operandData, 0), typeCode) <= 0;
|
|
1575
|
-
break;
|
|
1576
|
-
case Op.BETWEEN: {
|
|
1577
|
-
const min = toBigInt(operandData, 0);
|
|
1578
|
-
const max = toBigInt(operandData, 32);
|
|
1579
|
-
result = compareTyped(value, min, typeCode) >= 0 && compareTyped(value, max, typeCode) <= 0;
|
|
1580
|
-
break;
|
|
1581
|
-
}
|
|
1582
|
-
case Op.IN:
|
|
1583
|
-
result = isIn(value, operandData);
|
|
1584
|
-
break;
|
|
1585
|
-
case Op.BITMASK_ALL: {
|
|
1586
|
-
const mask = toBigInt(operandData, 0);
|
|
1587
|
-
result = (value & mask) === mask;
|
|
1588
|
-
break;
|
|
1589
|
-
}
|
|
1590
|
-
case Op.BITMASK_ANY: {
|
|
1591
|
-
const mask = toBigInt(operandData, 0);
|
|
1592
|
-
result = (value & mask) !== 0n;
|
|
1593
|
-
break;
|
|
1594
|
-
}
|
|
1595
|
-
case Op.BITMASK_NONE: {
|
|
1596
|
-
const mask = toBigInt(operandData, 0);
|
|
1597
|
-
result = (value & mask) === 0n;
|
|
1598
|
-
break;
|
|
1474
|
+
const hintHex = rule.hint?.value;
|
|
1475
|
+
const key = `${rule.scope.value}:${rule.path.value}:${hintHex ?? ""}`;
|
|
1476
|
+
const opHex = buildOperatorHex(rule);
|
|
1477
|
+
const existing = constraintMap.get(key);
|
|
1478
|
+
if (existing !== void 0) existing.operators.push(opHex);
|
|
1479
|
+
else {
|
|
1480
|
+
const constraint = {
|
|
1481
|
+
scope: rule.scope.value,
|
|
1482
|
+
path: rule.path.value,
|
|
1483
|
+
operators: [opHex],
|
|
1484
|
+
...hintHex !== void 0 && { hint: hintHex },
|
|
1485
|
+
span: rule.span
|
|
1486
|
+
};
|
|
1487
|
+
constraintMap.set(key, constraint);
|
|
1488
|
+
constraintOrder.push(key);
|
|
1489
|
+
}
|
|
1599
1490
|
}
|
|
1600
|
-
|
|
1601
|
-
}
|
|
1602
|
-
return
|
|
1491
|
+
return constraintOrder.map((k) => constraintMap.get(k));
|
|
1492
|
+
});
|
|
1493
|
+
return {
|
|
1494
|
+
isSelectorless: policy.isSelectorless,
|
|
1495
|
+
selector: policy.selector.value,
|
|
1496
|
+
descriptor: policy.descriptor.raw,
|
|
1497
|
+
groups,
|
|
1498
|
+
span: policy.span
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
function inspect(blob) {
|
|
1502
|
+
return decodePolicy(blob).policy;
|
|
1603
1503
|
}
|
|
1504
|
+
const PolicyCoder = {
|
|
1505
|
+
encode,
|
|
1506
|
+
decode,
|
|
1507
|
+
inspect
|
|
1508
|
+
};
|
|
1604
1509
|
//#endregion
|
|
1605
1510
|
//#region src/validation-issue.ts
|
|
1606
1511
|
const ZERO = `0x${"0".repeat(64)}`;
|
|
1607
|
-
/** Create an issue from operator compatibility results. */
|
|
1608
1512
|
function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
|
|
1609
1513
|
return {
|
|
1610
1514
|
severity: "error",
|
|
@@ -1617,7 +1521,6 @@ function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
|
|
|
1617
1521
|
message
|
|
1618
1522
|
};
|
|
1619
1523
|
}
|
|
1620
|
-
/** Issue for a calldata path that does not navigate the descriptor. */
|
|
1621
1524
|
function unnavigablePath(groupIndex, constraintIndex) {
|
|
1622
1525
|
return {
|
|
1623
1526
|
severity: "error",
|
|
@@ -1630,7 +1533,30 @@ function unnavigablePath(groupIndex, constraintIndex) {
|
|
|
1630
1533
|
message: "Path does not navigate the descriptor"
|
|
1631
1534
|
};
|
|
1632
1535
|
}
|
|
1633
|
-
|
|
1536
|
+
function nestedQuantifier(groupIndex, constraintIndex) {
|
|
1537
|
+
return {
|
|
1538
|
+
severity: "error",
|
|
1539
|
+
category: "typeMismatch",
|
|
1540
|
+
groupIndex,
|
|
1541
|
+
constraintIndex,
|
|
1542
|
+
code: "NESTED_QUANTIFIER",
|
|
1543
|
+
value1: ZERO,
|
|
1544
|
+
value2: ZERO,
|
|
1545
|
+
message: "Path applies more than one quantifier"
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function hintMismatch(groupIndex, constraintIndex) {
|
|
1549
|
+
return {
|
|
1550
|
+
severity: "error",
|
|
1551
|
+
category: "typeMismatch",
|
|
1552
|
+
groupIndex,
|
|
1553
|
+
constraintIndex,
|
|
1554
|
+
code: "HINT_MISMATCH",
|
|
1555
|
+
value1: ZERO,
|
|
1556
|
+
value2: ZERO,
|
|
1557
|
+
message: "Hint does not match the path compiled against the descriptor"
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1634
1560
|
function nonCanonicalOperand(groupIndex, constraintIndex, operand, canonical) {
|
|
1635
1561
|
return {
|
|
1636
1562
|
severity: "error",
|
|
@@ -1643,7 +1569,6 @@ function nonCanonicalOperand(groupIndex, constraintIndex, operand, canonical) {
|
|
|
1643
1569
|
message: "Operand is not the canonical encoding for the declared type"
|
|
1644
1570
|
};
|
|
1645
1571
|
}
|
|
1646
|
-
/** eq(v)/neq(v) or lengthEq(v)/lengthNeq(v) on the same path. */
|
|
1647
1572
|
function eqNeqContradiction(isLength, groupIndex, constraintIndex, value) {
|
|
1648
1573
|
return {
|
|
1649
1574
|
severity: "error",
|
|
@@ -1656,7 +1581,6 @@ function eqNeqContradiction(isLength, groupIndex, constraintIndex, value) {
|
|
|
1656
1581
|
message: isLength ? "lengthEq(v) and lengthNeq(v) on same path" : "eq(v) and neq(v) on same path"
|
|
1657
1582
|
};
|
|
1658
1583
|
}
|
|
1659
|
-
/** Multiple eq()/lengthEq() operators with different values. */
|
|
1660
1584
|
function conflictingEquality(isLength, groupIndex, constraintIndex, existing, newValue) {
|
|
1661
1585
|
return {
|
|
1662
1586
|
severity: "error",
|
|
@@ -1669,7 +1593,6 @@ function conflictingEquality(isLength, groupIndex, constraintIndex, existing, ne
|
|
|
1669
1593
|
message: isLength ? "Multiple lengthEq() operators with different values" : "Multiple eq() operators with different values"
|
|
1670
1594
|
};
|
|
1671
1595
|
}
|
|
1672
|
-
/** Value/length is outside the physical range of its type. */
|
|
1673
1596
|
function outOfPhysicalBounds(isLength, groupIndex, constraintIndex, value) {
|
|
1674
1597
|
return {
|
|
1675
1598
|
severity: "error",
|
|
@@ -1682,7 +1605,6 @@ function outOfPhysicalBounds(isLength, groupIndex, constraintIndex, value) {
|
|
|
1682
1605
|
message: isLength ? "Length value is outside the physical range" : "Value is outside the physical range of the type"
|
|
1683
1606
|
};
|
|
1684
1607
|
}
|
|
1685
|
-
/** gt()/lengthGt() on the maximum is impossible. */
|
|
1686
1608
|
function impossibleGt(isLength, groupIndex, constraintIndex, value) {
|
|
1687
1609
|
return {
|
|
1688
1610
|
severity: "error",
|
|
@@ -1695,7 +1617,6 @@ function impossibleGt(isLength, groupIndex, constraintIndex, value) {
|
|
|
1695
1617
|
message: isLength ? "lengthGt() on maximum length is impossible" : "gt() on type maximum is impossible"
|
|
1696
1618
|
};
|
|
1697
1619
|
}
|
|
1698
|
-
/** lt()/lengthLt() on the minimum is impossible. */
|
|
1699
1620
|
function impossibleLt(isLength, groupIndex, constraintIndex, value) {
|
|
1700
1621
|
return {
|
|
1701
1622
|
severity: "error",
|
|
@@ -1708,7 +1629,6 @@ function impossibleLt(isLength, groupIndex, constraintIndex, value) {
|
|
|
1708
1629
|
message: isLength ? "lengthLt() on minimum length is impossible" : "lt() on type minimum is impossible"
|
|
1709
1630
|
};
|
|
1710
1631
|
}
|
|
1711
|
-
/** eq()/lengthEq() value is excluded by a bound. */
|
|
1712
1632
|
function boundsExcludeEquality(isLength, groupIndex, constraintIndex, eq, bound) {
|
|
1713
1633
|
return {
|
|
1714
1634
|
severity: "error",
|
|
@@ -1721,7 +1641,6 @@ function boundsExcludeEquality(isLength, groupIndex, constraintIndex, eq, bound)
|
|
|
1721
1641
|
message: isLength ? "lengthEq() value is excluded by bound" : "eq() value is excluded by bound"
|
|
1722
1642
|
};
|
|
1723
1643
|
}
|
|
1724
|
-
/** Lower bound/length exceeds the upper bound. */
|
|
1725
1644
|
function impossibleRange(isLength, groupIndex, constraintIndex, lower, upper) {
|
|
1726
1645
|
return {
|
|
1727
1646
|
severity: "error",
|
|
@@ -1734,7 +1653,6 @@ function impossibleRange(isLength, groupIndex, constraintIndex, lower, upper) {
|
|
|
1734
1653
|
message: isLength ? "Lower length bound exceeds upper bound" : "Lower bound exceeds upper bound"
|
|
1735
1654
|
};
|
|
1736
1655
|
}
|
|
1737
|
-
/** Set operation excludes existing eq() value. */
|
|
1738
1656
|
function setExcludesEquality(groupIndex, constraintIndex, value) {
|
|
1739
1657
|
return {
|
|
1740
1658
|
severity: "error",
|
|
@@ -1747,7 +1665,6 @@ function setExcludesEquality(groupIndex, constraintIndex, value) {
|
|
|
1747
1665
|
message: "Set operation excludes existing eq() value"
|
|
1748
1666
|
};
|
|
1749
1667
|
}
|
|
1750
|
-
/** Multiple isIn() sets have no intersection. */
|
|
1751
1668
|
function emptySetIntersection(groupIndex, constraintIndex) {
|
|
1752
1669
|
return {
|
|
1753
1670
|
severity: "error",
|
|
@@ -1760,7 +1677,6 @@ function emptySetIntersection(groupIndex, constraintIndex) {
|
|
|
1760
1677
|
message: "Multiple isIn() sets have no intersection"
|
|
1761
1678
|
};
|
|
1762
1679
|
}
|
|
1763
|
-
/** All values in isIn() set are excluded by neq/notIn. */
|
|
1764
1680
|
function setFullyExcluded(groupIndex, constraintIndex) {
|
|
1765
1681
|
return {
|
|
1766
1682
|
severity: "error",
|
|
@@ -1773,7 +1689,6 @@ function setFullyExcluded(groupIndex, constraintIndex) {
|
|
|
1773
1689
|
message: "All values in isIn() set are excluded by neq/notIn"
|
|
1774
1690
|
};
|
|
1775
1691
|
}
|
|
1776
|
-
/** Bitmask operators conflict. */
|
|
1777
1692
|
function bitmaskContradiction(groupIndex, constraintIndex, mask, conflicting) {
|
|
1778
1693
|
return {
|
|
1779
1694
|
severity: "error",
|
|
@@ -1786,7 +1701,6 @@ function bitmaskContradiction(groupIndex, constraintIndex, mask, conflicting) {
|
|
|
1786
1701
|
message: "Bitmask operators conflict"
|
|
1787
1702
|
};
|
|
1788
1703
|
}
|
|
1789
|
-
/** bitmaskAny is impossible because all bits are forbidden. */
|
|
1790
1704
|
function bitmaskAnyImpossible(groupIndex, constraintIndex, mask, mustBeZero) {
|
|
1791
1705
|
return {
|
|
1792
1706
|
severity: "error",
|
|
@@ -1799,7 +1713,6 @@ function bitmaskAnyImpossible(groupIndex, constraintIndex, mask, mustBeZero) {
|
|
|
1799
1713
|
message: "bitmaskAny is impossible because all bits are forbidden"
|
|
1800
1714
|
};
|
|
1801
1715
|
}
|
|
1802
|
-
/** isIn/notIn set is not strictly sorted and deduplicated. */
|
|
1803
1716
|
function unsortedInSet(groupIndex, constraintIndex) {
|
|
1804
1717
|
return {
|
|
1805
1718
|
severity: "error",
|
|
@@ -1812,7 +1725,6 @@ function unsortedInSet(groupIndex, constraintIndex) {
|
|
|
1812
1725
|
message: "isIn/notIn set is not strictly sorted and deduplicated."
|
|
1813
1726
|
};
|
|
1814
1727
|
}
|
|
1815
|
-
/** Group contains zero constraints. */
|
|
1816
1728
|
function emptyGroup(groupIndex) {
|
|
1817
1729
|
return {
|
|
1818
1730
|
severity: "error",
|
|
@@ -1825,7 +1737,6 @@ function emptyGroup(groupIndex) {
|
|
|
1825
1737
|
message: "Group contains zero constraints."
|
|
1826
1738
|
};
|
|
1827
1739
|
}
|
|
1828
|
-
/** Numeric/length bound dominated by a stricter bound. */
|
|
1829
1740
|
function dominatedBound(isLength, groupIndex, constraintIndex, value) {
|
|
1830
1741
|
return {
|
|
1831
1742
|
severity: "warning",
|
|
@@ -1838,7 +1749,6 @@ function dominatedBound(isLength, groupIndex, constraintIndex, value) {
|
|
|
1838
1749
|
message: isLength ? "Length bound is dominated by a stricter bound" : "Numeric bound is dominated by a stricter bound"
|
|
1839
1750
|
};
|
|
1840
1751
|
}
|
|
1841
|
-
/** Bound redundant because eq()/lengthEq() is set. */
|
|
1842
1752
|
function redundantBound(isLength, groupIndex, constraintIndex, bound, eq) {
|
|
1843
1753
|
return {
|
|
1844
1754
|
severity: "warning",
|
|
@@ -1851,7 +1761,6 @@ function redundantBound(isLength, groupIndex, constraintIndex, bound, eq) {
|
|
|
1851
1761
|
message: isLength ? "Length bound is redundant because lengthEq() is set" : "Bound is redundant because eq() is set"
|
|
1852
1762
|
};
|
|
1853
1763
|
}
|
|
1854
|
-
/** notIn() value was present in isIn() set. */
|
|
1855
1764
|
function setReduction(groupIndex, constraintIndex, value) {
|
|
1856
1765
|
return {
|
|
1857
1766
|
severity: "warning",
|
|
@@ -1864,7 +1773,6 @@ function setReduction(groupIndex, constraintIndex, value) {
|
|
|
1864
1773
|
message: "notIn() value was present in isIn() set"
|
|
1865
1774
|
};
|
|
1866
1775
|
}
|
|
1867
|
-
/** isIn() sets partially overlap. */
|
|
1868
1776
|
function setRedundancy(groupIndex, constraintIndex, intersectionCount) {
|
|
1869
1777
|
return {
|
|
1870
1778
|
severity: "warning",
|
|
@@ -1877,7 +1785,6 @@ function setRedundancy(groupIndex, constraintIndex, intersectionCount) {
|
|
|
1877
1785
|
message: "isIn() sets partially overlap"
|
|
1878
1786
|
};
|
|
1879
1787
|
}
|
|
1880
|
-
/** Some values in isIn() set are excluded by neq/notIn. */
|
|
1881
1788
|
function setPartiallyExcluded(groupIndex, constraintIndex, excludedCount) {
|
|
1882
1789
|
return {
|
|
1883
1790
|
severity: "warning",
|
|
@@ -1890,7 +1797,6 @@ function setPartiallyExcluded(groupIndex, constraintIndex, excludedCount) {
|
|
|
1890
1797
|
message: "Some values in isIn() set are excluded by neq/notIn"
|
|
1891
1798
|
};
|
|
1892
1799
|
}
|
|
1893
|
-
/** Bitmask operation is redundant. */
|
|
1894
1800
|
function redundantBitmask(groupIndex, constraintIndex, mask, existing) {
|
|
1895
1801
|
return {
|
|
1896
1802
|
severity: "warning",
|
|
@@ -1903,7 +1809,6 @@ function redundantBitmask(groupIndex, constraintIndex, mask, existing) {
|
|
|
1903
1809
|
message: "Bitmask operation is redundant"
|
|
1904
1810
|
};
|
|
1905
1811
|
}
|
|
1906
|
-
/** gte/lte pair fusible into a single range operator. */
|
|
1907
1812
|
function fusibleRange(isLength, groupIndex, constraintIndex, low, high) {
|
|
1908
1813
|
return {
|
|
1909
1814
|
severity: "warning",
|
|
@@ -1916,7 +1821,6 @@ function fusibleRange(isLength, groupIndex, constraintIndex, low, high) {
|
|
|
1916
1821
|
message: isLength ? "lengthGte() and lengthLte() fuse into a single lengthBetween()" : "gte() and lte() fuse into a single between()"
|
|
1917
1822
|
};
|
|
1918
1823
|
}
|
|
1919
|
-
/** Duplicate operator in constraint. */
|
|
1920
1824
|
function duplicateConstraint(groupIndex, constraintIndex) {
|
|
1921
1825
|
return {
|
|
1922
1826
|
severity: "warning",
|
|
@@ -1929,7 +1833,6 @@ function duplicateConstraint(groupIndex, constraintIndex) {
|
|
|
1929
1833
|
message: "Duplicate operator in constraint"
|
|
1930
1834
|
};
|
|
1931
1835
|
}
|
|
1932
|
-
/** gte(min)/lengthGte(0) is always true. */
|
|
1933
1836
|
function vacuousGte(isLength, groupIndex, constraintIndex, value) {
|
|
1934
1837
|
return {
|
|
1935
1838
|
severity: "info",
|
|
@@ -1942,7 +1845,6 @@ function vacuousGte(isLength, groupIndex, constraintIndex, value) {
|
|
|
1942
1845
|
message: isLength ? "lengthGte(0) is always true" : "gte() bound equals type minimum (always true)"
|
|
1943
1846
|
};
|
|
1944
1847
|
}
|
|
1945
|
-
/** lte(max)/lengthLte(max) is always true. */
|
|
1946
1848
|
function vacuousLte(isLength, groupIndex, constraintIndex, value) {
|
|
1947
1849
|
return {
|
|
1948
1850
|
severity: "info",
|
|
@@ -1955,7 +1857,6 @@ function vacuousLte(isLength, groupIndex, constraintIndex, value) {
|
|
|
1955
1857
|
message: isLength ? "lengthLte() bound equals maximum (always true)" : "lte() bound equals type maximum (always true)"
|
|
1956
1858
|
};
|
|
1957
1859
|
}
|
|
1958
|
-
/** Negated range with inverted bounds excludes nothing (always true). */
|
|
1959
1860
|
function vacuousNegatedRange(isLength, groupIndex, constraintIndex, low, high) {
|
|
1960
1861
|
return {
|
|
1961
1862
|
severity: "info",
|
|
@@ -1968,7 +1869,6 @@ function vacuousNegatedRange(isLength, groupIndex, constraintIndex, low, high) {
|
|
|
1968
1869
|
message: isLength ? "Negated lengthBetween() with low > high excludes nothing (always true)" : "Negated between() with low > high excludes nothing (always true)"
|
|
1969
1870
|
};
|
|
1970
1871
|
}
|
|
1971
|
-
/** Path deeper than the reference enforcer's cap. */
|
|
1972
1872
|
function pathDepthExceeded(groupIndex, constraintIndex, depth, maxDepth) {
|
|
1973
1873
|
return {
|
|
1974
1874
|
severity: "warning",
|
|
@@ -1978,23 +1878,21 @@ function pathDepthExceeded(groupIndex, constraintIndex, depth, maxDepth) {
|
|
|
1978
1878
|
code: "PATH_DEPTH_EXCEEDED",
|
|
1979
1879
|
value1: depth,
|
|
1980
1880
|
value2: maxDepth,
|
|
1981
|
-
message: "Path depth exceeds the
|
|
1881
|
+
message: "Path depth exceeds the maximum a well-formed policy may declare"
|
|
1982
1882
|
};
|
|
1983
1883
|
}
|
|
1984
|
-
/** Quantifier over a static array beyond the reference enforcer's iteration cap. */
|
|
1985
1884
|
function quantifierOverStaticLimit(groupIndex, constraintIndex, arrayLength, maxLength) {
|
|
1986
1885
|
return {
|
|
1987
|
-
severity: "
|
|
1886
|
+
severity: "error",
|
|
1988
1887
|
category: "compatibility",
|
|
1989
1888
|
groupIndex,
|
|
1990
1889
|
constraintIndex,
|
|
1991
1890
|
code: "QUANTIFIER_OVER_STATIC_LIMIT",
|
|
1992
1891
|
value1: arrayLength,
|
|
1993
1892
|
value2: maxLength,
|
|
1994
|
-
message: "Quantifier over static array exceeds the
|
|
1893
|
+
message: "Quantifier over static array exceeds the maximum quantified length"
|
|
1995
1894
|
};
|
|
1996
1895
|
}
|
|
1997
|
-
/** Context property ID outside the assigned set. */
|
|
1998
1896
|
function unknownContextProperty(groupIndex, constraintIndex, contextId, maxContextId) {
|
|
1999
1897
|
return {
|
|
2000
1898
|
severity: "warning",
|
|
@@ -2007,7 +1905,6 @@ function unknownContextProperty(groupIndex, constraintIndex, contextId, maxConte
|
|
|
2007
1905
|
message: "Unknown context property ID"
|
|
2008
1906
|
};
|
|
2009
1907
|
}
|
|
2010
|
-
/** Negated operator under the existential quantifier. */
|
|
2011
1908
|
function negationUnderAny(groupIndex, constraintIndex, opCode) {
|
|
2012
1909
|
return {
|
|
2013
1910
|
severity: "warning",
|
|
@@ -2022,29 +1919,29 @@ function negationUnderAny(groupIndex, constraintIndex, opCode) {
|
|
|
2022
1919
|
}
|
|
2023
1920
|
//#endregion
|
|
2024
1921
|
//#region src/policy-validator.ts
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
1922
|
+
const NAVIGABILITY_CODES = new Set([
|
|
1923
|
+
"EMPTY_PATH",
|
|
1924
|
+
"PARAM_INDEX_OUT_OF_BOUNDS",
|
|
1925
|
+
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
1926
|
+
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
1927
|
+
"NOT_COMPOSITE"
|
|
1928
|
+
]);
|
|
2028
1929
|
const LENGTH_MAX = 4294967295n;
|
|
2029
1930
|
const UINT256_MAX = (1n << 256n) - 1n;
|
|
2030
1931
|
const INT256_SIGN_BIT = 1n << 255n;
|
|
2031
|
-
/** Signed-aware difference of two raw uint256 values: negative if a<b, zero if equal, positive if a>b. */
|
|
2032
1932
|
function signedCompare(a, b, signed) {
|
|
2033
1933
|
if (!signed) return a - b;
|
|
2034
1934
|
const toS = (v) => v >= INT256_SIGN_BIT ? v - (1n << 256n) : v;
|
|
2035
1935
|
return toS(a) - toS(b);
|
|
2036
1936
|
}
|
|
2037
|
-
/** True for uint and int type codes. */
|
|
2038
1937
|
function isNumericType(typeCode) {
|
|
2039
|
-
return typeCode <= TypeCode.UINT_MAX || typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
|
|
1938
|
+
return typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX || typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
|
|
2040
1939
|
}
|
|
2041
|
-
/** True for uint and bytes32 type codes. */
|
|
2042
1940
|
function isBitmaskCompatible(typeCode) {
|
|
2043
|
-
return typeCode <= TypeCode.UINT_MAX || typeCode === TypeCode.FIXED_BYTES_MIN + 31;
|
|
1941
|
+
return typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX || typeCode === TypeCode.FIXED_BYTES_MIN + 31;
|
|
2044
1942
|
}
|
|
2045
|
-
/** Physical domain limits for a type code (returned as raw uint256 bigints). */
|
|
2046
1943
|
function getDomainLimits(typeCode) {
|
|
2047
|
-
if (typeCode <= TypeCode.UINT_MAX) {
|
|
1944
|
+
if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
|
|
2048
1945
|
const bits = BigInt(typeCode - TypeCode.UINT_MIN + 1) * 8n;
|
|
2049
1946
|
return {
|
|
2050
1947
|
min: 0n,
|
|
@@ -2077,19 +1974,15 @@ function getDomainLimits(typeCode) {
|
|
|
2077
1974
|
max: UINT256_MAX
|
|
2078
1975
|
};
|
|
2079
1976
|
}
|
|
2080
|
-
/** True for EQ, GT, LT, GTE, LTE, BETWEEN, IN, BITMASK_*. */
|
|
2081
1977
|
function isValueOp(opBase) {
|
|
2082
1978
|
return opBase <= Op.BITMASK_NONE;
|
|
2083
1979
|
}
|
|
2084
|
-
/** True for GT, LT, GTE, LTE, BETWEEN. */
|
|
2085
1980
|
function isComparisonOp(opBase) {
|
|
2086
1981
|
return opBase >= Op.GT && opBase <= Op.BETWEEN;
|
|
2087
1982
|
}
|
|
2088
|
-
/** True for BITMASK_ALL, BITMASK_ANY, BITMASK_NONE. */
|
|
2089
1983
|
function isBitmaskOp(opBase) {
|
|
2090
1984
|
return opBase >= Op.BITMASK_ALL && opBase <= Op.BITMASK_NONE;
|
|
2091
1985
|
}
|
|
2092
|
-
/** Return the incompatibility reason, or null if the operator is allowed. */
|
|
2093
1986
|
function getIncompat(opBase, typeInfo) {
|
|
2094
1987
|
const { typeCode, isDynamic, staticSize } = typeInfo;
|
|
2095
1988
|
if (isValueOp(opBase)) {
|
|
@@ -2127,18 +2020,12 @@ function getIncompat(opBase, typeInfo) {
|
|
|
2127
2020
|
message: "Unknown operator code"
|
|
2128
2021
|
};
|
|
2129
2022
|
}
|
|
2130
|
-
/**
|
|
2131
|
-
* True if operator `opCode` is allowed on a target with the given type info.
|
|
2132
|
-
* Tolerates the NOT flag on `opCode`.
|
|
2133
|
-
*/
|
|
2134
2023
|
function isOpAllowed(opCode, typeInfo) {
|
|
2135
2024
|
return getIncompat(opCode & ~Op.NOT, typeInfo) === null;
|
|
2136
2025
|
}
|
|
2137
|
-
/** Read a single uint256 from operator hex (bytes after the opcode byte). */
|
|
2138
2026
|
function readValue(opHex) {
|
|
2139
2027
|
return BigInt(`0x${opHex.slice(4)}`);
|
|
2140
2028
|
}
|
|
2141
|
-
/** First payload word deviating from the canonical encoding for the type, with its canonical form, or null. */
|
|
2142
2029
|
function findNonCanonicalWord(opHex, typeCode) {
|
|
2143
2030
|
const body = opHex.slice(4);
|
|
2144
2031
|
for (let i = 0; i + 64 <= body.length; i += 64) {
|
|
@@ -2151,7 +2038,6 @@ function findNonCanonicalWord(opHex, typeCode) {
|
|
|
2151
2038
|
}
|
|
2152
2039
|
return null;
|
|
2153
2040
|
}
|
|
2154
|
-
/** Read a pair of uint256 values from a BETWEEN operator. */
|
|
2155
2041
|
function readPair(opHex) {
|
|
2156
2042
|
const body = opHex.slice(4);
|
|
2157
2043
|
return {
|
|
@@ -2159,7 +2045,6 @@ function readPair(opHex) {
|
|
|
2159
2045
|
high: BigInt(`0x${body.slice(64, 128)}`)
|
|
2160
2046
|
};
|
|
2161
2047
|
}
|
|
2162
|
-
/** Unpack an IN operator's payload into an array of bigint values. */
|
|
2163
2048
|
function unpackSet(opHex) {
|
|
2164
2049
|
const body = opHex.slice(4);
|
|
2165
2050
|
const count = body.length / 64;
|
|
@@ -2167,12 +2052,10 @@ function unpackSet(opHex) {
|
|
|
2167
2052
|
for (let i = 0; i < count; i++) values.push(BigInt(`0x${body.slice(i * 64, (i + 1) * 64)}`));
|
|
2168
2053
|
return values;
|
|
2169
2054
|
}
|
|
2170
|
-
/** True if every element is strictly greater than the previous. */
|
|
2171
2055
|
function isStrictlyAscending(values) {
|
|
2172
2056
|
for (let i = 1; i < values.length; i++) if (values[i] <= values[i - 1]) return false;
|
|
2173
2057
|
return true;
|
|
2174
2058
|
}
|
|
2175
|
-
/** Convert LENGTH_* opcode to its core comparison equivalent. */
|
|
2176
2059
|
function normalizeLengthOp(base) {
|
|
2177
2060
|
if (base === Op.LENGTH_EQ) return Op.EQ;
|
|
2178
2061
|
if (base === Op.LENGTH_GT) return Op.GT;
|
|
@@ -2181,7 +2064,6 @@ function normalizeLengthOp(base) {
|
|
|
2181
2064
|
if (base === Op.LENGTH_LTE) return Op.LTE;
|
|
2182
2065
|
return base;
|
|
2183
2066
|
}
|
|
2184
|
-
/** Convert a negated bound opcode to its positive equivalent. */
|
|
2185
2067
|
function negateBoundOp(base) {
|
|
2186
2068
|
if (base === Op.GT) return Op.LTE;
|
|
2187
2069
|
if (base === Op.GTE) return Op.LT;
|
|
@@ -2189,7 +2071,6 @@ function negateBoundOp(base) {
|
|
|
2189
2071
|
if (base === Op.LTE) return Op.GT;
|
|
2190
2072
|
return base;
|
|
2191
2073
|
}
|
|
2192
|
-
/** Fresh bound domain with no constraints recorded yet. */
|
|
2193
2074
|
function emptyBoundDomain(boundIsSigned, min, max) {
|
|
2194
2075
|
return {
|
|
2195
2076
|
isSigned: boundIsSigned,
|
|
@@ -2206,12 +2087,12 @@ function emptyBoundDomain(boundIsSigned, min, max) {
|
|
|
2206
2087
|
holes: []
|
|
2207
2088
|
};
|
|
2208
2089
|
}
|
|
2209
|
-
|
|
2210
|
-
function initContext(scope, path, typeInfo) {
|
|
2090
|
+
function initContext(scope, path, steps, typeInfo) {
|
|
2211
2091
|
const { min, max } = getDomainLimits(typeInfo.typeCode);
|
|
2212
2092
|
return {
|
|
2213
2093
|
scope,
|
|
2214
2094
|
path,
|
|
2095
|
+
steps,
|
|
2215
2096
|
typeInfo,
|
|
2216
2097
|
numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
|
|
2217
2098
|
bitmask: {
|
|
@@ -2226,11 +2107,7 @@ function initContext(scope, path, typeInfo) {
|
|
|
2226
2107
|
}
|
|
2227
2108
|
};
|
|
2228
2109
|
}
|
|
2229
|
-
/** Narrow the numeric bound domain with an operator and emit issues on contradiction or redundancy. */
|
|
2230
2110
|
function updateBound(domain, base, isNegated, value, isLength, groupIndex, constraintIndex, issues) {
|
|
2231
|
-
let changedEq = false;
|
|
2232
|
-
let changedLower = false;
|
|
2233
|
-
let changedUpper = false;
|
|
2234
2111
|
if (isNegated) {
|
|
2235
2112
|
if (base === Op.EQ) {
|
|
2236
2113
|
if (domain.hasEq && domain.eq === value) issues.push(eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
@@ -2248,6 +2125,9 @@ function updateBound(domain, base, isNegated, value, isLength, groupIndex, const
|
|
|
2248
2125
|
if (signedCompare(value, domain.min, domain.isSigned) < 0n || signedCompare(value, domain.max, domain.isSigned) > 0n) issues.push(outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2249
2126
|
else if (base === Op.GT && value === domain.max) issues.push(impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2250
2127
|
else if (base === Op.LT && value === domain.min) issues.push(impossibleLt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2128
|
+
let changedEq = false;
|
|
2129
|
+
let changedLower = false;
|
|
2130
|
+
let changedUpper = false;
|
|
2251
2131
|
if (base === Op.EQ) {
|
|
2252
2132
|
if (!domain.hasEq || domain.eq !== value) changedEq = true;
|
|
2253
2133
|
if (domain.hasEq) {
|
|
@@ -2319,7 +2199,6 @@ function updateBound(domain, base, isNegated, value, isLength, groupIndex, const
|
|
|
2319
2199
|
}
|
|
2320
2200
|
}
|
|
2321
2201
|
}
|
|
2322
|
-
/** Update the bitmask domain and emit issues on contradiction or redundancy. */
|
|
2323
2202
|
function updateBitmask(ctx, base, isNegated, mask, groupIndex, constraintIndex, issues) {
|
|
2324
2203
|
if (isNegated) return;
|
|
2325
2204
|
if (base === Op.BITMASK_ALL) {
|
|
@@ -2335,7 +2214,6 @@ function updateBitmask(ctx, base, isNegated, mask, groupIndex, constraintIndex,
|
|
|
2335
2214
|
if ((ctx.bitmask.mustBeOne & mask) !== 0n) issues.push(redundantBitmask(groupIndex, constraintIndex, bigintToHex(mask), bigintToHex(ctx.bitmask.mustBeOne)));
|
|
2336
2215
|
}
|
|
2337
2216
|
}
|
|
2338
|
-
/** Emit an issue if the IN set has been fully excluded by NOT_IN or NEQ holes. */
|
|
2339
2217
|
function checkSetEmpty(ctx, groupIndex, constraintIndex, issues) {
|
|
2340
2218
|
if (!ctx.set.hasIn) return;
|
|
2341
2219
|
let possibleCount = 0;
|
|
@@ -2358,7 +2236,6 @@ function checkSetEmpty(ctx, groupIndex, constraintIndex, issues) {
|
|
|
2358
2236
|
if (possibleCount === 0) issues.push(setFullyExcluded(groupIndex, constraintIndex));
|
|
2359
2237
|
else if (possibleCount < inCount) issues.push(setPartiallyExcluded(groupIndex, constraintIndex, bigintToHex(BigInt(inCount - possibleCount))));
|
|
2360
2238
|
}
|
|
2361
|
-
/** Update the set domain with IN or NOT_IN values and emit issues on contradiction or redundancy. */
|
|
2362
2239
|
function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues) {
|
|
2363
2240
|
if (isNegated) {
|
|
2364
2241
|
for (const value of values) {
|
|
@@ -2400,7 +2277,6 @@ function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues)
|
|
|
2400
2277
|
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
2401
2278
|
}
|
|
2402
2279
|
}
|
|
2403
|
-
/** Emit a warning if the constraint contains any identical operators. */
|
|
2404
2280
|
function checkDuplicates(issues, operators, groupIndex, constraintIndex) {
|
|
2405
2281
|
const seen = /* @__PURE__ */ new Set();
|
|
2406
2282
|
for (const op of operators) {
|
|
@@ -2412,18 +2288,12 @@ function checkDuplicates(issues, operators, groupIndex, constraintIndex) {
|
|
|
2412
2288
|
seen.add(key);
|
|
2413
2289
|
}
|
|
2414
2290
|
}
|
|
2415
|
-
/**
|
|
2416
|
-
* Report a lone unnegated gte/lte pair (value or length domain) as fusible into the
|
|
2417
|
-
* corresponding single range operator. Fires only for a satisfiable pair on a compatible
|
|
2418
|
-
* type; contradictions and dominated bounds are already reported by the domain checks.
|
|
2419
|
-
*/
|
|
2420
2291
|
function checkFusibleRange(ctx, operators, groupIndex, constraintIndex, issues) {
|
|
2421
2292
|
const numericPair = findLonePair(operators, Op.GTE, Op.LTE);
|
|
2422
2293
|
if (numericPair && getIncompat(Op.GTE, ctx.typeInfo) === null && signedCompare(numericPair.low, numericPair.high, ctx.numeric.isSigned) <= 0n) issues.push(fusibleRange(false, groupIndex, constraintIndex, bigintToHex(numericPair.low), bigintToHex(numericPair.high)));
|
|
2423
2294
|
const lengthPair = findLonePair(operators, Op.LENGTH_GTE, Op.LENGTH_LTE);
|
|
2424
2295
|
if (lengthPair && getIncompat(Op.LENGTH_GTE, ctx.typeInfo) === null && lengthPair.low <= lengthPair.high) issues.push(fusibleRange(true, groupIndex, constraintIndex, bigintToHex(lengthPair.low), bigintToHex(lengthPair.high)));
|
|
2425
2296
|
}
|
|
2426
|
-
/** Return the operand pair when the operators contain exactly one of each unnegated bound opcode. */
|
|
2427
2297
|
function findLonePair(operators, lowerOp, upperOp) {
|
|
2428
2298
|
let lowerCount = 0;
|
|
2429
2299
|
let upperCount = 0;
|
|
@@ -2446,19 +2316,18 @@ function findLonePair(operators, lowerOp, upperOp) {
|
|
|
2446
2316
|
high
|
|
2447
2317
|
} : null;
|
|
2448
2318
|
}
|
|
2449
|
-
/** Validate all operators on a single constraint against its domain context. */
|
|
2450
2319
|
function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues) {
|
|
2451
2320
|
const operators = constraint.operators;
|
|
2452
|
-
const underAny = constraint.scope === Scope.CALLDATA &&
|
|
2321
|
+
const underAny = constraint.scope === Scope.CALLDATA && ctx.steps.includes(Quantifier.ANY);
|
|
2453
2322
|
for (const opHex of operators) {
|
|
2454
2323
|
const opCode = parseInt(opHex.slice(2, 4), 16);
|
|
2455
2324
|
const base = opCode & ~Op.NOT;
|
|
2456
|
-
const isNegated = (opCode & Op.NOT) !== 0;
|
|
2457
2325
|
const dataLength = (opHex.length - 4) / 2;
|
|
2458
2326
|
if (dataLength > 65535 || !isValidOperatorData(base, dataLength)) {
|
|
2459
2327
|
issues.push(fromOpRule("UNKNOWN_OPERATOR", "Unknown operator code", groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
2460
2328
|
continue;
|
|
2461
2329
|
}
|
|
2330
|
+
const isNegated = (opCode & Op.NOT) !== 0;
|
|
2462
2331
|
if (underAny && isNegated) issues.push(negationUnderAny(groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
2463
2332
|
const incompat = getIncompat(base, ctx.typeInfo);
|
|
2464
2333
|
if (incompat) {
|
|
@@ -2505,7 +2374,6 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
|
|
|
2505
2374
|
}
|
|
2506
2375
|
checkDuplicates(issues, operators, groupIndex, constraintIndex);
|
|
2507
2376
|
}
|
|
2508
|
-
/** Validate all constraints in a group, building per-path contexts for cross-constraint analysis. */
|
|
2509
2377
|
function validateGroup(data, descBytes, groupIndex, issues) {
|
|
2510
2378
|
const constraints = data.groups[groupIndex];
|
|
2511
2379
|
const contexts = [];
|
|
@@ -2518,24 +2386,28 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2518
2386
|
break;
|
|
2519
2387
|
}
|
|
2520
2388
|
if (!ctx) {
|
|
2389
|
+
const steps = parsePathSteps(constraint.path);
|
|
2521
2390
|
let typeInfo;
|
|
2522
2391
|
if (constraint.scope === Scope.CALLDATA) {
|
|
2523
|
-
const steps = parsePathSteps(constraint.path);
|
|
2524
2392
|
if (steps.length > Limits.MAX_PATH_DEPTH) issues.push(pathDepthExceeded(groupIndex, constraintIndex, bigintToHex(BigInt(steps.length)), bigintToHex(BigInt(Limits.MAX_PATH_DEPTH))));
|
|
2525
2393
|
let walk;
|
|
2526
2394
|
try {
|
|
2527
2395
|
walk = Descriptor.walkPath(descBytes, steps);
|
|
2528
2396
|
} catch (error) {
|
|
2529
|
-
if (error instanceof CallciumError && error.code
|
|
2397
|
+
if (error instanceof CallciumError && NAVIGABILITY_CODES.has(error.code)) {
|
|
2530
2398
|
issues.push(unnavigablePath(groupIndex, constraintIndex));
|
|
2531
2399
|
continue;
|
|
2532
2400
|
}
|
|
2533
2401
|
throw error;
|
|
2534
2402
|
}
|
|
2403
|
+
if (steps.filter(isQuantifier).length > 1) {
|
|
2404
|
+
issues.push(nestedQuantifier(groupIndex, constraintIndex));
|
|
2405
|
+
continue;
|
|
2406
|
+
}
|
|
2535
2407
|
if (walk.quantifiedStaticLength > Limits.MAX_QUANTIFIED_ARRAY_LENGTH) issues.push(quantifierOverStaticLimit(groupIndex, constraintIndex, bigintToHex(BigInt(walk.quantifiedStaticLength)), bigintToHex(BigInt(Limits.MAX_QUANTIFIED_ARRAY_LENGTH))));
|
|
2536
2408
|
typeInfo = walk.typeInfo;
|
|
2537
2409
|
} else {
|
|
2538
|
-
const ctxId =
|
|
2410
|
+
const ctxId = steps[0];
|
|
2539
2411
|
if (ctxId > MAX_CONTEXT_PROPERTY_ID) {
|
|
2540
2412
|
issues.push(unknownContextProperty(groupIndex, constraintIndex, bigintToHex(BigInt(ctxId)), bigintToHex(BigInt(MAX_CONTEXT_PROPERTY_ID))));
|
|
2541
2413
|
typeInfo = {
|
|
@@ -2549,24 +2421,20 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2549
2421
|
staticSize: 32
|
|
2550
2422
|
};
|
|
2551
2423
|
}
|
|
2552
|
-
ctx = initContext(constraint.scope, normalizedPath, typeInfo);
|
|
2424
|
+
ctx = initContext(constraint.scope, normalizedPath, steps, typeInfo);
|
|
2553
2425
|
contexts.push(ctx);
|
|
2554
2426
|
}
|
|
2427
|
+
if (constraint.scope === Scope.CALLDATA && constraint.hint !== void 0 && ctx.steps.length <= Limits.MAX_PATH_DEPTH) {
|
|
2428
|
+
if (bytesToHex(Descriptor.compileHint(descBytes, ctx.steps)) !== constraint.hint.toLowerCase()) issues.push(hintMismatch(groupIndex, constraintIndex));
|
|
2429
|
+
}
|
|
2555
2430
|
validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues);
|
|
2556
2431
|
checkFusibleRange(ctx, constraint.operators, groupIndex, constraintIndex, issues);
|
|
2557
2432
|
}
|
|
2558
2433
|
}
|
|
2559
|
-
/**
|
|
2560
|
-
* Validate a policy for type mismatches, contradictions, redundancies, and vacuities.
|
|
2561
|
-
* @param data - The canonical policy data to validate.
|
|
2562
|
-
* @returns All validation issues found, ordered by group and constraint index.
|
|
2563
|
-
* @throws {CallciumError} If the descriptor is not well-formed; semantic validation is
|
|
2564
|
-
* defined only over a well-formed descriptor.
|
|
2565
|
-
*/
|
|
2566
2434
|
function validate(data) {
|
|
2567
|
-
const issues = [];
|
|
2568
2435
|
const descBytes = hexToBytes(data.descriptor);
|
|
2569
2436
|
decodeDescriptor(descBytes);
|
|
2437
|
+
const issues = [];
|
|
2570
2438
|
for (let groupIndex = 0; groupIndex < data.groups.length; groupIndex++) {
|
|
2571
2439
|
if (data.groups[groupIndex].length === 0) {
|
|
2572
2440
|
issues.push(emptyGroup(groupIndex));
|
|
@@ -2576,102 +2444,80 @@ function validate(data) {
|
|
|
2576
2444
|
}
|
|
2577
2445
|
return issues;
|
|
2578
2446
|
}
|
|
2579
|
-
/** Semantic validation for policies. */
|
|
2580
2447
|
const PolicyValidator = { validate };
|
|
2581
2448
|
//#endregion
|
|
2582
2449
|
//#region src/signature.ts
|
|
2583
|
-
/** Return true if the char code is an ASCII letter. */
|
|
2584
2450
|
function isAlpha(c) {
|
|
2585
2451
|
return c >= 65 && c <= 90 || c >= 97 && c <= 122;
|
|
2586
2452
|
}
|
|
2587
|
-
/** Return true if the char code is an ASCII alphanumeric character. */
|
|
2588
2453
|
function isAlphanum(c) {
|
|
2589
2454
|
return isAlpha(c) || c >= 48 && c <= 57;
|
|
2590
2455
|
}
|
|
2591
|
-
/**
|
|
2592
|
-
* Parse an ABI function signature into its selector and types.
|
|
2593
|
-
*
|
|
2594
|
-
* Strict mode: no whitespace, name must match `[A-Za-z_][A-Za-z0-9_]*`,
|
|
2595
|
-
* structure must be `name(types)` with the final `)` as the last character.
|
|
2596
|
-
*
|
|
2597
|
-
* @param signature - A function signature string, e.g. `"transfer(address,uint256)"`.
|
|
2598
|
-
* @returns The 4-byte selector and the comma-separated types string.
|
|
2599
|
-
* @throws {CallciumError} with code `"INVALID_SIGNATURE"` if the signature is malformed.
|
|
2600
|
-
*/
|
|
2601
2456
|
function parse(signature) {
|
|
2602
|
-
if (signature.length < 3) throw new CallciumError("
|
|
2457
|
+
if (signature.length < 3) throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: too short, got "${signature}"`);
|
|
2603
2458
|
let openParen = -1;
|
|
2604
2459
|
for (let i = 0; i < signature.length; i++) {
|
|
2605
2460
|
const c = signature.charCodeAt(i);
|
|
2606
|
-
if (c === 32 || c === 9 || c === 10 || c === 13) throw new CallciumError("
|
|
2607
|
-
if (c > 126) throw new CallciumError("
|
|
2461
|
+
if (c === 32 || c === 9 || c === 10 || c === 13) throw new CallciumError("SIGNATURE_CONTAINS_WHITESPACE", "Signature must not contain whitespace");
|
|
2462
|
+
if (c > 126) throw new CallciumError("MALFORMED_SIGNATURE", "Signature must contain only ASCII characters");
|
|
2608
2463
|
if (openParen === -1 && c === 40) openParen = i;
|
|
2609
2464
|
}
|
|
2610
|
-
if (openParen < 1) throw new CallciumError("
|
|
2611
|
-
if (!signature.endsWith(")")) throw new CallciumError("
|
|
2465
|
+
if (openParen < 1) throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: must have a function name followed by parentheses, got "${signature}"`);
|
|
2466
|
+
if (!signature.endsWith(")")) throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: must end with ")", got "${signature}"`);
|
|
2612
2467
|
const firstChar = signature.charCodeAt(0);
|
|
2613
|
-
if (!isAlpha(firstChar) && firstChar !== 95) throw new CallciumError("
|
|
2468
|
+
if (!isAlpha(firstChar) && firstChar !== 95) throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must start with a letter or underscore");
|
|
2614
2469
|
for (let i = 1; i < openParen; i++) {
|
|
2615
2470
|
const c = signature.charCodeAt(i);
|
|
2616
|
-
if (!isAlphanum(c) && c !== 95) throw new CallciumError("
|
|
2471
|
+
if (!isAlphanum(c) && c !== 95) throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must contain only alphanumeric characters or underscores");
|
|
2617
2472
|
}
|
|
2618
|
-
const types = signature.slice(openParen + 1, signature.length - 1);
|
|
2619
2473
|
const encoded = new Uint8Array(signature.length);
|
|
2620
2474
|
for (let i = 0; i < signature.length; i++) encoded[i] = signature.charCodeAt(i);
|
|
2621
2475
|
return {
|
|
2622
2476
|
selector: bytesToHex(keccak_256(encoded).subarray(0, 4)),
|
|
2623
|
-
types
|
|
2477
|
+
types: signature.slice(openParen + 1, signature.length - 1)
|
|
2624
2478
|
};
|
|
2625
2479
|
}
|
|
2626
|
-
/** ABI function signature parser. */
|
|
2627
2480
|
const SignatureParser = { parse };
|
|
2628
2481
|
//#endregion
|
|
2629
2482
|
//#region src/policy-builder.ts
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
/** Validate a calldata-scope path against the descriptor. */
|
|
2637
|
-
function validateCalldataPath(path, desc) {
|
|
2638
|
-
const steps = parsePathSteps(path);
|
|
2639
|
-
if (steps.length === 0) throw new CallciumError("INVALID_PATH", "Calldata path must have at least one step.");
|
|
2483
|
+
function validateContextPath(steps) {
|
|
2484
|
+
if (steps.length !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step.");
|
|
2485
|
+
const step = steps[0];
|
|
2486
|
+
if (step > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ID 0x${step.toString(16).padStart(4, "0")}.`);
|
|
2487
|
+
}
|
|
2488
|
+
function validateCalldataPath(steps, desc) {
|
|
2640
2489
|
const argIndex = steps[0];
|
|
2641
2490
|
const paramCount = Descriptor.paramCount(desc);
|
|
2642
|
-
if (argIndex >= paramCount) throw new CallciumError("
|
|
2491
|
+
if (argIndex >= paramCount) throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${argIndex} out of range (descriptor has ${paramCount} params).`);
|
|
2643
2492
|
let offset = Descriptor.paramOffset(desc, argIndex);
|
|
2644
2493
|
let hasQuantifier = false;
|
|
2645
2494
|
for (let i = 1; i < steps.length; i++) {
|
|
2646
2495
|
const step = steps[i];
|
|
2647
2496
|
const info = Descriptor.inspect(desc, offset);
|
|
2497
|
+
const isArray = info.typeCode === TypeCode.STATIC_ARRAY || info.typeCode === TypeCode.DYNAMIC_ARRAY;
|
|
2498
|
+
if (isQuantifier(step)) {
|
|
2499
|
+
if (!isArray) throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node.");
|
|
2500
|
+
if (hasQuantifier) throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed.");
|
|
2501
|
+
hasQuantifier = true;
|
|
2502
|
+
}
|
|
2648
2503
|
if (info.typeCode === TypeCode.TUPLE) {
|
|
2649
|
-
if (isQuantifier(step)) throw new CallciumError("INVALID_PATH", "Quantifier step is not valid on a tuple node.");
|
|
2650
2504
|
const fieldCount = Descriptor.tupleFieldCount(desc, offset);
|
|
2651
|
-
if (step >= fieldCount) throw new CallciumError("
|
|
2505
|
+
if (step >= fieldCount) throw new CallciumError("TUPLE_FIELD_OUT_OF_BOUNDS", `Tuple field index ${step} out of range (tuple has ${fieldCount} fields).`);
|
|
2652
2506
|
offset = Descriptor.tupleFieldOffset(desc, offset, step);
|
|
2653
|
-
} else if (
|
|
2654
|
-
if (isQuantifier(step)) {
|
|
2655
|
-
if (hasQuantifier) throw new CallciumError("INVALID_PATH", "Nested quantifiers are not allowed.");
|
|
2656
|
-
hasQuantifier = true;
|
|
2657
|
-
} else if (info.typeCode === TypeCode.STATIC_ARRAY) {
|
|
2507
|
+
} else if (isArray) {
|
|
2508
|
+
if (!isQuantifier(step) && info.typeCode === TypeCode.STATIC_ARRAY) {
|
|
2658
2509
|
const arrayLength = Descriptor.staticArrayLength(desc, offset);
|
|
2659
|
-
if (step >= arrayLength) throw new CallciumError("
|
|
2510
|
+
if (step >= arrayLength) throw new CallciumError("STATIC_ARRAY_INDEX_OUT_OF_BOUNDS", `Array index ${step} out of range (static array has ${arrayLength} elements).`);
|
|
2660
2511
|
}
|
|
2661
2512
|
offset = Descriptor.arrayElementOffset(offset);
|
|
2662
|
-
} else throw new CallciumError("
|
|
2513
|
+
} else throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type.");
|
|
2663
2514
|
}
|
|
2664
2515
|
}
|
|
2665
|
-
/** Fluent builder for constructing Callcium policies. */
|
|
2666
2516
|
var PolicyBuilder = class PolicyBuilder {
|
|
2667
2517
|
draft;
|
|
2668
2518
|
constructor(draft) {
|
|
2669
2519
|
this.draft = draft;
|
|
2670
2520
|
}
|
|
2671
|
-
/**
|
|
2672
|
-
* Create a builder from a function signature.
|
|
2673
|
-
* @param signature - ABI function signature, e.g. `"transfer(address,uint256)"`.
|
|
2674
|
-
*/
|
|
2675
2521
|
static create(signature) {
|
|
2676
2522
|
const parsed = SignatureParser.parse(signature);
|
|
2677
2523
|
const descriptor = DescriptorCoder.fromTypes(parsed.types);
|
|
@@ -2683,10 +2529,6 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2683
2529
|
pathHashes: [/* @__PURE__ */ new Set()]
|
|
2684
2530
|
});
|
|
2685
2531
|
}
|
|
2686
|
-
/**
|
|
2687
|
-
* Create a selectorless builder from a raw type string.
|
|
2688
|
-
* @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256"`.
|
|
2689
|
-
*/
|
|
2690
2532
|
static createRaw(typesCsv) {
|
|
2691
2533
|
return new PolicyBuilder({
|
|
2692
2534
|
isSelectorless: true,
|
|
@@ -2696,73 +2538,50 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2696
2538
|
pathHashes: [/* @__PURE__ */ new Set()]
|
|
2697
2539
|
});
|
|
2698
2540
|
}
|
|
2699
|
-
/**
|
|
2700
|
-
* Add a constraint to the current group.
|
|
2701
|
-
* @param constraint - A `Constraint` object or a `ConstraintBuilder` instance.
|
|
2702
|
-
*/
|
|
2703
2541
|
add(constraint) {
|
|
2704
2542
|
const c = {
|
|
2705
2543
|
scope: constraint.scope,
|
|
2706
2544
|
path: constraint.path,
|
|
2707
2545
|
operators: [...constraint.operators]
|
|
2708
2546
|
};
|
|
2709
|
-
if (c.operators.length === 0) throw new CallciumError("
|
|
2710
|
-
|
|
2711
|
-
|
|
2547
|
+
if (c.operators.length === 0) throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator.");
|
|
2548
|
+
const steps = parsePathSteps(c.path);
|
|
2549
|
+
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
|
|
2550
|
+
if (c.scope === Scope.CONTEXT) validateContextPath(steps);
|
|
2551
|
+
else if (c.scope === Scope.CALLDATA) validateCalldataPath(steps, this.draft.descriptor);
|
|
2712
2552
|
else throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${c.scope}.`);
|
|
2713
2553
|
const key = `${c.scope}:${c.path.toLowerCase()}`;
|
|
2714
2554
|
const currentHashes = this.draft.pathHashes[this.draft.pathHashes.length - 1];
|
|
2715
|
-
if (currentHashes.has(key)) throw new CallciumError("
|
|
2555
|
+
if (currentHashes.has(key)) throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${c.path} in the same group.`);
|
|
2716
2556
|
currentHashes.add(key);
|
|
2717
2557
|
this.draft.groups[this.draft.groups.length - 1].push(c);
|
|
2718
2558
|
return this;
|
|
2719
2559
|
}
|
|
2720
|
-
/** Start a new constraint group (OR branch). */
|
|
2721
2560
|
or() {
|
|
2722
2561
|
if (this.draft.groups[this.draft.groups.length - 1].length === 0) throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty.");
|
|
2723
2562
|
this.draft.groups.push([]);
|
|
2724
2563
|
this.draft.pathHashes.push(/* @__PURE__ */ new Set());
|
|
2725
2564
|
return this;
|
|
2726
2565
|
}
|
|
2727
|
-
/**
|
|
2728
|
-
* Build the policy into an encoded binary blob with strict validation.
|
|
2729
|
-
* Throws on any issue, regardless of severity. Use {@link validate} to
|
|
2730
|
-
* inspect issues, or {@link buildUnsafe} to encode without validation.
|
|
2731
|
-
* @returns The policy as a 0x-prefixed hex string.
|
|
2732
|
-
* @throws {CallciumError} If any group is empty or validation finds any issue.
|
|
2733
|
-
*/
|
|
2734
2566
|
build() {
|
|
2735
2567
|
this.checkGroups();
|
|
2736
2568
|
const policyData = this.toPolicyData();
|
|
2737
2569
|
const issues = PolicyValidator.validate(policyData);
|
|
2738
|
-
if (issues.length > 0) throw new
|
|
2570
|
+
if (issues.length > 0) throw new ValidationError(issues);
|
|
2739
2571
|
return PolicyCoder.encode(policyData);
|
|
2740
2572
|
}
|
|
2741
|
-
/**
|
|
2742
|
-
* Build the policy into an encoded binary blob without validation.
|
|
2743
|
-
* The resulting policy may be invalid. Prefer {@link build}; use this
|
|
2744
|
-
* only to knowingly bypass a reported issue.
|
|
2745
|
-
* @returns The policy as a 0x-prefixed hex string.
|
|
2746
|
-
* @throws {CallciumError} If any group is empty.
|
|
2747
|
-
*/
|
|
2748
2573
|
buildUnsafe() {
|
|
2749
2574
|
this.checkGroups();
|
|
2750
2575
|
return PolicyCoder.encode(this.toPolicyData());
|
|
2751
2576
|
}
|
|
2752
|
-
/**
|
|
2753
|
-
* Validate the policy without encoding.
|
|
2754
|
-
* @returns All validation issues found.
|
|
2755
|
-
*/
|
|
2756
2577
|
validate() {
|
|
2757
2578
|
this.checkGroups();
|
|
2758
2579
|
const policyData = this.toPolicyData();
|
|
2759
2580
|
return PolicyValidator.validate(policyData);
|
|
2760
2581
|
}
|
|
2761
|
-
/** Throw if any group is empty. */
|
|
2762
2582
|
checkGroups() {
|
|
2763
2583
|
for (let i = 0; i < this.draft.groups.length; i++) if (this.draft.groups[i].length === 0) throw new CallciumError("EMPTY_GROUP", `Group ${i} is empty.`);
|
|
2764
2584
|
}
|
|
2765
|
-
/** Convert the draft to a PolicyData structure. */
|
|
2766
2585
|
toPolicyData() {
|
|
2767
2586
|
return {
|
|
2768
2587
|
isSelectorless: this.draft.isSelectorless,
|
|
@@ -2774,13 +2593,9 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2774
2593
|
};
|
|
2775
2594
|
//#endregion
|
|
2776
2595
|
//#region src/constraint.ts
|
|
2777
|
-
/** Strip an optional 0x prefix and validate a 40-hex-char (20-byte) address body. */
|
|
2778
2596
|
function addressBody(value) {
|
|
2779
|
-
|
|
2780
|
-
if (body.length !== 40) throw new CallciumError("INVALID_CONSTRAINT", `Invalid address length: expected 40 hex chars, got ${body.length}`);
|
|
2781
|
-
return body;
|
|
2597
|
+
return toAddress(value).slice(2);
|
|
2782
2598
|
}
|
|
2783
|
-
/** Convert a scalar value to a 32-byte big-endian word. */
|
|
2784
2599
|
function encodeWord(value) {
|
|
2785
2600
|
const word = new Uint8Array(32);
|
|
2786
2601
|
if (typeof value === "boolean") {
|
|
@@ -2789,11 +2604,7 @@ function encodeWord(value) {
|
|
|
2789
2604
|
}
|
|
2790
2605
|
if (typeof value === "string") {
|
|
2791
2606
|
const body = addressBody(value);
|
|
2792
|
-
for (let i = 0; i < 20; i++)
|
|
2793
|
-
const byte = parseInt(body.substring(i * 2, i * 2 + 2), 16);
|
|
2794
|
-
if (Number.isNaN(byte)) throw new CallciumError("INVALID_CONSTRAINT", `Invalid hex character in address at position ${i * 2}`);
|
|
2795
|
-
word[12 + i] = byte;
|
|
2796
|
-
}
|
|
2607
|
+
for (let i = 0; i < 20; i++) word[12 + i] = parseInt(body.substring(i * 2, i * 2 + 2), 16);
|
|
2797
2608
|
return word;
|
|
2798
2609
|
}
|
|
2799
2610
|
let bigValue = typeof value === "number" ? BigInt(value) : value;
|
|
@@ -2803,14 +2614,12 @@ function encodeWord(value) {
|
|
|
2803
2614
|
}
|
|
2804
2615
|
return word;
|
|
2805
2616
|
}
|
|
2806
|
-
/** Pack a scalar value as a hex operator payload (opCode byte + 32-byte word). */
|
|
2807
2617
|
function singleOp(opCode, value) {
|
|
2808
2618
|
const buffer = new Uint8Array(33);
|
|
2809
2619
|
buffer[0] = opCode;
|
|
2810
2620
|
buffer.set(encodeWord(value), 1);
|
|
2811
2621
|
return bytesToHex(buffer);
|
|
2812
2622
|
}
|
|
2813
|
-
/** Pack a range operator (opCode byte + min word + max word). */
|
|
2814
2623
|
function rangeOp(opCode, min, max) {
|
|
2815
2624
|
if (min > max) throw new CallciumError("INVALID_RANGE", `Range min (${min}) must not exceed max (${max})`);
|
|
2816
2625
|
const buffer = new Uint8Array(65);
|
|
@@ -2819,7 +2628,7 @@ function rangeOp(opCode, min, max) {
|
|
|
2819
2628
|
buffer.set(encodeWord(max), 33);
|
|
2820
2629
|
return bytesToHex(buffer);
|
|
2821
2630
|
}
|
|
2822
|
-
|
|
2631
|
+
const MAX_SET_MEMBERS = Limits.MAX_SET_MEMBERS;
|
|
2823
2632
|
function setOp(opCode, values) {
|
|
2824
2633
|
const bigs = values.map((v) => {
|
|
2825
2634
|
if (typeof v === "bigint") return v;
|
|
@@ -2831,118 +2640,81 @@ function setOp(opCode, values) {
|
|
|
2831
2640
|
const deduped = [];
|
|
2832
2641
|
for (const value of bigs) if (deduped.length === 0 || deduped[deduped.length - 1] !== value) deduped.push(value);
|
|
2833
2642
|
if (deduped.length === 0) throw new CallciumError("EMPTY_SET", "Set must contain at least one value");
|
|
2643
|
+
if (deduped.length > MAX_SET_MEMBERS) throw new CallciumError("SET_TOO_LARGE", `Set must contain at most ${MAX_SET_MEMBERS} values`);
|
|
2834
2644
|
const buffer = new Uint8Array(1 + deduped.length * 32);
|
|
2835
2645
|
buffer[0] = opCode;
|
|
2836
2646
|
for (let i = 0; i < deduped.length; i++) buffer.set(encodeWord(deduped[i]), 1 + i * 32);
|
|
2837
2647
|
return bytesToHex(buffer);
|
|
2838
2648
|
}
|
|
2839
|
-
/** Encode a sequence of uint16 path steps as a big-endian hex string. */
|
|
2840
2649
|
function encodePath(steps) {
|
|
2841
2650
|
const buffer = new Uint8Array(steps.length * 2);
|
|
2842
2651
|
for (let i = 0; i < steps.length; i++) writeBE16(buffer, i * 2, steps[i]);
|
|
2843
2652
|
return bytesToHex(buffer);
|
|
2844
2653
|
}
|
|
2845
|
-
/**
|
|
2846
|
-
* Mutable builder that accumulates operators targeting a single path.
|
|
2847
|
-
* Implements the `Constraint` interface so it can be passed directly to policy builders.
|
|
2848
|
-
*/
|
|
2849
2654
|
var ConstraintBuilder = class {
|
|
2850
2655
|
scope;
|
|
2851
2656
|
path;
|
|
2852
2657
|
operators;
|
|
2853
|
-
/** @internal */
|
|
2854
2658
|
constructor(scope, path) {
|
|
2855
2659
|
this.scope = scope;
|
|
2856
2660
|
this.path = path;
|
|
2857
2661
|
this.operators = [];
|
|
2858
2662
|
}
|
|
2859
|
-
/** Push a pre-encoded operator hex string and return this for chaining. */
|
|
2860
2663
|
push(opHex) {
|
|
2861
2664
|
this.operators.push(opHex);
|
|
2862
2665
|
return this;
|
|
2863
2666
|
}
|
|
2864
|
-
/** Assert the value equals `value`. */
|
|
2865
2667
|
eq(value) {
|
|
2866
2668
|
return this.push(singleOp(Op.EQ, value));
|
|
2867
2669
|
}
|
|
2868
|
-
/** Assert the value does not equal `value`. */
|
|
2869
2670
|
neq(value) {
|
|
2870
2671
|
return this.push(singleOp(Op.EQ | Op.NOT, value));
|
|
2871
2672
|
}
|
|
2872
|
-
/** Assert the value is greater than `bound`. */
|
|
2873
2673
|
gt(bound) {
|
|
2874
2674
|
return this.push(singleOp(Op.GT, bound));
|
|
2875
2675
|
}
|
|
2876
|
-
/** Assert the value is less than `bound`. */
|
|
2877
2676
|
lt(bound) {
|
|
2878
2677
|
return this.push(singleOp(Op.LT, bound));
|
|
2879
2678
|
}
|
|
2880
|
-
/** Assert the value is greater than or equal to `bound`. */
|
|
2881
2679
|
gte(bound) {
|
|
2882
2680
|
return this.push(singleOp(Op.GTE, bound));
|
|
2883
2681
|
}
|
|
2884
|
-
/** Assert the value is less than or equal to `bound`. */
|
|
2885
2682
|
lte(bound) {
|
|
2886
2683
|
return this.push(singleOp(Op.LTE, bound));
|
|
2887
2684
|
}
|
|
2888
|
-
/**
|
|
2889
|
-
* Assert the value is within [min, max] inclusive.
|
|
2890
|
-
* @throws {CallciumError} If min > max.
|
|
2891
|
-
*/
|
|
2892
2685
|
between(min, max) {
|
|
2893
2686
|
return this.push(rangeOp(Op.BETWEEN, BigInt(min), BigInt(max)));
|
|
2894
2687
|
}
|
|
2895
|
-
/**
|
|
2896
|
-
* Assert the value is a member of the set.
|
|
2897
|
-
* Values are sorted and deduplicated before encoding.
|
|
2898
|
-
* @throws {CallciumError} If the set is empty after deduplication.
|
|
2899
|
-
*/
|
|
2900
2688
|
isIn(values) {
|
|
2901
2689
|
return this.push(setOp(Op.IN, values));
|
|
2902
2690
|
}
|
|
2903
|
-
/**
|
|
2904
|
-
* Assert the value is not a member of the set.
|
|
2905
|
-
* @throws {CallciumError} If the set is empty after deduplication.
|
|
2906
|
-
*/
|
|
2907
2691
|
notIn(values) {
|
|
2908
2692
|
return this.push(setOp(Op.IN | Op.NOT, values));
|
|
2909
2693
|
}
|
|
2910
|
-
/** Assert all bits in `mask` are set. */
|
|
2911
2694
|
bitmaskAll(mask) {
|
|
2912
2695
|
return this.push(singleOp(Op.BITMASK_ALL, mask));
|
|
2913
2696
|
}
|
|
2914
|
-
/** Assert at least one bit in `mask` is set. */
|
|
2915
2697
|
bitmaskAny(mask) {
|
|
2916
2698
|
return this.push(singleOp(Op.BITMASK_ANY, mask));
|
|
2917
2699
|
}
|
|
2918
|
-
/** Assert no bit in `mask` is set. */
|
|
2919
2700
|
bitmaskNone(mask) {
|
|
2920
2701
|
return this.push(singleOp(Op.BITMASK_NONE, mask));
|
|
2921
2702
|
}
|
|
2922
|
-
/** Assert the runtime length equals `n`. */
|
|
2923
2703
|
lengthEq(n) {
|
|
2924
2704
|
return this.push(singleOp(Op.LENGTH_EQ, n));
|
|
2925
2705
|
}
|
|
2926
|
-
/** Assert the runtime length is greater than `n`. */
|
|
2927
2706
|
lengthGt(n) {
|
|
2928
2707
|
return this.push(singleOp(Op.LENGTH_GT, n));
|
|
2929
2708
|
}
|
|
2930
|
-
/** Assert the runtime length is less than `n`. */
|
|
2931
2709
|
lengthLt(n) {
|
|
2932
2710
|
return this.push(singleOp(Op.LENGTH_LT, n));
|
|
2933
2711
|
}
|
|
2934
|
-
/** Assert the runtime length is greater than or equal to `n`. */
|
|
2935
2712
|
lengthGte(n) {
|
|
2936
2713
|
return this.push(singleOp(Op.LENGTH_GTE, n));
|
|
2937
2714
|
}
|
|
2938
|
-
/** Assert the runtime length is less than or equal to `n`. */
|
|
2939
2715
|
lengthLte(n) {
|
|
2940
2716
|
return this.push(singleOp(Op.LENGTH_LTE, n));
|
|
2941
2717
|
}
|
|
2942
|
-
/**
|
|
2943
|
-
* Assert the runtime length is within [min, max] inclusive.
|
|
2944
|
-
* @throws {CallciumError} If min > max.
|
|
2945
|
-
*/
|
|
2946
2718
|
lengthBetween(min, max) {
|
|
2947
2719
|
return this.push(rangeOp(Op.LENGTH_BETWEEN, BigInt(min), BigInt(max)));
|
|
2948
2720
|
}
|
|
@@ -2950,419 +2722,60 @@ var ConstraintBuilder = class {
|
|
|
2950
2722
|
function arg(...steps) {
|
|
2951
2723
|
return new ConstraintBuilder(Scope.CALLDATA, encodePath(steps));
|
|
2952
2724
|
}
|
|
2953
|
-
/** Target the `msg.sender` context property. */
|
|
2954
2725
|
function msgSender() {
|
|
2955
2726
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_SENDER]));
|
|
2956
2727
|
}
|
|
2957
|
-
/** Target the `msg.value` context property. */
|
|
2958
2728
|
function msgValue() {
|
|
2959
2729
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_VALUE]));
|
|
2960
2730
|
}
|
|
2961
|
-
/** Target the `block.timestamp` context property. */
|
|
2962
2731
|
function blockTimestamp() {
|
|
2963
2732
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_TIMESTAMP]));
|
|
2964
2733
|
}
|
|
2965
|
-
/** Target the `block.number` context property. */
|
|
2966
2734
|
function blockNumber() {
|
|
2967
2735
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_NUMBER]));
|
|
2968
2736
|
}
|
|
2969
|
-
/** Target the `block.chainid` context property. */
|
|
2970
2737
|
function chainId() {
|
|
2971
2738
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.CHAIN_ID]));
|
|
2972
2739
|
}
|
|
2973
|
-
/** Target the `tx.origin` context property. */
|
|
2974
2740
|
function txOrigin() {
|
|
2975
2741
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.TX_ORIGIN]));
|
|
2976
2742
|
}
|
|
2977
|
-
/** Target the `block.basefee` context property. */
|
|
2978
2743
|
function baseFee() {
|
|
2979
2744
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BASE_FEE]));
|
|
2980
2745
|
}
|
|
2981
|
-
/** Target the `tx.gasprice` context property. */
|
|
2982
2746
|
function gasPrice() {
|
|
2983
2747
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.GAS_PRICE]));
|
|
2984
2748
|
}
|
|
2985
2749
|
//#endregion
|
|
2986
|
-
//#region src/reader.ts
|
|
2987
|
-
|
|
2988
|
-
function load32(callData, offset) {
|
|
2750
|
+
//#region src/calldata-reader.ts
|
|
2751
|
+
function loadWord(callData, offset) {
|
|
2989
2752
|
if (offset < 0 || offset + 32 > callData.length) return { code: "CALLDATA_OUT_OF_BOUNDS" };
|
|
2990
2753
|
return callData.subarray(offset, offset + 32);
|
|
2991
2754
|
}
|
|
2992
|
-
/**
|
|
2993
|
-
* Read ABI pointer (offset or length) from a 32-byte slot.
|
|
2994
|
-
* Validates high 28 bytes are zero (rejects values > 32-bit).
|
|
2995
|
-
*/
|
|
2996
2755
|
function readPointer(callData, head) {
|
|
2997
|
-
const word =
|
|
2756
|
+
const word = loadWord(callData, head);
|
|
2998
2757
|
if (word instanceof Uint8Array) {
|
|
2999
2758
|
for (let i = 0; i < 28; i++) if (word[i] !== 0) return { code: "CALLDATA_OUT_OF_BOUNDS" };
|
|
3000
2759
|
return (word[28] << 24 | word[29] << 16 | word[30] << 8 | word[31]) >>> 0;
|
|
3001
2760
|
}
|
|
3002
2761
|
return word;
|
|
3003
2762
|
}
|
|
3004
|
-
/** Return the number of bytes this node occupies in the head region. */
|
|
3005
|
-
function headContribution(node) {
|
|
3006
|
-
return node.isDynamic ? 32 : node.staticSize;
|
|
3007
|
-
}
|
|
3008
|
-
/** Read a single BE16 path step at the given step index. */
|
|
3009
|
-
function readStep(pathBytes, stepIndex) {
|
|
3010
|
-
return readU16(pathBytes, stepIndex * 2);
|
|
3011
|
-
}
|
|
3012
|
-
/** Navigate one path step through a node (tuple field, array element, or quantifier). */
|
|
3013
|
-
function descend(node, callData, head, base, childIndex) {
|
|
3014
|
-
if (node.type === "tuple") return descendTuple(node, callData, head, base, childIndex);
|
|
3015
|
-
if (node.type === "dynamicArray") return descendDynamicArray(node, callData, head, base, childIndex);
|
|
3016
|
-
if (node.type === "staticArray") return descendStaticArray(node, callData, head, base, childIndex);
|
|
3017
|
-
throw new CallciumError("INVALID_PATH", `Cannot descend into elementary type (${node.type}).`);
|
|
3018
|
-
}
|
|
3019
|
-
/** Descend into a tuple field by index. */
|
|
3020
|
-
function descendTuple(node, callData, head, base, childIndex) {
|
|
3021
|
-
if (childIndex >= node.fields.length) throw new CallciumError("INVALID_PATH", `Tuple field index ${childIndex} out of range (${node.fields.length} fields).`);
|
|
3022
|
-
let tupleBase;
|
|
3023
|
-
let cursor;
|
|
3024
|
-
if (node.isDynamic) {
|
|
3025
|
-
const ptrResult = readPointer(callData, head);
|
|
3026
|
-
if (typeof ptrResult !== "number") return {
|
|
3027
|
-
ok: false,
|
|
3028
|
-
code: ptrResult.code
|
|
3029
|
-
};
|
|
3030
|
-
tupleBase = base + ptrResult;
|
|
3031
|
-
cursor = tupleBase;
|
|
3032
|
-
} else {
|
|
3033
|
-
tupleBase = base;
|
|
3034
|
-
cursor = head;
|
|
3035
|
-
}
|
|
3036
|
-
for (let i = 0; i < childIndex; i++) cursor += headContribution(node.fields[i]);
|
|
3037
|
-
return {
|
|
3038
|
-
ok: true,
|
|
3039
|
-
location: {
|
|
3040
|
-
head: cursor,
|
|
3041
|
-
base: tupleBase,
|
|
3042
|
-
node: node.fields[childIndex]
|
|
3043
|
-
}
|
|
3044
|
-
};
|
|
3045
|
-
}
|
|
3046
|
-
/** Descend into a dynamic array element by index. */
|
|
3047
|
-
function descendDynamicArray(node, callData, head, base, childIndex) {
|
|
3048
|
-
const arrayBaseResult = readPointer(callData, head);
|
|
3049
|
-
if (typeof arrayBaseResult !== "number") return {
|
|
3050
|
-
ok: false,
|
|
3051
|
-
code: arrayBaseResult.code
|
|
3052
|
-
};
|
|
3053
|
-
const arrayBase = base + arrayBaseResult;
|
|
3054
|
-
const lengthResult = readPointer(callData, arrayBase);
|
|
3055
|
-
if (typeof lengthResult !== "number") return {
|
|
3056
|
-
ok: false,
|
|
3057
|
-
code: lengthResult.code
|
|
3058
|
-
};
|
|
3059
|
-
if (childIndex >= lengthResult) return {
|
|
3060
|
-
ok: false,
|
|
3061
|
-
code: "ARRAY_INDEX_OUT_OF_BOUNDS"
|
|
3062
|
-
};
|
|
3063
|
-
const headsBase = arrayBase + 32;
|
|
3064
|
-
const elem = node.element;
|
|
3065
|
-
if (elem.isDynamic) return {
|
|
3066
|
-
ok: true,
|
|
3067
|
-
location: {
|
|
3068
|
-
head: headsBase + childIndex * 32,
|
|
3069
|
-
base: headsBase,
|
|
3070
|
-
node: elem
|
|
3071
|
-
}
|
|
3072
|
-
};
|
|
3073
|
-
return {
|
|
3074
|
-
ok: true,
|
|
3075
|
-
location: {
|
|
3076
|
-
head: headsBase + childIndex * elem.staticSize,
|
|
3077
|
-
base: arrayBase,
|
|
3078
|
-
node: elem
|
|
3079
|
-
}
|
|
3080
|
-
};
|
|
3081
|
-
}
|
|
3082
|
-
/** Descend into a static array element by index. */
|
|
3083
|
-
function descendStaticArray(node, callData, head, base, childIndex) {
|
|
3084
|
-
if (childIndex >= node.length) throw new CallciumError("INVALID_PATH", `Static array index ${childIndex} out of range (length ${node.length}).`);
|
|
3085
|
-
const elem = node.element;
|
|
3086
|
-
if (elem.isDynamic) {
|
|
3087
|
-
const ptrResult = readPointer(callData, head);
|
|
3088
|
-
if (typeof ptrResult !== "number") return {
|
|
3089
|
-
ok: false,
|
|
3090
|
-
code: ptrResult.code
|
|
3091
|
-
};
|
|
3092
|
-
const arrayBase = base + ptrResult;
|
|
3093
|
-
return {
|
|
3094
|
-
ok: true,
|
|
3095
|
-
location: {
|
|
3096
|
-
head: arrayBase + childIndex * 32,
|
|
3097
|
-
base: arrayBase,
|
|
3098
|
-
node: elem
|
|
3099
|
-
}
|
|
3100
|
-
};
|
|
3101
|
-
}
|
|
3102
|
-
return {
|
|
3103
|
-
ok: true,
|
|
3104
|
-
location: {
|
|
3105
|
-
head: head + childIndex * elem.staticSize,
|
|
3106
|
-
base,
|
|
3107
|
-
node: elem
|
|
3108
|
-
}
|
|
3109
|
-
};
|
|
3110
|
-
}
|
|
3111
|
-
/**
|
|
3112
|
-
* Resolve a path to a calldata location, stopping at a leaf or quantifier.
|
|
3113
|
-
*
|
|
3114
|
-
* The path is a sequence of BE16 steps. The first step selects the top-level
|
|
3115
|
-
* parameter; subsequent steps descend into tuples and arrays. If a quantifier
|
|
3116
|
-
* step is encountered, returns a QuantifierResult with the array location and
|
|
3117
|
-
* remaining path suffix.
|
|
3118
|
-
*/
|
|
3119
|
-
function locate(tree, callData, pathBytes, baseOffset) {
|
|
3120
|
-
const stepCount = pathBytes.length / 2;
|
|
3121
|
-
if (stepCount === 0) throw new CallciumError("INVALID_PATH", "Path is empty.");
|
|
3122
|
-
const paramIndex = readStep(pathBytes, 0);
|
|
3123
|
-
if (paramIndex >= tree.length) throw new CallciumError("INVALID_PATH", `Param index ${paramIndex} out of range (${tree.length} params).`);
|
|
3124
|
-
let head = baseOffset;
|
|
3125
|
-
for (let i = 0; i < paramIndex; i++) head += headContribution(tree[i]);
|
|
3126
|
-
let base = baseOffset;
|
|
3127
|
-
let node = tree[paramIndex];
|
|
3128
|
-
for (let stepIndex = 1; stepIndex < stepCount; stepIndex++) {
|
|
3129
|
-
const step = readStep(pathBytes, stepIndex);
|
|
3130
|
-
if (isQuantifier(step)) {
|
|
3131
|
-
const remaining = pathBytes.subarray((stepIndex + 1) * 2);
|
|
3132
|
-
for (let remainingStepIndex = 0; remainingStepIndex < remaining.length / 2; remainingStepIndex++) if (isQuantifier(readStep(remaining, remainingStepIndex))) throw new CallciumError("INVALID_QUANTIFIER", "Nested quantifiers are not supported.");
|
|
3133
|
-
return {
|
|
3134
|
-
ok: true,
|
|
3135
|
-
type: "quantifier",
|
|
3136
|
-
quantifier: {
|
|
3137
|
-
quantifier: step,
|
|
3138
|
-
location: {
|
|
3139
|
-
head,
|
|
3140
|
-
base,
|
|
3141
|
-
node
|
|
3142
|
-
},
|
|
3143
|
-
remainingPath: remaining
|
|
3144
|
-
}
|
|
3145
|
-
};
|
|
3146
|
-
}
|
|
3147
|
-
const result = descend(node, callData, head, base, step);
|
|
3148
|
-
if (!result.ok) return {
|
|
3149
|
-
ok: false,
|
|
3150
|
-
code: result.code
|
|
3151
|
-
};
|
|
3152
|
-
head = result.location.head;
|
|
3153
|
-
base = result.location.base;
|
|
3154
|
-
node = result.location.node;
|
|
3155
|
-
}
|
|
3156
|
-
return {
|
|
3157
|
-
ok: true,
|
|
3158
|
-
type: "leaf",
|
|
3159
|
-
location: {
|
|
3160
|
-
head,
|
|
3161
|
-
base,
|
|
3162
|
-
node
|
|
3163
|
-
}
|
|
3164
|
-
};
|
|
3165
|
-
}
|
|
3166
|
-
/**
|
|
3167
|
-
* Build the element-shape fields shared by dynamic- and static-array nodes, branching only on
|
|
3168
|
-
* whether the element itself is dynamic.
|
|
3169
|
-
*/
|
|
3170
|
-
function makeArrayShape(length, elem, dynamicHeadsBase, staticDataOffset, staticCompositeBase) {
|
|
3171
|
-
return elem.isDynamic ? {
|
|
3172
|
-
length,
|
|
3173
|
-
elementNode: elem,
|
|
3174
|
-
elementIsDynamic: true,
|
|
3175
|
-
elementStaticSize: 0,
|
|
3176
|
-
headsBase: dynamicHeadsBase,
|
|
3177
|
-
dataOffset: 0,
|
|
3178
|
-
compositeBase: dynamicHeadsBase
|
|
3179
|
-
} : {
|
|
3180
|
-
length,
|
|
3181
|
-
elementNode: elem,
|
|
3182
|
-
elementIsDynamic: false,
|
|
3183
|
-
elementStaticSize: elem.staticSize,
|
|
3184
|
-
headsBase: 0,
|
|
3185
|
-
dataOffset: staticDataOffset,
|
|
3186
|
-
compositeBase: staticCompositeBase
|
|
3187
|
-
};
|
|
3188
|
-
}
|
|
3189
|
-
/** Extract array shape from a location pointing to an array node. */
|
|
3190
|
-
function arrayShape(callData, location) {
|
|
3191
|
-
const { head, base, node } = location;
|
|
3192
|
-
if (node.type === "dynamicArray") {
|
|
3193
|
-
const arrayBaseResult = readPointer(callData, head);
|
|
3194
|
-
if (typeof arrayBaseResult !== "number") return {
|
|
3195
|
-
ok: false,
|
|
3196
|
-
code: arrayBaseResult.code
|
|
3197
|
-
};
|
|
3198
|
-
const arrayBase = base + arrayBaseResult;
|
|
3199
|
-
const lengthResult = readPointer(callData, arrayBase);
|
|
3200
|
-
if (typeof lengthResult !== "number") return {
|
|
3201
|
-
ok: false,
|
|
3202
|
-
code: lengthResult.code
|
|
3203
|
-
};
|
|
3204
|
-
const headsBase = arrayBase + 32;
|
|
3205
|
-
return {
|
|
3206
|
-
ok: true,
|
|
3207
|
-
shape: makeArrayShape(lengthResult, node.element, headsBase, headsBase, arrayBase)
|
|
3208
|
-
};
|
|
3209
|
-
}
|
|
3210
|
-
if (node.type === "staticArray") {
|
|
3211
|
-
const elem = node.element;
|
|
3212
|
-
let dynamicHeadsBase = 0;
|
|
3213
|
-
if (elem.isDynamic) {
|
|
3214
|
-
const arrayBaseResult = readPointer(callData, head);
|
|
3215
|
-
if (typeof arrayBaseResult !== "number") return {
|
|
3216
|
-
ok: false,
|
|
3217
|
-
code: arrayBaseResult.code
|
|
3218
|
-
};
|
|
3219
|
-
dynamicHeadsBase = base + arrayBaseResult;
|
|
3220
|
-
}
|
|
3221
|
-
return {
|
|
3222
|
-
ok: true,
|
|
3223
|
-
shape: makeArrayShape(node.length, elem, dynamicHeadsBase, head, base)
|
|
3224
|
-
};
|
|
3225
|
-
}
|
|
3226
|
-
throw new CallciumError("INVALID_PATH", `Cannot compute array shape for non-array node (${node.type}).`);
|
|
3227
|
-
}
|
|
3228
|
-
/** Compute the location for element N within a precomputed array shape. */
|
|
3229
|
-
function arrayElementAt(shape, index, callData) {
|
|
3230
|
-
if (index >= shape.length) return {
|
|
3231
|
-
ok: false,
|
|
3232
|
-
code: "ARRAY_INDEX_OUT_OF_BOUNDS"
|
|
3233
|
-
};
|
|
3234
|
-
let head;
|
|
3235
|
-
let base;
|
|
3236
|
-
if (!shape.elementIsDynamic) {
|
|
3237
|
-
head = shape.dataOffset + index * shape.elementStaticSize;
|
|
3238
|
-
base = shape.compositeBase;
|
|
3239
|
-
} else {
|
|
3240
|
-
head = shape.headsBase + index * 32;
|
|
3241
|
-
base = shape.headsBase;
|
|
3242
|
-
}
|
|
3243
|
-
if (head + 32 > callData.length) return {
|
|
3244
|
-
ok: false,
|
|
3245
|
-
code: "CALLDATA_OUT_OF_BOUNDS"
|
|
3246
|
-
};
|
|
3247
|
-
return {
|
|
3248
|
-
ok: true,
|
|
3249
|
-
location: {
|
|
3250
|
-
head,
|
|
3251
|
-
base,
|
|
3252
|
-
node: shape.elementNode
|
|
3253
|
-
}
|
|
3254
|
-
};
|
|
3255
|
-
}
|
|
3256
|
-
/** Load a 32-byte scalar value from a resolved location. */
|
|
3257
|
-
function loadScalar(callData, location) {
|
|
3258
|
-
const result = load32(callData, location.head);
|
|
3259
|
-
if (result instanceof Uint8Array) return {
|
|
3260
|
-
ok: true,
|
|
3261
|
-
value: result
|
|
3262
|
-
};
|
|
3263
|
-
return {
|
|
3264
|
-
ok: false,
|
|
3265
|
-
code: result.code
|
|
3266
|
-
};
|
|
3267
|
-
}
|
|
3268
|
-
/** Load the data offset and logical length for a dynamic type (bytes, string, or dynamic array). */
|
|
3269
|
-
function loadSlice(callData, location) {
|
|
3270
|
-
const ptrResult = readPointer(callData, location.head);
|
|
3271
|
-
if (typeof ptrResult !== "number") return {
|
|
3272
|
-
ok: false,
|
|
3273
|
-
code: ptrResult.code
|
|
3274
|
-
};
|
|
3275
|
-
const dataStart = location.base + ptrResult;
|
|
3276
|
-
const lengthResult = readPointer(callData, dataStart);
|
|
3277
|
-
if (typeof lengthResult !== "number") return {
|
|
3278
|
-
ok: false,
|
|
3279
|
-
code: lengthResult.code
|
|
3280
|
-
};
|
|
3281
|
-
const dataOffset = dataStart + 32;
|
|
3282
|
-
const node = location.node;
|
|
3283
|
-
const stride = node.type === "dynamicArray" ? node.element.isDynamic ? 32 : node.element.staticSize : 1;
|
|
3284
|
-
if (dataOffset > callData.length || lengthResult > Math.floor((callData.length - dataOffset) / stride)) return {
|
|
3285
|
-
ok: false,
|
|
3286
|
-
code: "CALLDATA_OUT_OF_BOUNDS"
|
|
3287
|
-
};
|
|
3288
|
-
return {
|
|
3289
|
-
ok: true,
|
|
3290
|
-
dataOffset,
|
|
3291
|
-
length: lengthResult
|
|
3292
|
-
};
|
|
3293
|
-
}
|
|
3294
|
-
/** Continue navigating through additional path steps from a resolved location. */
|
|
3295
|
-
function descendPath(callData, location, pathBytes) {
|
|
3296
|
-
const stepCount = pathBytes.length / 2;
|
|
3297
|
-
let { head, base, node } = location;
|
|
3298
|
-
for (let stepIndex = 0; stepIndex < stepCount; stepIndex++) {
|
|
3299
|
-
const step = readStep(pathBytes, stepIndex);
|
|
3300
|
-
const result = descend(node, callData, head, base, step);
|
|
3301
|
-
if (!result.ok) return {
|
|
3302
|
-
ok: false,
|
|
3303
|
-
code: result.code
|
|
3304
|
-
};
|
|
3305
|
-
head = result.location.head;
|
|
3306
|
-
base = result.location.base;
|
|
3307
|
-
node = result.location.node;
|
|
3308
|
-
}
|
|
3309
|
-
return {
|
|
3310
|
-
ok: true,
|
|
3311
|
-
location: {
|
|
3312
|
-
head,
|
|
3313
|
-
base,
|
|
3314
|
-
node
|
|
3315
|
-
}
|
|
3316
|
-
};
|
|
3317
|
-
}
|
|
3318
2763
|
//#endregion
|
|
3319
2764
|
//#region src/policy-enforcer.ts
|
|
3320
|
-
/** Violations that abort evaluation instead of failing only their group (spec §9.3). */
|
|
3321
2765
|
const ABORT_VIOLATION_CODES = new Set([
|
|
3322
2766
|
"CALLDATA_OUT_OF_BOUNDS",
|
|
3323
2767
|
"ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
3324
2768
|
"NON_CANONICAL_VALUE",
|
|
3325
2769
|
"QUANTIFIER_LIMIT_EXCEEDED"
|
|
3326
2770
|
]);
|
|
3327
|
-
/** Convert a hex address to a 256-bit bigint (zero-padded to 32 bytes). */
|
|
3328
2771
|
function addressToBigInt(hex) {
|
|
3329
2772
|
const bytes = hexToBytes(hex);
|
|
3330
2773
|
const padded = new Uint8Array(32);
|
|
3331
2774
|
padded.set(bytes, 32 - bytes.length);
|
|
3332
2775
|
return toBigInt(padded, 0);
|
|
3333
2776
|
}
|
|
3334
|
-
/**
|
|
3335
|
-
* Walk a descriptor subtree through path steps and return the terminal leaf type code.
|
|
3336
|
-
*
|
|
3337
|
-
* Used only for violation metadata. Valid policies have structurally valid
|
|
3338
|
-
* quantifier suffixes (enforced by `PolicyValidator`); if a hand-crafted blob
|
|
3339
|
-
* violates that invariant, return the last resolvable node's type code as
|
|
3340
|
-
* best-effort diagnostic context.
|
|
3341
|
-
*/
|
|
3342
|
-
function resolveLeafTypeCode(elementNode, remainingPath) {
|
|
3343
|
-
let node = elementNode;
|
|
3344
|
-
const stepCount = remainingPath.length / 2;
|
|
3345
|
-
for (let step = 0; step < stepCount; step++) {
|
|
3346
|
-
const childIndex = readU16(remainingPath, step * 2);
|
|
3347
|
-
if (node.type === "tuple") {
|
|
3348
|
-
const field = node.fields[childIndex];
|
|
3349
|
-
if (!field) break;
|
|
3350
|
-
node = field;
|
|
3351
|
-
} else if (node.type === "staticArray" || node.type === "dynamicArray") node = node.element;
|
|
3352
|
-
else break;
|
|
3353
|
-
}
|
|
3354
|
-
return node.typeCode;
|
|
3355
|
-
}
|
|
3356
|
-
/**
|
|
3357
|
-
* Check a policy against ABI-encoded call data without throwing on violations.
|
|
3358
|
-
* @param policy - Binary policy blob as 0x-prefixed hex string.
|
|
3359
|
-
* @param callData - ABI-encoded call data as 0x-prefixed hex string.
|
|
3360
|
-
* @param context - Optional execution context for context-scoped rules.
|
|
3361
|
-
* @returns Pass with matched group index, or fail with one violation per evaluated failing group.
|
|
3362
|
-
* @throws {CallciumError} If the policy blob is structurally malformed.
|
|
3363
|
-
*/
|
|
3364
2777
|
function check(policy, callData, context) {
|
|
3365
|
-
const { policy: decoded,
|
|
2778
|
+
const { policy: decoded, data: policyBytes } = decodePolicy(policy);
|
|
3366
2779
|
const callDataBytes = hexToBytes(callData);
|
|
3367
2780
|
if (!decoded.isSelectorless) {
|
|
3368
2781
|
if (callDataBytes.length < 4) return {
|
|
@@ -3387,7 +2800,7 @@ function check(policy, callData, context) {
|
|
|
3387
2800
|
let groupFailed = false;
|
|
3388
2801
|
for (let ruleIndex = 0; ruleIndex < group.rules.length; ruleIndex++) {
|
|
3389
2802
|
const rule = group.rules[ruleIndex];
|
|
3390
|
-
const violation = evaluateRule(rule,
|
|
2803
|
+
const violation = evaluateRule(rule, policyBytes, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
|
|
3391
2804
|
if (violation !== null) {
|
|
3392
2805
|
allViolations.push(violation);
|
|
3393
2806
|
if (ABORT_VIOLATION_CODES.has(violation.code)) return {
|
|
@@ -3408,170 +2821,183 @@ function check(policy, callData, context) {
|
|
|
3408
2821
|
violations: allViolations
|
|
3409
2822
|
};
|
|
3410
2823
|
}
|
|
3411
|
-
/**
|
|
3412
|
-
* Enforce a policy against ABI-encoded call data, throwing on violation.
|
|
3413
|
-
* @param policy - Binary policy blob as 0x-prefixed hex string.
|
|
3414
|
-
* @param callData - ABI-encoded call data as 0x-prefixed hex string.
|
|
3415
|
-
* @param context - Optional execution context for context-scoped rules.
|
|
3416
|
-
* @throws {PolicyViolationError} If the policy rejects the call data.
|
|
3417
|
-
* @throws {CallciumError} If the policy blob is structurally malformed.
|
|
3418
|
-
*/
|
|
3419
2824
|
function enforce(policy, callData, context) {
|
|
3420
2825
|
const result = check(policy, callData, context);
|
|
3421
2826
|
if (!result.ok) throw new PolicyViolationError(result.violations);
|
|
3422
2827
|
}
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
2828
|
+
function fieldBytes(policyBytes, field) {
|
|
2829
|
+
return policyBytes.subarray(field.span.start, field.span.end);
|
|
2830
|
+
}
|
|
2831
|
+
function evaluateRule(rule, policyBytes, callDataBytes, baseOffset, groupIndex, ruleIndex, context) {
|
|
3427
2832
|
const opCode = rule.opCode.value;
|
|
3428
|
-
const operandData =
|
|
3429
|
-
if (scope === Scope.CONTEXT) return evaluateContextRule(
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
group: groupIndex,
|
|
3433
|
-
rule: ruleIndex,
|
|
3434
|
-
code: locResult.code,
|
|
3435
|
-
scope: Scope.CALLDATA,
|
|
3436
|
-
path: rule.path.value,
|
|
3437
|
-
opCode,
|
|
3438
|
-
operandData: rule.data.value
|
|
3439
|
-
};
|
|
3440
|
-
if (locResult.type === "quantifier") return evaluateQuantifier(locResult.quantifier, callDataBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value);
|
|
3441
|
-
return evaluateLeaf(locResult.location, callDataBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value);
|
|
2833
|
+
const operandData = fieldBytes(policyBytes, rule.data);
|
|
2834
|
+
if (rule.scope.value === Scope.CONTEXT) return evaluateContextRule(fieldBytes(policyBytes, rule.path), opCode, operandData, groupIndex, ruleIndex, rule.path.value, context);
|
|
2835
|
+
if (rule.hint === void 0) throw new CallciumError("MALFORMED_HINT", "Calldata rule carries no hint block");
|
|
2836
|
+
return evaluateCalldataRule(fieldBytes(policyBytes, rule.hint), callDataBytes, baseOffset, opCode, operandData, groupIndex, ruleIndex, rule.path.value);
|
|
3442
2837
|
}
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
2838
|
+
function readTargetBlock(hint, targetOffset) {
|
|
2839
|
+
return {
|
|
2840
|
+
targetDelta: readU32(hint, targetOffset),
|
|
2841
|
+
targetMeta: readU16(hint, targetOffset + PolicyFormat.HINT_TARGET_META_OFFSET),
|
|
2842
|
+
typeCode: hint[targetOffset + PolicyFormat.HINT_TARGET_TYPECODE_OFFSET]
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
function follow(callData, from, at) {
|
|
2846
|
+
const word = readPointer(callData, at);
|
|
2847
|
+
if (typeof word !== "number") return word;
|
|
2848
|
+
return from + word;
|
|
2849
|
+
}
|
|
2850
|
+
function chainResolve(hint, callData, base, hopsOffset, hopCount) {
|
|
2851
|
+
for (let i = 0; i < hopCount; i++) {
|
|
2852
|
+
const hop = hopsOffset + i * PolicyFormat.HINT_HOP_SIZE;
|
|
2853
|
+
const index = readU16(hint, hop + PolicyFormat.HINT_HOP_INDEX_OFFSET);
|
|
2854
|
+
if (index === PolicyFormat.HINT_NO_INDEX) {
|
|
2855
|
+
const next = follow(callData, base, base + readU32(hint, hop));
|
|
2856
|
+
if (typeof next !== "number") return next;
|
|
2857
|
+
base = next;
|
|
2858
|
+
continue;
|
|
2859
|
+
}
|
|
2860
|
+
const meta = readU16(hint, hop + PolicyFormat.HINT_HOP_META_OFFSET);
|
|
2861
|
+
let elems = base;
|
|
2862
|
+
if ((meta & PolicyFormat.HINT_META_DYNAMIC_ARRAY) !== 0) {
|
|
2863
|
+
const length = readPointer(callData, elems);
|
|
2864
|
+
if (typeof length !== "number") return length;
|
|
2865
|
+
if (index >= length) return { code: "ARRAY_INDEX_OUT_OF_BOUNDS" };
|
|
2866
|
+
elems += 32;
|
|
2867
|
+
}
|
|
2868
|
+
const slot = elems + index * (meta & PolicyFormat.HINT_META_STRIDE_MASK) * 32;
|
|
2869
|
+
if ((meta & PolicyFormat.HINT_META_ELEM_DYNAMIC) !== 0) {
|
|
2870
|
+
const next = follow(callData, elems, slot);
|
|
2871
|
+
if (typeof next !== "number") return next;
|
|
2872
|
+
base = next;
|
|
2873
|
+
} else base = slot;
|
|
2874
|
+
}
|
|
2875
|
+
return base;
|
|
3472
2876
|
}
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
if (isLengthOp(opCode)) {
|
|
3483
|
-
let valueLength;
|
|
3484
|
-
if (node.isDynamic) {
|
|
3485
|
-
const lengthResult = loadSlice(callDataBytes, location);
|
|
3486
|
-
if (!lengthResult.ok) return { error: lengthResult.code };
|
|
3487
|
-
valueLength = lengthResult.length;
|
|
3488
|
-
} else valueLength = node.staticSize;
|
|
2877
|
+
function evalTarget(callData, target, block, opCode, operandData) {
|
|
2878
|
+
const typeCode = block.typeCode;
|
|
2879
|
+
if (isLengthValidType(typeCode)) {
|
|
2880
|
+
if (!isLengthOp(opCode)) throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Value operator on a target without a scalar word.");
|
|
2881
|
+
const length = readPointer(callData, target);
|
|
2882
|
+
if (typeof length !== "number") return { error: length.code };
|
|
2883
|
+
const stride = typeCode === TypeCode.DYNAMIC_ARRAY ? (block.targetMeta & PolicyFormat.HINT_META_STRIDE_MASK) * 32 : 1;
|
|
2884
|
+
const room = callData.length - (target + 32);
|
|
2885
|
+
if (stride !== 0 && length > Math.floor(room / stride)) return { error: "CALLDATA_OUT_OF_BOUNDS" };
|
|
3489
2886
|
return {
|
|
3490
|
-
passed: applyOperator(opCode, 0n,
|
|
3491
|
-
|
|
2887
|
+
passed: applyOperator(opCode, 0n, length, operandData, typeCode),
|
|
2888
|
+
value: BigInt(length)
|
|
3492
2889
|
};
|
|
3493
2890
|
}
|
|
3494
|
-
if (
|
|
3495
|
-
const
|
|
3496
|
-
if (!
|
|
3497
|
-
const value = toBigInt(
|
|
3498
|
-
if (canonicalize(value,
|
|
2891
|
+
if (classifyTypeCode(typeCode).typeClass !== "elementary") throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Operator target does not carry a scalar word.");
|
|
2892
|
+
const word = loadWord(callData, target);
|
|
2893
|
+
if (!(word instanceof Uint8Array)) return { error: word.code };
|
|
2894
|
+
const value = toBigInt(word, 0);
|
|
2895
|
+
if (canonicalize(value, typeCode) !== value) return {
|
|
3499
2896
|
error: "NON_CANONICAL_VALUE",
|
|
3500
|
-
|
|
2897
|
+
value
|
|
3501
2898
|
};
|
|
3502
2899
|
return {
|
|
3503
|
-
passed: applyOperator(opCode, value, 32, operandData,
|
|
3504
|
-
|
|
2900
|
+
passed: applyOperator(opCode, value, 32, operandData, typeCode),
|
|
2901
|
+
value
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
function navigationViolation(frame, code, elementIndex) {
|
|
2905
|
+
return {
|
|
2906
|
+
group: frame.group,
|
|
2907
|
+
rule: frame.rule,
|
|
2908
|
+
code,
|
|
2909
|
+
scope: Scope.CALLDATA,
|
|
2910
|
+
path: frame.path,
|
|
2911
|
+
opCode: frame.opCode,
|
|
2912
|
+
operandData: bytesToHex(frame.operandData),
|
|
2913
|
+
typeCode: frame.typeCode,
|
|
2914
|
+
...elementIndex !== void 0 && { elementIndex }
|
|
3505
2915
|
};
|
|
3506
2916
|
}
|
|
3507
|
-
|
|
3508
|
-
function evaluateLeaf(location, callDataBytes, opCode, operandData, groupIndex, ruleIndex, pathHex) {
|
|
3509
|
-
const result = applyLeafOperator(callDataBytes, location, opCode, operandData);
|
|
3510
|
-
const typeCode = location.node.typeCode;
|
|
2917
|
+
function targetViolation(frame, result, elementIndex) {
|
|
3511
2918
|
if ("error" in result) {
|
|
3512
2919
|
if (result.error === "NON_CANONICAL_VALUE") return {
|
|
3513
|
-
group:
|
|
3514
|
-
rule:
|
|
2920
|
+
group: frame.group,
|
|
2921
|
+
rule: frame.rule,
|
|
3515
2922
|
code: result.error,
|
|
3516
2923
|
scope: Scope.CALLDATA,
|
|
3517
|
-
path:
|
|
3518
|
-
opCode,
|
|
3519
|
-
operandData: bytesToHex(operandData),
|
|
3520
|
-
typeCode,
|
|
3521
|
-
resolvedValue: result.
|
|
3522
|
-
|
|
3523
|
-
return {
|
|
3524
|
-
group: groupIndex,
|
|
3525
|
-
rule: ruleIndex,
|
|
3526
|
-
code: result.error,
|
|
3527
|
-
scope: Scope.CALLDATA,
|
|
3528
|
-
path: pathHex,
|
|
3529
|
-
opCode,
|
|
3530
|
-
operandData: bytesToHex(operandData),
|
|
3531
|
-
typeCode
|
|
2924
|
+
path: frame.path,
|
|
2925
|
+
opCode: frame.opCode,
|
|
2926
|
+
operandData: bytesToHex(frame.operandData),
|
|
2927
|
+
typeCode: frame.typeCode,
|
|
2928
|
+
resolvedValue: bigintToHex(result.value),
|
|
2929
|
+
...elementIndex !== void 0 && { elementIndex }
|
|
3532
2930
|
};
|
|
2931
|
+
return navigationViolation(frame, result.error, elementIndex);
|
|
3533
2932
|
}
|
|
3534
|
-
if (
|
|
3535
|
-
|
|
3536
|
-
|
|
2933
|
+
if (result.passed) return null;
|
|
2934
|
+
return {
|
|
2935
|
+
group: frame.group,
|
|
2936
|
+
rule: frame.rule,
|
|
3537
2937
|
code: "VALUE_MISMATCH",
|
|
3538
2938
|
scope: Scope.CALLDATA,
|
|
2939
|
+
path: frame.path,
|
|
2940
|
+
opCode: frame.opCode,
|
|
2941
|
+
operandData: bytesToHex(frame.operandData),
|
|
2942
|
+
typeCode: frame.typeCode,
|
|
2943
|
+
resolvedValue: bigintToHex(result.value),
|
|
2944
|
+
...elementIndex !== void 0 && { elementIndex }
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
function evaluateCalldataRule(hint, callDataBytes, baseOffset, opCode, operandData, groupIndex, ruleIndex, pathHex) {
|
|
2948
|
+
const header = hint[0];
|
|
2949
|
+
const hopCount = header & PolicyFormat.HINT_HOP_COUNT_MASK;
|
|
2950
|
+
const kind = header >> PolicyFormat.HINT_KIND_SHIFT;
|
|
2951
|
+
const hopsOffset = PolicyFormat.HINT_HEADER_SIZE;
|
|
2952
|
+
if (kind !== PolicyFormat.HINT_KIND_NONE) return evaluateQuantified(hint, callDataBytes, baseOffset, kind, hopsOffset, hopCount, opCode, operandData, groupIndex, ruleIndex, pathHex);
|
|
2953
|
+
const block = readTargetBlock(hint, hopsOffset + hopCount * PolicyFormat.HINT_HOP_SIZE);
|
|
2954
|
+
const frame = {
|
|
2955
|
+
group: groupIndex,
|
|
2956
|
+
rule: ruleIndex,
|
|
3539
2957
|
path: pathHex,
|
|
3540
2958
|
opCode,
|
|
3541
|
-
operandData
|
|
3542
|
-
typeCode
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
return
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
function
|
|
3549
|
-
const
|
|
3550
|
-
const
|
|
3551
|
-
const
|
|
3552
|
-
const
|
|
3553
|
-
const
|
|
3554
|
-
|
|
2959
|
+
operandData,
|
|
2960
|
+
typeCode: block.typeCode
|
|
2961
|
+
};
|
|
2962
|
+
const base = chainResolve(hint, callDataBytes, baseOffset, hopsOffset, hopCount);
|
|
2963
|
+
if (typeof base !== "number") return navigationViolation(frame, base.code);
|
|
2964
|
+
return targetViolation(frame, evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData));
|
|
2965
|
+
}
|
|
2966
|
+
function evaluateQuantified(hint, callDataBytes, baseOffset, kind, hopsOffset, hopCount, opCode, operandData, groupIndex, ruleIndex, pathHex) {
|
|
2967
|
+
const frameOffset = hopsOffset + hopCount * PolicyFormat.HINT_HOP_SIZE;
|
|
2968
|
+
const suffixHeaderOffset = frameOffset + PolicyFormat.HINT_FRAME_PREFIX_SIZE;
|
|
2969
|
+
const suffixHopCount = hint[suffixHeaderOffset] & PolicyFormat.HINT_HOP_COUNT_MASK;
|
|
2970
|
+
const suffixHopsOffset = suffixHeaderOffset + PolicyFormat.HINT_HEADER_SIZE;
|
|
2971
|
+
const block = readTargetBlock(hint, suffixHopsOffset + suffixHopCount * PolicyFormat.HINT_HOP_SIZE);
|
|
2972
|
+
const frame = {
|
|
3555
2973
|
group: groupIndex,
|
|
3556
2974
|
rule: ruleIndex,
|
|
3557
|
-
code: shapeResult.code,
|
|
3558
|
-
scope: Scope.CALLDATA,
|
|
3559
2975
|
path: pathHex,
|
|
3560
2976
|
opCode,
|
|
3561
|
-
operandData
|
|
3562
|
-
typeCode:
|
|
3563
|
-
};
|
|
3564
|
-
const
|
|
3565
|
-
if (
|
|
2977
|
+
operandData,
|
|
2978
|
+
typeCode: block.typeCode
|
|
2979
|
+
};
|
|
2980
|
+
const chained = chainResolve(hint, callDataBytes, baseOffset, hopsOffset, hopCount);
|
|
2981
|
+
if (typeof chained !== "number") return navigationViolation(frame, chained.code);
|
|
2982
|
+
let elems = chained + readU32(hint, frameOffset);
|
|
2983
|
+
let count = readU16(hint, frameOffset + PolicyFormat.HINT_FRAME_COUNT_OFFSET);
|
|
2984
|
+
if (count === 0) {
|
|
2985
|
+
const length = readPointer(callDataBytes, elems);
|
|
2986
|
+
if (typeof length !== "number") return navigationViolation(frame, length.code);
|
|
2987
|
+
count = length;
|
|
2988
|
+
elems += 32;
|
|
2989
|
+
}
|
|
2990
|
+
if (count > Limits.MAX_QUANTIFIED_ARRAY_LENGTH) return {
|
|
3566
2991
|
group: groupIndex,
|
|
3567
2992
|
rule: ruleIndex,
|
|
3568
2993
|
code: "QUANTIFIER_LIMIT_EXCEEDED",
|
|
3569
2994
|
scope: Scope.CALLDATA,
|
|
3570
2995
|
path: pathHex,
|
|
3571
|
-
resolvedValue: bigintToHex(BigInt(
|
|
2996
|
+
resolvedValue: bigintToHex(BigInt(count))
|
|
3572
2997
|
};
|
|
3573
|
-
|
|
3574
|
-
|
|
2998
|
+
const isUniversal = kind === PolicyFormat.HINT_KIND_ALL;
|
|
2999
|
+
if (count === 0) {
|
|
3000
|
+
if (isUniversal) return null;
|
|
3575
3001
|
return {
|
|
3576
3002
|
group: groupIndex,
|
|
3577
3003
|
rule: ruleIndex,
|
|
@@ -3580,84 +3006,28 @@ function evaluateQuantifier(qResult, callDataBytes, opCode, operandData, groupIn
|
|
|
3580
3006
|
path: pathHex
|
|
3581
3007
|
};
|
|
3582
3008
|
}
|
|
3583
|
-
const
|
|
3584
|
-
const
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
path: pathHex,
|
|
3594
|
-
opCode,
|
|
3595
|
-
operandData: operandHex,
|
|
3596
|
-
typeCode: leafTypeCode,
|
|
3597
|
-
elementIndex: elemIndex
|
|
3598
|
-
};
|
|
3599
|
-
continue;
|
|
3009
|
+
const meta = readU16(hint, frameOffset + PolicyFormat.HINT_FRAME_META_OFFSET);
|
|
3010
|
+
const elemStride = (meta & PolicyFormat.HINT_META_STRIDE_MASK) * 32;
|
|
3011
|
+
const elemIsDynamic = (meta & PolicyFormat.HINT_META_ELEM_DYNAMIC) !== 0;
|
|
3012
|
+
for (let elemIndex = 0; elemIndex < count; elemIndex++) {
|
|
3013
|
+
const slot = elems + elemIndex * elemStride;
|
|
3014
|
+
let elem = slot;
|
|
3015
|
+
if (elemIsDynamic) {
|
|
3016
|
+
const followed = follow(callDataBytes, elems, slot);
|
|
3017
|
+
if (typeof followed !== "number") return navigationViolation(frame, followed.code, elemIndex);
|
|
3018
|
+
elem = followed;
|
|
3600
3019
|
}
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
if (isUniversal) return {
|
|
3607
|
-
group: groupIndex,
|
|
3608
|
-
rule: ruleIndex,
|
|
3609
|
-
code: navResult.code,
|
|
3610
|
-
scope: Scope.CALLDATA,
|
|
3611
|
-
path: pathHex,
|
|
3612
|
-
opCode,
|
|
3613
|
-
operandData: operandHex,
|
|
3614
|
-
typeCode: leafTypeCode,
|
|
3615
|
-
elementIndex: elemIndex
|
|
3616
|
-
};
|
|
3617
|
-
continue;
|
|
3618
|
-
}
|
|
3619
|
-
leafLocation = navResult.location;
|
|
3620
|
-
} else leafLocation = elemLocation;
|
|
3621
|
-
const applied = applyLeafOperator(callDataBytes, leafLocation, opCode, operandData);
|
|
3622
|
-
if ("error" in applied) {
|
|
3623
|
-
if (applied.error === "NON_CANONICAL_VALUE") return {
|
|
3624
|
-
group: groupIndex,
|
|
3625
|
-
rule: ruleIndex,
|
|
3626
|
-
code: applied.error,
|
|
3627
|
-
scope: Scope.CALLDATA,
|
|
3628
|
-
path: pathHex,
|
|
3629
|
-
opCode,
|
|
3630
|
-
operandData: operandHex,
|
|
3631
|
-
typeCode: leafTypeCode,
|
|
3632
|
-
elementIndex: elemIndex,
|
|
3633
|
-
resolvedValue: applied.resolvedValue
|
|
3634
|
-
};
|
|
3635
|
-
if (isUniversal) return {
|
|
3636
|
-
group: groupIndex,
|
|
3637
|
-
rule: ruleIndex,
|
|
3638
|
-
code: applied.error,
|
|
3639
|
-
scope: Scope.CALLDATA,
|
|
3640
|
-
path: pathHex,
|
|
3641
|
-
opCode,
|
|
3642
|
-
operandData: operandHex,
|
|
3643
|
-
typeCode: leafTypeCode,
|
|
3644
|
-
elementIndex: elemIndex
|
|
3645
|
-
};
|
|
3646
|
-
continue;
|
|
3020
|
+
let base = elem;
|
|
3021
|
+
if (suffixHopCount > 0) {
|
|
3022
|
+
const chainedElem = chainResolve(hint, callDataBytes, elem, suffixHopsOffset, suffixHopCount);
|
|
3023
|
+
if (typeof chainedElem !== "number") return navigationViolation(frame, chainedElem.code, elemIndex);
|
|
3024
|
+
base = chainedElem;
|
|
3647
3025
|
}
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
path: pathHex,
|
|
3654
|
-
opCode,
|
|
3655
|
-
operandData: operandHex,
|
|
3656
|
-
typeCode: leafTypeCode,
|
|
3657
|
-
resolvedValue: applied.resolvedValue,
|
|
3658
|
-
elementIndex: elemIndex
|
|
3659
|
-
};
|
|
3660
|
-
if (!isUniversal && applied.passed) return null;
|
|
3026
|
+
const applied = evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData);
|
|
3027
|
+
if ("error" in applied) return targetViolation(frame, applied, elemIndex);
|
|
3028
|
+
if (applied.passed) {
|
|
3029
|
+
if (!isUniversal) return null;
|
|
3030
|
+
} else if (isUniversal) return targetViolation(frame, applied, elemIndex);
|
|
3661
3031
|
}
|
|
3662
3032
|
if (isUniversal) return null;
|
|
3663
3033
|
return {
|
|
@@ -3667,16 +3037,44 @@ function evaluateQuantifier(qResult, callDataBytes, opCode, operandData, groupIn
|
|
|
3667
3037
|
scope: Scope.CALLDATA,
|
|
3668
3038
|
path: pathHex,
|
|
3669
3039
|
opCode,
|
|
3670
|
-
operandData:
|
|
3671
|
-
typeCode:
|
|
3040
|
+
operandData: bytesToHex(operandData),
|
|
3041
|
+
typeCode: block.typeCode
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
function evaluateContextRule(pathBytes, opCode, operandData, groupIndex, ruleIndex, pathHex, context) {
|
|
3045
|
+
const propertyId = readU16(pathBytes, 0);
|
|
3046
|
+
if (propertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ID 0x${propertyId.toString(16).padStart(4, "0")}`);
|
|
3047
|
+
const propInfo = lookupContextProperty(propertyId);
|
|
3048
|
+
const contextValue = context?.[propInfo.contextKey];
|
|
3049
|
+
if (contextValue === void 0) return {
|
|
3050
|
+
group: groupIndex,
|
|
3051
|
+
rule: ruleIndex,
|
|
3052
|
+
code: "MISSING_CONTEXT",
|
|
3053
|
+
scope: Scope.CONTEXT,
|
|
3054
|
+
path: pathHex,
|
|
3055
|
+
typeCode: propInfo.typeCode
|
|
3056
|
+
};
|
|
3057
|
+
let value;
|
|
3058
|
+
if (typeof contextValue === "string") value = addressToBigInt(contextValue);
|
|
3059
|
+
else value = contextValue;
|
|
3060
|
+
if (!applyOperator(opCode, value, 32, operandData, propInfo.typeCode)) return {
|
|
3061
|
+
group: groupIndex,
|
|
3062
|
+
rule: ruleIndex,
|
|
3063
|
+
code: "VALUE_MISMATCH",
|
|
3064
|
+
scope: Scope.CONTEXT,
|
|
3065
|
+
path: pathHex,
|
|
3066
|
+
opCode,
|
|
3067
|
+
operandData: bytesToHex(operandData),
|
|
3068
|
+
typeCode: propInfo.typeCode,
|
|
3069
|
+
resolvedValue: bigintToHex(value)
|
|
3672
3070
|
};
|
|
3071
|
+
return null;
|
|
3673
3072
|
}
|
|
3674
|
-
/** Check and enforce policies against ABI-encoded call data. */
|
|
3675
3073
|
const PolicyEnforcer = {
|
|
3676
3074
|
check,
|
|
3677
3075
|
enforce
|
|
3678
3076
|
};
|
|
3679
3077
|
//#endregion
|
|
3680
|
-
export { CallciumError, ContextProperty, Descriptor, DescriptorCoder, Limits, Op, PolicyBuilder, PolicyCoder, PolicyEnforcer, PolicyValidator, PolicyViolationError, Quantifier, Scope, TypeCode, arg, baseFee, blockNumber, blockTimestamp, bytesToHex, chainId, gasPrice, hexToBytes, isLengthOp, isOpAllowed, isQuantifier, lookupContextProperty, lookupOp, lookupQuantifier, lookupScope, lookupTypeCode, msgSender, msgValue, parsePathSteps, toAddress, txOrigin };
|
|
3078
|
+
export { CallciumError, ContextProperty, Descriptor, DescriptorCoder, Limits, Op, PolicyBuilder, PolicyCoder, PolicyEnforcer, PolicyFormat, PolicyValidator, PolicyViolationError, Quantifier, Scope, TypeCode, ValidationError, arg, baseFee, blockNumber, blockTimestamp, bytesToHex, chainId, gasPrice, hexToBytes, isLengthOp, isOpAllowed, isQuantifier, lookupContextProperty, lookupOp, lookupQuantifier, lookupScope, lookupTypeCode, msgSender, msgValue, parsePathSteps, toAddress, txOrigin };
|
|
3681
3079
|
|
|
3682
3080
|
//# sourceMappingURL=index.js.map
|