@callcium/sdk 0.5.0 → 0.7.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/dist/index.d.ts +136 -46
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +871 -1411
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- 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
1
|
import { keccak_256 } from "@noble/hashes/sha3";
|
|
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 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 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,43 +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
|
-
if (typeCode === TypeCode.STATIC_ARRAY
|
|
534
|
+
if (typeCode === TypeCode.STATIC_ARRAY) if (isQuantifier(step)) quantifiedStaticLength = staticArrayLength(desc, cursor);
|
|
535
|
+
else {
|
|
536
|
+
const length = staticArrayLength(desc, cursor);
|
|
537
|
+
if (step >= length) throw new CallciumError("STATIC_ARRAY_INDEX_OUT_OF_BOUNDS", `Array index ${step} out of range (length=${length}).`);
|
|
538
|
+
}
|
|
607
539
|
cursor = arrayElementOffset(cursor);
|
|
608
|
-
} else throw new CallciumError("
|
|
540
|
+
} else throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}.`);
|
|
609
541
|
}
|
|
610
542
|
return {
|
|
611
543
|
typeInfo: inspect$1(desc, cursor),
|
|
612
544
|
quantifiedStaticLength
|
|
613
545
|
};
|
|
614
546
|
}
|
|
615
|
-
/**
|
|
616
|
-
* Return tuple field count at a tuple node offset.
|
|
617
|
-
* @param desc - Raw descriptor bytes.
|
|
618
|
-
* @param offset - Byte position of the tuple node.
|
|
619
|
-
*/
|
|
620
547
|
function tupleFieldCount(desc, offset) {
|
|
621
548
|
return readU16(desc, offset + 4);
|
|
622
549
|
}
|
|
623
|
-
/**
|
|
624
|
-
* Return static array length at a static array node offset.
|
|
625
|
-
* @param desc - Raw descriptor bytes.
|
|
626
|
-
* @param offset - Byte position of the static array node.
|
|
627
|
-
*/
|
|
628
550
|
function staticArrayLength(desc, offset) {
|
|
629
551
|
const elemOffset = arrayElementOffset(offset);
|
|
630
552
|
return readU16(desc, elemOffset + nodeLength(desc, elemOffset));
|
|
631
553
|
}
|
|
632
|
-
/**
|
|
633
|
-
* Return the byte offset of the array element descriptor.
|
|
634
|
-
* @param offset - Byte position of the array node (static or dynamic).
|
|
635
|
-
*/
|
|
636
554
|
function arrayElementOffset(offset) {
|
|
637
555
|
return offset + DescriptorFormat.ARRAY_HEADER_SIZE;
|
|
638
556
|
}
|
|
639
|
-
|
|
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
|
+
}
|
|
640
687
|
const Descriptor = {
|
|
641
688
|
paramCount,
|
|
642
689
|
paramOffset,
|
|
@@ -647,88 +694,47 @@ const Descriptor = {
|
|
|
647
694
|
staticArrayLength,
|
|
648
695
|
nodeLength,
|
|
649
696
|
arrayElementOffset,
|
|
650
|
-
walkPath
|
|
697
|
+
walkPath,
|
|
698
|
+
compileHint
|
|
651
699
|
};
|
|
652
700
|
//#endregion
|
|
653
701
|
//#region src/type-desc.ts
|
|
654
|
-
/**
|
|
655
|
-
* Extract the staticWords count from a descriptor node.
|
|
656
|
-
*
|
|
657
|
-
* For elementary types (typeCode < STATIC_ARRAY), returns 1 unless the type is
|
|
658
|
-
* BYTES or STRING, which are ABI-dynamic and return 0. For composite types,
|
|
659
|
-
* reads the 24-bit meta field at offset 1 and shifts right by
|
|
660
|
-
* META_STATIC_WORDS_SHIFT.
|
|
661
|
-
*/
|
|
662
702
|
function extractStaticWords(desc) {
|
|
663
703
|
const typeCode = desc[0];
|
|
664
704
|
if (typeCode < TypeCode.STATIC_ARRAY) return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING ? 0 : 1;
|
|
665
705
|
return readU24(desc, 1) >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
|
|
666
706
|
}
|
|
667
|
-
/** Build a descriptor node for `address`. */
|
|
668
707
|
function address() {
|
|
669
708
|
return new Uint8Array([TypeCode.ADDRESS]);
|
|
670
709
|
}
|
|
671
|
-
/** Build a descriptor node for `bool`. */
|
|
672
710
|
function bool() {
|
|
673
711
|
return new Uint8Array([TypeCode.BOOL]);
|
|
674
712
|
}
|
|
675
|
-
/** Build a descriptor node for `function`. */
|
|
676
713
|
function function_() {
|
|
677
714
|
return new Uint8Array([TypeCode.FUNCTION]);
|
|
678
715
|
}
|
|
679
|
-
/** Build a descriptor node for `uint256`. */
|
|
680
|
-
function uint256() {
|
|
681
|
-
return new Uint8Array([TypeCode.UINT_MAX]);
|
|
682
|
-
}
|
|
683
|
-
/**
|
|
684
|
-
* Build a descriptor node for `uintN`.
|
|
685
|
-
* @param bits - Bit width, 8–256 in steps of 8.
|
|
686
|
-
* @throws {CallciumError} If bits is out of range or not a multiple of 8.
|
|
687
|
-
*/
|
|
688
716
|
function uintN(bits) {
|
|
689
|
-
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.`);
|
|
690
718
|
return new Uint8Array([TypeCode.UINT_MIN + (bits / 8 - 1)]);
|
|
691
719
|
}
|
|
692
|
-
/**
|
|
693
|
-
* Build a descriptor node for `intN`.
|
|
694
|
-
* @param bits - Bit width, 8–256 in steps of 8.
|
|
695
|
-
* @throws {CallciumError} If bits is out of range or not a multiple of 8.
|
|
696
|
-
*/
|
|
697
720
|
function intN(bits) {
|
|
698
|
-
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.`);
|
|
699
722
|
return new Uint8Array([TypeCode.INT_MIN + (bits / 8 - 1)]);
|
|
700
723
|
}
|
|
701
|
-
/** Build a descriptor node for `bytes` (dynamic). */
|
|
702
724
|
function bytes() {
|
|
703
725
|
return new Uint8Array([TypeCode.BYTES]);
|
|
704
726
|
}
|
|
705
|
-
/** Build a descriptor node for `string`. */
|
|
706
727
|
function string_() {
|
|
707
728
|
return new Uint8Array([TypeCode.STRING]);
|
|
708
729
|
}
|
|
709
|
-
/**
|
|
710
|
-
* Build a descriptor node for `bytesN`.
|
|
711
|
-
* @param n - Byte width, 1–32.
|
|
712
|
-
* @throws {CallciumError} If n is out of range.
|
|
713
|
-
*/
|
|
714
730
|
function bytesN(n) {
|
|
715
|
-
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.`);
|
|
716
732
|
return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (n - 1)]);
|
|
717
733
|
}
|
|
718
|
-
/**
|
|
719
|
-
* Build a descriptor node for a dynamic array `T[]` or static array `T[length]`.
|
|
720
|
-
*
|
|
721
|
-
* Dynamic form: `[0x81][meta:3][elemDesc]`
|
|
722
|
-
* Static form: `[0x80][meta:3][elemDesc][length:be16]`
|
|
723
|
-
*
|
|
724
|
-
* @param elemDesc - Descriptor bytes for the element type.
|
|
725
|
-
* @param length - Fixed array length. When omitted, produces a dynamic array.
|
|
726
|
-
* @throws {CallciumError} If length is 0 or exceeds MAX_STATIC_ARRAY_LENGTH.
|
|
727
|
-
*/
|
|
728
734
|
function array(elemDesc, length) {
|
|
729
735
|
if (length === void 0) {
|
|
730
736
|
const nodeLength = DescriptorFormat.ARRAY_HEADER_SIZE + elemDesc.length;
|
|
731
|
-
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}.`);
|
|
732
738
|
const meta24 = 0 << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
|
|
733
739
|
const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + elemDesc.length);
|
|
734
740
|
buf[0] = TypeCode.DYNAMIC_ARRAY;
|
|
@@ -738,10 +744,10 @@ function array(elemDesc, length) {
|
|
|
738
744
|
}
|
|
739
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}.`);
|
|
740
746
|
const nodeLength = DescriptorFormat.ARRAY_HEADER_SIZE + elemDesc.length + DescriptorFormat.ARRAY_LENGTH_SIZE;
|
|
741
|
-
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}.`);
|
|
742
748
|
const elemStaticWords = extractStaticWords(elemDesc);
|
|
743
749
|
const staticWords = elemStaticWords === 0 ? 0 : length * elemStaticWords;
|
|
744
|
-
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}.`);
|
|
745
751
|
const meta24 = staticWords << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
|
|
746
752
|
const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + elemDesc.length + DescriptorFormat.ARRAY_LENGTH_SIZE);
|
|
747
753
|
buf[0] = TypeCode.STATIC_ARRAY;
|
|
@@ -750,19 +756,11 @@ function array(elemDesc, length) {
|
|
|
750
756
|
writeBE16(buf, 4 + elemDesc.length, length);
|
|
751
757
|
return buf;
|
|
752
758
|
}
|
|
753
|
-
/**
|
|
754
|
-
* Build a descriptor node for a tuple.
|
|
755
|
-
*
|
|
756
|
-
* Format: `[0x90][meta:3][fieldCount:be16][field0][field1]...`
|
|
757
|
-
*
|
|
758
|
-
* @param fieldDescs - Descriptor bytes for each field, in order.
|
|
759
|
-
* @throws {CallciumError} If fieldDescs is empty, exceeds MAX_TUPLE_FIELDS, or nodeLength overflows MAX_NODE_LENGTH.
|
|
760
|
-
*/
|
|
761
759
|
function tuple(fieldDescs) {
|
|
762
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}.`);
|
|
763
761
|
const totalFieldBytes = fieldDescs.reduce((sum, f) => sum + f.length, 0);
|
|
764
762
|
const nodeLength = DescriptorFormat.TUPLE_HEADER_SIZE + totalFieldBytes;
|
|
765
|
-
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}.`);
|
|
766
764
|
let anyDynamic = false;
|
|
767
765
|
let staticWordsSum = 0;
|
|
768
766
|
for (const field of fieldDescs) {
|
|
@@ -771,7 +769,7 @@ function tuple(fieldDescs) {
|
|
|
771
769
|
staticWordsSum += staticWords;
|
|
772
770
|
}
|
|
773
771
|
const staticWords = anyDynamic ? 0 : staticWordsSum;
|
|
774
|
-
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}.`);
|
|
775
773
|
const meta24 = staticWords << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
|
|
776
774
|
const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + DescriptorFormat.TUPLE_FIELDCOUNT_SIZE + totalFieldBytes);
|
|
777
775
|
buf[0] = TypeCode.TUPLE;
|
|
@@ -786,10 +784,6 @@ function tuple(fieldDescs) {
|
|
|
786
784
|
}
|
|
787
785
|
//#endregion
|
|
788
786
|
//#region src/descriptor-coder.ts
|
|
789
|
-
/**
|
|
790
|
-
* Collect the positions of depth-0 commas in input[start..end).
|
|
791
|
-
* Commas inside `()` or `[]` are skipped.
|
|
792
|
-
*/
|
|
793
787
|
function commaPositions(input, start, end) {
|
|
794
788
|
const positions = [];
|
|
795
789
|
let depth = 0;
|
|
@@ -801,26 +795,21 @@ function commaPositions(input, start, end) {
|
|
|
801
795
|
}
|
|
802
796
|
return positions;
|
|
803
797
|
}
|
|
804
|
-
|
|
805
|
-
* Parse a decimal integer from input[start..end).
|
|
806
|
-
* Returns -1 if the substring is empty or contains non-digit characters.
|
|
807
|
-
*/
|
|
798
|
+
const MAX_TYPE_STRING_NUMBER = 256 ** DescriptorFormat.ARRAY_LENGTH_SIZE - 1;
|
|
808
799
|
function parseUint(input, start, end) {
|
|
809
|
-
if (start >= end)
|
|
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}`);
|
|
810
802
|
let value = 0;
|
|
811
803
|
for (let i = start; i < end; i++) {
|
|
812
804
|
const code = input.charCodeAt(i);
|
|
813
|
-
if (code < 48 || code > 57)
|
|
805
|
+
if (code < 48 || code > 57) throw new CallciumError("MALFORMED_TYPE_STRING", `Non-digit character at position ${i}`);
|
|
814
806
|
value = value * 10 + (code - 48);
|
|
815
807
|
}
|
|
808
|
+
if (value > MAX_TYPE_STRING_NUMBER) throw new CallciumError("MALFORMED_TYPE_STRING", `Number at position ${start} is too wide`);
|
|
816
809
|
return value;
|
|
817
810
|
}
|
|
818
|
-
/**
|
|
819
|
-
* Parse a tuple literal `(field0,field1,...)` where the opening `(` is at
|
|
820
|
-
* `start` and the substring to parse ends before `end`.
|
|
821
|
-
*/
|
|
822
811
|
function parseTuple(input, start, end) {
|
|
823
|
-
if (input[start] !== "(") throw new CallciumError("
|
|
812
|
+
if (input[start] !== "(") throw new CallciumError("MALFORMED_TYPE_STRING", `Expected '(' at position ${start}`);
|
|
824
813
|
let depth = 0;
|
|
825
814
|
let closePos = -1;
|
|
826
815
|
for (let i = start; i < end; i++) if (input[i] === "(") depth++;
|
|
@@ -831,7 +820,8 @@ function parseTuple(input, start, end) {
|
|
|
831
820
|
break;
|
|
832
821
|
}
|
|
833
822
|
}
|
|
834
|
-
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}`);
|
|
835
825
|
const innerStart = start + 1;
|
|
836
826
|
const innerEnd = closePos;
|
|
837
827
|
const commas = commaPositions(input, innerStart, innerEnd);
|
|
@@ -842,10 +832,9 @@ function parseTuple(input, start, end) {
|
|
|
842
832
|
segStart = comma + 1;
|
|
843
833
|
}
|
|
844
834
|
segments.push([segStart, innerEnd]);
|
|
845
|
-
if (segments.length === 1 && segments[0][0] === segments[0][1])
|
|
835
|
+
if (segments.length === 1 && segments[0][0] === segments[0][1]) return tuple([]);
|
|
846
836
|
return tuple(segments.map(([fieldStart, fieldEnd]) => parseType(input, fieldStart, fieldEnd)));
|
|
847
837
|
}
|
|
848
|
-
/** Parse the base type (no array suffixes) from input[start..end). */
|
|
849
838
|
function parseBaseType(input, start, end) {
|
|
850
839
|
const segment = input.slice(start, end);
|
|
851
840
|
if (segment === "address") return address();
|
|
@@ -853,63 +842,31 @@ function parseBaseType(input, start, end) {
|
|
|
853
842
|
if (segment === "function") return function_();
|
|
854
843
|
if (segment === "bytes") return bytes();
|
|
855
844
|
if (segment === "string") return string_();
|
|
856
|
-
if (segment === "uint256") return uint256();
|
|
857
|
-
if (segment === "int256") return intN(256);
|
|
858
845
|
if (segment.startsWith("(")) return parseTuple(input, start, end);
|
|
859
|
-
if (segment.startsWith("uint"))
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
return uintN(bits);
|
|
863
|
-
}
|
|
864
|
-
if (segment.startsWith("int")) {
|
|
865
|
-
const bits = parseUint(input, start + 3, end);
|
|
866
|
-
if (bits === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
|
|
867
|
-
return intN(bits);
|
|
868
|
-
}
|
|
869
|
-
if (segment.startsWith("bytes")) {
|
|
870
|
-
const n = parseUint(input, start + 5, end);
|
|
871
|
-
if (n === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
|
|
872
|
-
return bytesN(n);
|
|
873
|
-
}
|
|
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));
|
|
874
849
|
throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
|
|
875
850
|
}
|
|
876
|
-
/**
|
|
877
|
-
* Parse a type segment from input[start..end).
|
|
878
|
-
*
|
|
879
|
-
* Scans backward from end to collect all `[...]` suffixes, identifies the
|
|
880
|
-
* base type extent, parses the base, then applies suffixes left-to-right.
|
|
881
|
-
*/
|
|
882
851
|
function parseType(input, start, end) {
|
|
883
|
-
if (start >= end) throw new CallciumError("
|
|
852
|
+
if (start >= end) throw new CallciumError("MALFORMED_TYPE_STRING", "Empty type segment");
|
|
884
853
|
const suffixes = [];
|
|
885
854
|
let baseEnd = end;
|
|
886
855
|
while (baseEnd > start && input[baseEnd - 1] === "]") {
|
|
887
856
|
const closePos = baseEnd - 1;
|
|
888
857
|
let openPos = closePos - 1;
|
|
889
858
|
while (openPos > start && input[openPos] !== "[") openPos--;
|
|
890
|
-
if (input[openPos] !== "[") throw new CallciumError("
|
|
859
|
+
if (input[openPos] !== "[") throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched ']' at position ${closePos}`);
|
|
891
860
|
const innerStart = openPos + 1;
|
|
892
861
|
const innerEnd = closePos;
|
|
893
|
-
|
|
894
|
-
else {
|
|
895
|
-
const length = parseUint(input, innerStart, innerEnd);
|
|
896
|
-
if (length === -1) throw new CallciumError("INVALID_TYPE_STRING", `Invalid array length at position ${innerStart}`);
|
|
897
|
-
suffixes.unshift(length);
|
|
898
|
-
}
|
|
862
|
+
suffixes.unshift(innerStart === innerEnd ? void 0 : parseUint(input, innerStart, innerEnd));
|
|
899
863
|
baseEnd = openPos;
|
|
900
864
|
}
|
|
865
|
+
if (input[start] !== "(" && input.slice(start, baseEnd).includes("[")) throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '[' in '${input.slice(start, baseEnd)}'`);
|
|
901
866
|
let desc = parseBaseType(input, start, baseEnd);
|
|
902
867
|
for (const length of suffixes) desc = array(desc, length);
|
|
903
868
|
return desc;
|
|
904
869
|
}
|
|
905
|
-
/**
|
|
906
|
-
* Encode a comma-separated list of ABI type strings into a binary descriptor.
|
|
907
|
-
*
|
|
908
|
-
* @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[],string"`.
|
|
909
|
-
* @returns Binary descriptor bytes starting with the version+paramCount header.
|
|
910
|
-
* @throws {CallciumError} With code `INVALID_TYPE_STRING` for malformed input.
|
|
911
|
-
* @throws {CallciumError} With code `UNKNOWN_TYPE` for unrecognised type names.
|
|
912
|
-
*/
|
|
913
870
|
function fromTypes(typesCsv) {
|
|
914
871
|
if (typesCsv === "") return new Uint8Array([DescriptorFormat.VERSION, 0]);
|
|
915
872
|
const commas = commaPositions(typesCsv, 0, typesCsv.length);
|
|
@@ -921,7 +878,7 @@ function fromTypes(typesCsv) {
|
|
|
921
878
|
}
|
|
922
879
|
segments.push([segStart, typesCsv.length]);
|
|
923
880
|
const paramDescs = segments.map(([start, end]) => parseType(typesCsv, start, end));
|
|
924
|
-
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}.`);
|
|
925
882
|
const totalBytes = paramDescs.reduce((sum, d) => sum + d.length, 0);
|
|
926
883
|
const result = new Uint8Array(DescriptorFormat.HEADER_SIZE + totalBytes);
|
|
927
884
|
result[0] = DescriptorFormat.VERSION;
|
|
@@ -933,7 +890,6 @@ function fromTypes(typesCsv) {
|
|
|
933
890
|
}
|
|
934
891
|
return result;
|
|
935
892
|
}
|
|
936
|
-
/** Reconstruct a Solidity type string from a descriptor node at the given offset. */
|
|
937
893
|
function nodeToTypeString(desc, offset) {
|
|
938
894
|
const typeCode = desc[offset];
|
|
939
895
|
if (typeCode === TypeCode.TUPLE) {
|
|
@@ -953,15 +909,6 @@ function nodeToTypeString(desc, offset) {
|
|
|
953
909
|
if (typeCode === TypeCode.DYNAMIC_ARRAY) return `${nodeToTypeString(desc, Descriptor.arrayElementOffset(offset))}[]`;
|
|
954
910
|
return lookupTypeCode(typeCode).label;
|
|
955
911
|
}
|
|
956
|
-
/**
|
|
957
|
-
* Reconstruct a comma-separated list of ABI type strings from a binary descriptor.
|
|
958
|
-
*
|
|
959
|
-
* Inverse of `fromTypes`: `toTypes(fromTypes(s))` returns `s` for any valid input.
|
|
960
|
-
*
|
|
961
|
-
* @param desc - Binary descriptor bytes (with version+paramCount header).
|
|
962
|
-
* @returns Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[]"`.
|
|
963
|
-
* @throws {CallciumError} If the descriptor is malformed.
|
|
964
|
-
*/
|
|
965
912
|
function toTypes(desc) {
|
|
966
913
|
const count = Descriptor.paramCount(desc);
|
|
967
914
|
const types = [];
|
|
@@ -972,23 +919,15 @@ function toTypes(desc) {
|
|
|
972
919
|
}
|
|
973
920
|
return types.join(",");
|
|
974
921
|
}
|
|
975
|
-
/** Recursively parse a single descriptor node starting at offset. */
|
|
976
922
|
function parseNode(data, offset, depth) {
|
|
977
923
|
if (offset >= data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
|
|
978
924
|
const code = data[offset];
|
|
979
925
|
const info = classifyTypeCode(code);
|
|
980
926
|
const metaOffset = offset + DescriptorFormat.TYPECODE_SIZE;
|
|
981
927
|
if (info.typeClass === "elementary") return {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
isDynamic: info.isDynamic,
|
|
986
|
-
staticSize: info.isDynamic ? 0 : 32,
|
|
987
|
-
span: {
|
|
988
|
-
start: offset,
|
|
989
|
-
end: metaOffset
|
|
990
|
-
}
|
|
991
|
-
},
|
|
928
|
+
typeCode: code,
|
|
929
|
+
isDynamic: info.isDynamic,
|
|
930
|
+
staticSize: info.isDynamic ? 0 : 32,
|
|
992
931
|
next: metaOffset
|
|
993
932
|
};
|
|
994
933
|
if (depth > DescriptorFormat.MAX_NESTING_DEPTH) throw new CallciumError("NESTING_TOO_DEEP", `Composite nesting exceeds maximum depth ${DescriptorFormat.MAX_NESTING_DEPTH}`, offset);
|
|
@@ -998,7 +937,7 @@ function parseNode(data, offset, depth) {
|
|
|
998
937
|
const staticWords = meta >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
|
|
999
938
|
const nodeLength = meta & DescriptorFormat.META_NODE_LENGTH_MASK;
|
|
1000
939
|
const minHeader = info.typeClass === "tuple" ? DescriptorFormat.TUPLE_HEADER_SIZE : DescriptorFormat.ARRAY_HEADER_SIZE;
|
|
1001
|
-
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);
|
|
1002
941
|
if (offset + nodeLength > data.length) throw new CallciumError("NODE_OVERFLOW", "Composite node extends beyond descriptor", offset);
|
|
1003
942
|
const isDynamic = staticWords === 0;
|
|
1004
943
|
const staticSize = isDynamic ? 0 : staticWords * 32;
|
|
@@ -1009,89 +948,54 @@ function parseNode(data, offset, depth) {
|
|
|
1009
948
|
const fieldCount = readU16(data, fieldCountOffset);
|
|
1010
949
|
if (fieldCount === 0) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", "Tuple must have at least one field", offset);
|
|
1011
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);
|
|
1012
|
-
const fields = [];
|
|
1013
951
|
let cursor = offset + DescriptorFormat.TUPLE_HEADER_SIZE;
|
|
1014
|
-
for (let i = 0; i < fieldCount; i++)
|
|
1015
|
-
const result = parseNode(data, cursor, depth + 1);
|
|
1016
|
-
fields.push(result.node);
|
|
1017
|
-
cursor = result.next;
|
|
1018
|
-
}
|
|
952
|
+
for (let i = 0; i < fieldCount; i++) cursor = parseNode(data, cursor, depth + 1).next;
|
|
1019
953
|
return {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
isDynamic,
|
|
1024
|
-
staticSize,
|
|
1025
|
-
fields,
|
|
1026
|
-
span: {
|
|
1027
|
-
start: offset,
|
|
1028
|
-
end: nodeEnd
|
|
1029
|
-
}
|
|
1030
|
-
},
|
|
954
|
+
typeCode: code,
|
|
955
|
+
isDynamic,
|
|
956
|
+
staticSize,
|
|
1031
957
|
next: nodeEnd
|
|
1032
958
|
};
|
|
1033
959
|
}
|
|
1034
960
|
if (info.typeClass === "staticArray") {
|
|
1035
|
-
const
|
|
1036
|
-
const lengthOffset = elemResult.next;
|
|
961
|
+
const lengthOffset = parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1).next;
|
|
1037
962
|
if (lengthOffset + DescriptorFormat.ARRAY_LENGTH_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Missing static array length suffix", offset);
|
|
1038
963
|
const length = readU16(data, lengthOffset);
|
|
1039
964
|
if (length === 0) throw new CallciumError("INVALID_ARRAY_LENGTH", "Static array length must be greater than zero", offset);
|
|
1040
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);
|
|
1041
966
|
return {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
isDynamic,
|
|
1046
|
-
staticSize,
|
|
1047
|
-
element: elemResult.node,
|
|
1048
|
-
length,
|
|
1049
|
-
span: {
|
|
1050
|
-
start: offset,
|
|
1051
|
-
end: nodeEnd
|
|
1052
|
-
}
|
|
1053
|
-
},
|
|
967
|
+
typeCode: code,
|
|
968
|
+
isDynamic,
|
|
969
|
+
staticSize,
|
|
1054
970
|
next: nodeEnd
|
|
1055
971
|
};
|
|
1056
972
|
}
|
|
973
|
+
parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
|
|
1057
974
|
return {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
isDynamic: true,
|
|
1062
|
-
staticSize: 0,
|
|
1063
|
-
element: parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1).node,
|
|
1064
|
-
span: {
|
|
1065
|
-
start: offset,
|
|
1066
|
-
end: nodeEnd
|
|
1067
|
-
}
|
|
1068
|
-
},
|
|
975
|
+
typeCode: code,
|
|
976
|
+
isDynamic: true,
|
|
977
|
+
staticSize: 0,
|
|
1069
978
|
next: nodeEnd
|
|
1070
979
|
};
|
|
1071
980
|
}
|
|
1072
|
-
/** Convert a zero-based param index to a BE16 hex path step. */
|
|
1073
981
|
function indexToPath(index) {
|
|
1074
982
|
return `0x${index.toString(16).padStart(4, "0")}`;
|
|
1075
983
|
}
|
|
1076
|
-
/** Decode a binary descriptor blob, returning the structural representation and AST. */
|
|
1077
984
|
function decodeDescriptor(data) {
|
|
1078
985
|
if (data.length < 1) throw new CallciumError("MALFORMED_HEADER", "Descriptor is empty");
|
|
1079
986
|
const version = data[0];
|
|
1080
987
|
if (version !== DescriptorFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${DescriptorFormat.VERSION})`);
|
|
1081
988
|
if (data.length < DescriptorFormat.HEADER_SIZE) throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
|
|
1082
|
-
const declaredCount = data[1];
|
|
1083
989
|
const params = [];
|
|
1084
|
-
const tree = [];
|
|
1085
990
|
let cursor = DescriptorFormat.HEADER_SIZE;
|
|
1086
991
|
while (cursor < data.length) {
|
|
1087
|
-
if (params.length >= DescriptorFormat.MAX_PARAMS) throw new CallciumError("
|
|
1088
|
-
const {
|
|
1089
|
-
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);
|
|
1090
994
|
params.push({
|
|
1091
995
|
index: params.length,
|
|
1092
|
-
typeCode
|
|
1093
|
-
isDynamic
|
|
1094
|
-
staticSize
|
|
996
|
+
typeCode,
|
|
997
|
+
isDynamic,
|
|
998
|
+
staticSize,
|
|
1095
999
|
path: indexToPath(params.length),
|
|
1096
1000
|
span: {
|
|
1097
1001
|
start: cursor,
|
|
@@ -1100,34 +1004,146 @@ function decodeDescriptor(data) {
|
|
|
1100
1004
|
});
|
|
1101
1005
|
cursor = next;
|
|
1102
1006
|
}
|
|
1007
|
+
const declaredCount = data[1];
|
|
1103
1008
|
if (params.length !== declaredCount) throw new CallciumError("PARAM_COUNT_MISMATCH", `Header declares ${declaredCount} params but ${params.length} were parsed`);
|
|
1104
|
-
return {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
},
|
|
1109
|
-
tree
|
|
1110
|
-
};
|
|
1009
|
+
return { descriptor: {
|
|
1010
|
+
version,
|
|
1011
|
+
params
|
|
1012
|
+
} };
|
|
1111
1013
|
}
|
|
1112
|
-
/** Encode and decode descriptors. */
|
|
1113
1014
|
const DescriptorCoder = {
|
|
1114
1015
|
fromTypes,
|
|
1115
1016
|
toTypes
|
|
1116
1017
|
};
|
|
1117
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
|
|
1118
1139
|
//#region src/policy-coder.ts
|
|
1119
|
-
/**
|
|
1120
|
-
* Parse a BE16-encoded hex path into an array of step values.
|
|
1121
|
-
* @param path - 0x-prefixed hex string containing BE16-encoded path steps.
|
|
1122
|
-
* @returns Array of numeric step values.
|
|
1123
|
-
*/
|
|
1124
1140
|
function parsePathSteps(path) {
|
|
1125
|
-
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`);
|
|
1126
1143
|
const steps = [];
|
|
1127
|
-
for (let
|
|
1144
|
+
for (let offset = 0; offset < bytes.length; offset += PolicyFormat.PATH_STEP_SIZE) steps.push(readU16(bytes, offset));
|
|
1128
1145
|
return steps;
|
|
1129
1146
|
}
|
|
1130
|
-
/** Wrap a value with its byte span for positional tracking. */
|
|
1131
1147
|
function field(value, start, end) {
|
|
1132
1148
|
return {
|
|
1133
1149
|
value,
|
|
@@ -1137,7 +1153,60 @@ function field(value, start, end) {
|
|
|
1137
1153
|
}
|
|
1138
1154
|
};
|
|
1139
1155
|
}
|
|
1140
|
-
|
|
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
|
+
}
|
|
1141
1210
|
function decodePolicy(blob) {
|
|
1142
1211
|
const data = hexToBytes(blob);
|
|
1143
1212
|
if (data.length < PolicyFormat.DESC_OFFSET + 1) throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short");
|
|
@@ -1145,10 +1214,10 @@ function decodePolicy(blob) {
|
|
|
1145
1214
|
const version = headerByte & PolicyFormat.VERSION_MASK;
|
|
1146
1215
|
if (version !== PolicyFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${PolicyFormat.VERSION})`);
|
|
1147
1216
|
if ((headerByte & PolicyFormat.RESERVED_MASK) !== 0) throw new CallciumError("MALFORMED_HEADER", "Reserved header bits must be zero");
|
|
1148
|
-
const isSelectorless = (headerByte & PolicyFormat.FLAG_NO_SELECTOR) !== 0;
|
|
1149
1217
|
const selectorStart = PolicyFormat.SELECTOR_OFFSET;
|
|
1150
1218
|
const selectorEnd = selectorStart + PolicyFormat.SELECTOR_SIZE;
|
|
1151
1219
|
const selectorHex = toHex(data, selectorStart, selectorEnd);
|
|
1220
|
+
const isSelectorless = (headerByte & PolicyFormat.FLAG_NO_SELECTOR) !== 0;
|
|
1152
1221
|
if (isSelectorless && selectorHex !== "0x00000000") throw new CallciumError("MALFORMED_HEADER", "Selectorless policy must have a zeroed selector slot");
|
|
1153
1222
|
const descLengthStart = PolicyFormat.DESC_LENGTH_OFFSET;
|
|
1154
1223
|
const descLengthValue = readU16(data, descLengthStart);
|
|
@@ -1157,7 +1226,7 @@ function decodePolicy(blob) {
|
|
|
1157
1226
|
const descEnd = descStart + descLengthValue;
|
|
1158
1227
|
const groupCountOffset = descEnd;
|
|
1159
1228
|
if (data.length < groupCountOffset + PolicyFormat.GROUP_COUNT_SIZE) throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short for descriptor and group count");
|
|
1160
|
-
const { descriptor: desc
|
|
1229
|
+
const { descriptor: desc } = decodeDescriptor(data.subarray(descStart, descEnd));
|
|
1161
1230
|
const params = desc.params.map((param) => ({
|
|
1162
1231
|
...param,
|
|
1163
1232
|
span: {
|
|
@@ -1174,55 +1243,66 @@ function decodePolicy(blob) {
|
|
|
1174
1243
|
const groups = [];
|
|
1175
1244
|
for (let groupIndex = 0; groupIndex < groupCountValue; groupIndex++) {
|
|
1176
1245
|
if (offset + PolicyFormat.GROUP_HEADER_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading group header", offset);
|
|
1177
|
-
const ruleCountStart = offset;
|
|
1178
|
-
const ruleCountValue = readU16(data, ruleCountStart);
|
|
1179
1246
|
const groupSizeStart = offset + PolicyFormat.GROUP_RULECOUNT_SIZE;
|
|
1180
1247
|
const groupSizeValue = readU32(data, groupSizeStart);
|
|
1181
1248
|
const groupBodyStart = offset + PolicyFormat.GROUP_HEADER_SIZE;
|
|
1182
1249
|
const groupEnd = groupBodyStart + groupSizeValue;
|
|
1250
|
+
const ruleCountStart = offset;
|
|
1251
|
+
const ruleCountValue = readU16(data, ruleCountStart);
|
|
1183
1252
|
if (ruleCountValue === 0) throw new CallciumError("EMPTY_GROUP", "Group must contain at least one rule", offset);
|
|
1184
|
-
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);
|
|
1185
1254
|
if (groupEnd > data.length) throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
|
|
1186
1255
|
const rules = [];
|
|
1187
1256
|
let ruleOffset = groupBodyStart;
|
|
1188
1257
|
for (let ruleIndex = 0; ruleIndex < ruleCountValue; ruleIndex++) {
|
|
1189
1258
|
if (ruleOffset + PolicyFormat.RULE_SIZE_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading rule size", ruleOffset);
|
|
1190
1259
|
const ruleSizeValue = readU16(data, ruleOffset);
|
|
1191
|
-
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);
|
|
1192
1261
|
if (ruleOffset + ruleSizeValue > groupEnd) throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
|
|
1193
1262
|
const scopeOffset = ruleOffset + PolicyFormat.RULE_SCOPE_OFFSET;
|
|
1194
1263
|
const scopeValue = data[scopeOffset];
|
|
1195
1264
|
if (scopeValue !== Scope.CONTEXT && scopeValue !== Scope.CALLDATA) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${scopeValue}`, ruleOffset);
|
|
1196
1265
|
const depthOffset = ruleOffset + PolicyFormat.RULE_DEPTH_OFFSET;
|
|
1197
1266
|
const depthValue = data[depthOffset];
|
|
1198
|
-
if (depthValue === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
|
|
1199
|
-
if (depthValue > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depthValue} exceeds maximum ${Limits.MAX_PATH_DEPTH}`, ruleOffset);
|
|
1200
|
-
if (scopeValue === Scope.CONTEXT && depthValue !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step", ruleOffset);
|
|
1201
1267
|
const pathStart = ruleOffset + PolicyFormat.RULE_PATH_OFFSET;
|
|
1202
1268
|
const pathLength = depthValue * PolicyFormat.PATH_STEP_SIZE;
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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);
|
|
1207
1275
|
}
|
|
1208
|
-
const opCodeOffset =
|
|
1209
|
-
const opCodeValue = data[opCodeOffset];
|
|
1276
|
+
const opCodeOffset = hintStart + hintSize;
|
|
1210
1277
|
const dataLengthOffset = opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE;
|
|
1211
1278
|
const dataLengthValue = readU16(data, dataLengthOffset);
|
|
1212
|
-
const
|
|
1213
|
-
const expectedRuleSize = PolicyFormat.RULE_FIXED_OVERHEAD + pathLength + dataLengthValue;
|
|
1279
|
+
const expectedRuleSize = PolicyFormat.RULE_FIXED_OVERHEAD + pathLength + hintSize + dataLengthValue;
|
|
1214
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];
|
|
1215
1282
|
const opBase = opCodeValue & ~Op.NOT;
|
|
1216
|
-
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;
|
|
1217
1288
|
if (opBase === Op.IN) {
|
|
1218
1289
|
const wordCount = dataLengthValue / 32;
|
|
1219
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);
|
|
1220
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);
|
|
1221
1300
|
rules.push({
|
|
1222
1301
|
ruleSize: field(ruleSizeValue, ruleOffset, ruleOffset + PolicyFormat.RULE_SIZE_SIZE),
|
|
1223
1302
|
scope: field(scopeValue, scopeOffset, scopeOffset + 1),
|
|
1224
1303
|
pathDepth: field(depthValue, depthOffset, depthOffset + 1),
|
|
1225
1304
|
path: field(pathHex, pathStart, pathStart + pathLength),
|
|
1305
|
+
...hintSize > 0 && { hint: field(toHex(data, hintStart, hintStart + hintSize), hintStart, hintStart + hintSize) },
|
|
1226
1306
|
opCode: field(opCodeValue, opCodeOffset, opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE),
|
|
1227
1307
|
dataLength: field(dataLengthValue, dataLengthOffset, dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE),
|
|
1228
1308
|
data: field(toHex(data, dataStart, dataStart + dataLengthValue), dataStart, dataStart + dataLengthValue),
|
|
@@ -1272,25 +1352,30 @@ function decodePolicy(blob) {
|
|
|
1272
1352
|
version,
|
|
1273
1353
|
isSelectorless
|
|
1274
1354
|
},
|
|
1275
|
-
|
|
1355
|
+
data
|
|
1276
1356
|
};
|
|
1277
1357
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
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).");
|
|
1280
1361
|
const path = hexToBytes(constraint.path);
|
|
1281
|
-
|
|
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) => ({
|
|
1282
1368
|
scope: constraint.scope,
|
|
1283
1369
|
path,
|
|
1284
|
-
operator
|
|
1370
|
+
operator,
|
|
1371
|
+
hint
|
|
1285
1372
|
}));
|
|
1286
1373
|
}
|
|
1287
|
-
/** Compare two byte arrays lexicographically. */
|
|
1288
1374
|
function compareBytes(a, b) {
|
|
1289
1375
|
const length = Math.min(a.length, b.length);
|
|
1290
1376
|
for (let i = 0; i < length; i++) if (a[i] !== b[i]) return a[i] - b[i];
|
|
1291
1377
|
return a.length - b.length;
|
|
1292
1378
|
}
|
|
1293
|
-
/** Sort rules by (scope, pathDepth, pathBytes, operatorBytes). */
|
|
1294
1379
|
function sortRules(rules) {
|
|
1295
1380
|
rules.sort((a, b) => {
|
|
1296
1381
|
if (a.scope !== b.scope) return a.scope - b.scope;
|
|
@@ -1302,30 +1387,24 @@ function sortRules(rules) {
|
|
|
1302
1387
|
return compareBytes(a.operator, b.operator);
|
|
1303
1388
|
});
|
|
1304
1389
|
}
|
|
1305
|
-
/** Serialize a single rule to its wire format bytes. */
|
|
1306
1390
|
function encodeRule(rule) {
|
|
1307
|
-
if (rule.path.length === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step.");
|
|
1308
|
-
if ((rule.path.length & 1) !== 0) throw new CallciumError("INVALID_PATH", "Path byte length must be even.");
|
|
1309
|
-
if (rule.operator.length < 1) throw new CallciumError("INVALID_OPERATOR", "Operator must have at least one byte (opcode).");
|
|
1310
1391
|
const depth = rule.path.length / 2;
|
|
1311
|
-
if (depth > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${Limits.MAX_PATH_DEPTH}.`);
|
|
1312
1392
|
if (rule.scope === Scope.CONTEXT && depth !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step.");
|
|
1313
|
-
const opCode = rule.operator[0];
|
|
1314
1393
|
const data = rule.operator.subarray(1);
|
|
1315
|
-
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;
|
|
1316
1395
|
if (ruleSize > 65535) throw new CallciumError("RULE_SIZE_OVERFLOW", `Rule size ${ruleSize} exceeds maximum 65535`);
|
|
1317
1396
|
const buf = new Uint8Array(ruleSize);
|
|
1318
1397
|
writeBE16(buf, 0, ruleSize);
|
|
1319
1398
|
buf[2] = rule.scope;
|
|
1320
1399
|
buf[3] = depth;
|
|
1321
1400
|
buf.set(rule.path, 4);
|
|
1322
|
-
|
|
1323
|
-
|
|
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];
|
|
1324
1404
|
writeBE16(buf, opOffset + 1, data.length);
|
|
1325
1405
|
buf.set(data, opOffset + 3);
|
|
1326
1406
|
return buf;
|
|
1327
1407
|
}
|
|
1328
|
-
/** Serialize all rules in a group to a single byte array. */
|
|
1329
1408
|
function encodeGroupRules(rules) {
|
|
1330
1409
|
const parts = rules.map(encodeRule);
|
|
1331
1410
|
const totalSize = parts.reduce((sum, p) => sum + p.length, 0);
|
|
@@ -1337,40 +1416,35 @@ function encodeGroupRules(rules) {
|
|
|
1337
1416
|
}
|
|
1338
1417
|
return buf;
|
|
1339
1418
|
}
|
|
1340
|
-
/** Build a single operator hex string from a decoded rule's opCode and data. */
|
|
1341
1419
|
function buildOperatorHex(rule) {
|
|
1342
1420
|
return `0x${rule.opCode.value.toString(16).padStart(2, "0")}${rule.data.value.slice(2)}`;
|
|
1343
1421
|
}
|
|
1344
|
-
/**
|
|
1345
|
-
* Encode a PolicyData structure into the canonical binary format.
|
|
1346
|
-
* @param data - The policy data to encode.
|
|
1347
|
-
* @returns The encoded policy as a 0x-prefixed hex string.
|
|
1348
|
-
*/
|
|
1349
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`);
|
|
1350
1426
|
const sortedGroups = data.groups.map((group) => {
|
|
1351
|
-
const rules = group.flatMap(flattenConstraint);
|
|
1427
|
+
const rules = group.flatMap((constraint) => flattenConstraint(constraint, descBytes));
|
|
1352
1428
|
sortRules(rules);
|
|
1353
1429
|
return rules;
|
|
1354
1430
|
});
|
|
1355
|
-
if (
|
|
1356
|
-
|
|
1357
|
-
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) => {
|
|
1358
1433
|
if (rules.length === 0) throw new CallciumError("EMPTY_GROUP", `Group ${groupIndex} is empty`);
|
|
1359
1434
|
if (rules.length > 65535) throw new CallciumError("RULE_COUNT_OVERFLOW", `Group ${groupIndex} rule count ${rules.length} exceeds maximum 65535`);
|
|
1360
|
-
const wireBytes = encodeGroupRules(rules);
|
|
1361
1435
|
return {
|
|
1362
|
-
wireBytes,
|
|
1363
|
-
hash: keccak_256(wireBytes),
|
|
1436
|
+
wireBytes: encodeGroupRules(rules),
|
|
1364
1437
|
ruleCount: rules.length
|
|
1365
1438
|
};
|
|
1366
1439
|
});
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
+
}
|
|
1370
1444
|
const selectorBytes = data.isSelectorless ? new Uint8Array(4) : hexToBytes(data.selector);
|
|
1371
1445
|
const headerByte = PolicyFormat.VERSION | (data.isSelectorless ? PolicyFormat.FLAG_NO_SELECTOR : 0);
|
|
1372
1446
|
let totalSize = PolicyFormat.HEADER_SIZE + PolicyFormat.SELECTOR_SIZE + PolicyFormat.DESC_LENGTH_SIZE + descBytes.length + PolicyFormat.GROUP_COUNT_SIZE;
|
|
1373
|
-
for (const g of
|
|
1447
|
+
for (const g of encodedGroups) totalSize += PolicyFormat.GROUP_HEADER_SIZE + g.wireBytes.length;
|
|
1374
1448
|
const out = new Uint8Array(totalSize);
|
|
1375
1449
|
let offset = 0;
|
|
1376
1450
|
out[offset++] = headerByte;
|
|
@@ -1380,32 +1454,25 @@ function encode(data) {
|
|
|
1380
1454
|
offset += PolicyFormat.DESC_LENGTH_SIZE;
|
|
1381
1455
|
out.set(descBytes, offset);
|
|
1382
1456
|
offset += descBytes.length;
|
|
1383
|
-
out[offset++] =
|
|
1384
|
-
for (const g of
|
|
1457
|
+
out[offset++] = encodedGroups.length;
|
|
1458
|
+
for (const g of encodedGroups) {
|
|
1385
1459
|
writeBE16(out, offset, g.ruleCount);
|
|
1386
1460
|
offset += PolicyFormat.GROUP_RULECOUNT_SIZE;
|
|
1387
|
-
out
|
|
1388
|
-
|
|
1389
|
-
out[offset++] = g.wireBytes.length >>> 8 & 255;
|
|
1390
|
-
out[offset++] = g.wireBytes.length & 255;
|
|
1461
|
+
writeBE32(out, offset, g.wireBytes.length);
|
|
1462
|
+
offset += PolicyFormat.GROUP_SIZE_SIZE;
|
|
1391
1463
|
out.set(g.wireBytes, offset);
|
|
1392
1464
|
offset += g.wireBytes.length;
|
|
1393
1465
|
}
|
|
1394
1466
|
return bytesToHex(out);
|
|
1395
1467
|
}
|
|
1396
|
-
/**
|
|
1397
|
-
* Decode a binary policy blob into a PolicyData structure.
|
|
1398
|
-
* @param blob - Binary policy as 0x-prefixed hex string.
|
|
1399
|
-
* @returns The decoded policy data with constraints grouped by scope and path.
|
|
1400
|
-
* @throws {CallciumError} If the blob is structurally malformed.
|
|
1401
|
-
*/
|
|
1402
1468
|
function decode(blob) {
|
|
1403
1469
|
const { policy } = decodePolicy(blob);
|
|
1404
1470
|
const groups = policy.groups.map((group) => {
|
|
1405
1471
|
const constraintMap = /* @__PURE__ */ new Map();
|
|
1406
1472
|
const constraintOrder = [];
|
|
1407
1473
|
for (const rule of group.rules) {
|
|
1408
|
-
const
|
|
1474
|
+
const hintHex = rule.hint?.value;
|
|
1475
|
+
const key = `${rule.scope.value}:${rule.path.value}:${hintHex ?? ""}`;
|
|
1409
1476
|
const opHex = buildOperatorHex(rule);
|
|
1410
1477
|
const existing = constraintMap.get(key);
|
|
1411
1478
|
if (existing !== void 0) existing.operators.push(opHex);
|
|
@@ -1414,6 +1481,7 @@ function decode(blob) {
|
|
|
1414
1481
|
scope: rule.scope.value,
|
|
1415
1482
|
path: rule.path.value,
|
|
1416
1483
|
operators: [opHex],
|
|
1484
|
+
...hintHex !== void 0 && { hint: hintHex },
|
|
1417
1485
|
span: rule.span
|
|
1418
1486
|
};
|
|
1419
1487
|
constraintMap.set(key, constraint);
|
|
@@ -1426,180 +1494,21 @@ function decode(blob) {
|
|
|
1426
1494
|
isSelectorless: policy.isSelectorless,
|
|
1427
1495
|
selector: policy.selector.value,
|
|
1428
1496
|
descriptor: policy.descriptor.raw,
|
|
1429
|
-
groups,
|
|
1430
|
-
span: policy.span
|
|
1431
|
-
};
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
* @param blob - Binary policy as 0x-prefixed hex string.
|
|
1436
|
-
* @returns The inspected policy with per-field spans for every structural element.
|
|
1437
|
-
* @throws {CallciumError} If the blob is structurally malformed.
|
|
1438
|
-
*/
|
|
1439
|
-
function inspect(blob) {
|
|
1440
|
-
return decodePolicy(blob).policy;
|
|
1441
|
-
}
|
|
1442
|
-
/** Encode, decode, and inspect policies in the canonical binary format. */
|
|
1443
|
-
const PolicyCoder = {
|
|
1444
|
-
encode,
|
|
1445
|
-
decode,
|
|
1446
|
-
inspect
|
|
1447
|
-
};
|
|
1448
|
-
//#endregion
|
|
1449
|
-
//#region src/operators.ts
|
|
1450
|
-
/** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
|
|
1451
|
-
function toBigInt(bytes, offset = 0) {
|
|
1452
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
|
|
1453
|
-
return view.getBigUint64(0, false) << 192n | view.getBigUint64(8, false) << 128n | view.getBigUint64(16, false) << 64n | view.getBigUint64(24, false);
|
|
1454
|
-
}
|
|
1455
|
-
/** Return true when the type code represents a signed integer (int8 through int256). */
|
|
1456
|
-
function isSigned(typeCode) {
|
|
1457
|
-
return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
|
|
1458
|
-
}
|
|
1459
|
-
/**
|
|
1460
|
-
* Return true when the type code represents a left-aligned type (fixed bytes or function),
|
|
1461
|
-
* whose value occupies the high bytes of the word with zero padding below.
|
|
1462
|
-
*/
|
|
1463
|
-
function isLeftAligned(typeCode) {
|
|
1464
|
-
return typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX || typeCode === TypeCode.FUNCTION;
|
|
1465
|
-
}
|
|
1466
|
-
/**
|
|
1467
|
-
* Canonicalize a raw 256-bit calldata word to its ABI value for the declared type (Policy Spec §7.8).
|
|
1468
|
-
* A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
|
|
1469
|
-
* unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
|
|
1470
|
-
* comparison use the canonical ABI value rather than the raw bytes.
|
|
1471
|
-
* @param value - The raw 256-bit word loaded from calldata.
|
|
1472
|
-
* @param typeCode - The declared type code of the value.
|
|
1473
|
-
* @returns The canonicalized 256-bit value.
|
|
1474
|
-
*/
|
|
1475
|
-
function canonicalize(value, typeCode) {
|
|
1476
|
-
if (typeCode <= TypeCode.UINT_MAX) {
|
|
1477
|
-
const bits = BigInt((typeCode + 1) * 8);
|
|
1478
|
-
return bits === 256n ? value : value & (1n << bits) - 1n;
|
|
1479
|
-
}
|
|
1480
|
-
if (typeCode <= TypeCode.INT_MAX) {
|
|
1481
|
-
const bits = (typeCode - TypeCode.INT_MIN + 1) * 8;
|
|
1482
|
-
return bits === 256 ? value : BigInt.asUintN(256, BigInt.asIntN(bits, value));
|
|
1483
|
-
}
|
|
1484
|
-
if (typeCode === TypeCode.ADDRESS) return value & (1n << 160n) - 1n;
|
|
1485
|
-
if (typeCode === TypeCode.BOOL) return value & 1n;
|
|
1486
|
-
if (typeCode === TypeCode.FUNCTION) return value >> 64n << 64n;
|
|
1487
|
-
if (typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX) {
|
|
1488
|
-
const n = typeCode - TypeCode.FIXED_BYTES_MIN + 1;
|
|
1489
|
-
if (n === 32) return value;
|
|
1490
|
-
const padBits = BigInt((32 - n) * 8);
|
|
1491
|
-
return value >> padBits << padBits;
|
|
1492
|
-
}
|
|
1493
|
-
return value;
|
|
1494
|
-
}
|
|
1495
|
-
/** Return true when the operator code (with or without NOT flag) is a LENGTH_* variant. */
|
|
1496
|
-
function isLengthOp(opCode) {
|
|
1497
|
-
const base = opCode & ~Op.NOT;
|
|
1498
|
-
return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
|
|
1499
|
-
}
|
|
1500
|
-
/** Map a LENGTH_* opcode base to its core value-comparison twin. */
|
|
1501
|
-
function lengthToValueOp(base) {
|
|
1502
|
-
if (base === Op.LENGTH_EQ) return Op.EQ;
|
|
1503
|
-
if (base === Op.LENGTH_GT) return Op.GT;
|
|
1504
|
-
if (base === Op.LENGTH_LT) return Op.LT;
|
|
1505
|
-
if (base === Op.LENGTH_GTE) return Op.GTE;
|
|
1506
|
-
if (base === Op.LENGTH_LTE) return Op.LTE;
|
|
1507
|
-
return Op.BETWEEN;
|
|
1508
|
-
}
|
|
1509
|
-
/** Return true when LENGTH_* operators are valid for the given type code (bytes, string, or dynamic array). */
|
|
1510
|
-
function isLengthValidType(typeCode) {
|
|
1511
|
-
return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
|
|
1512
|
-
}
|
|
1513
|
-
/** Compare two 256-bit values, using two's complement interpretation for signed type codes. */
|
|
1514
|
-
function compareTyped(a, b, typeCode) {
|
|
1515
|
-
if (isSigned(typeCode)) {
|
|
1516
|
-
const sa = BigInt.asIntN(256, a);
|
|
1517
|
-
const sb = BigInt.asIntN(256, b);
|
|
1518
|
-
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
1519
|
-
}
|
|
1520
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
1521
|
-
}
|
|
1522
|
-
/** Binary search over a sorted array of 32-byte words. Comparison is always unsigned. */
|
|
1523
|
-
function isIn(value, data) {
|
|
1524
|
-
const count = data.length / 32;
|
|
1525
|
-
let lo = 0;
|
|
1526
|
-
let hi = count - 1;
|
|
1527
|
-
while (lo <= hi) {
|
|
1528
|
-
const mid = lo + hi >>> 1;
|
|
1529
|
-
const elem = toBigInt(data, mid * 32);
|
|
1530
|
-
if (value === elem) return true;
|
|
1531
|
-
if (value < elem) hi = mid - 1;
|
|
1532
|
-
else lo = mid + 1;
|
|
1533
|
-
}
|
|
1534
|
-
return false;
|
|
1535
|
-
}
|
|
1536
|
-
/**
|
|
1537
|
-
* Apply a single operator against a value and operand data.
|
|
1538
|
-
* Handles the NOT flag internally by inverting the base operator result.
|
|
1539
|
-
* @param opCode - Operator code, potentially OR'd with the NOT flag.
|
|
1540
|
-
* @param value - The 256-bit value to test (ignored for LENGTH_* operators).
|
|
1541
|
-
* @param valueLength - Runtime byte or element count, only meaningful for LENGTH_* operators.
|
|
1542
|
-
* @param operandData - Raw operand bytes (32 per operand, 64 for range operators).
|
|
1543
|
-
* @param typeCode - ABI type code, used to select signed vs. unsigned comparison.
|
|
1544
|
-
* @returns Whether the value satisfies the operator.
|
|
1545
|
-
*/
|
|
1546
|
-
function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
1547
|
-
const negate = (opCode & Op.NOT) !== 0;
|
|
1548
|
-
let base = opCode & ~Op.NOT;
|
|
1549
|
-
if (isLengthOp(base)) {
|
|
1550
|
-
value = BigInt(valueLength);
|
|
1551
|
-
typeCode = TypeCode.UINT_MAX;
|
|
1552
|
-
base = lengthToValueOp(base);
|
|
1553
|
-
}
|
|
1554
|
-
let result;
|
|
1555
|
-
switch (base) {
|
|
1556
|
-
case Op.EQ:
|
|
1557
|
-
result = value === toBigInt(operandData, 0);
|
|
1558
|
-
break;
|
|
1559
|
-
case Op.GT:
|
|
1560
|
-
result = compareTyped(value, toBigInt(operandData, 0), typeCode) > 0;
|
|
1561
|
-
break;
|
|
1562
|
-
case Op.LT:
|
|
1563
|
-
result = compareTyped(value, toBigInt(operandData, 0), typeCode) < 0;
|
|
1564
|
-
break;
|
|
1565
|
-
case Op.GTE:
|
|
1566
|
-
result = compareTyped(value, toBigInt(operandData, 0), typeCode) >= 0;
|
|
1567
|
-
break;
|
|
1568
|
-
case Op.LTE:
|
|
1569
|
-
result = compareTyped(value, toBigInt(operandData, 0), typeCode) <= 0;
|
|
1570
|
-
break;
|
|
1571
|
-
case Op.BETWEEN: {
|
|
1572
|
-
const min = toBigInt(operandData, 0);
|
|
1573
|
-
const max = toBigInt(operandData, 32);
|
|
1574
|
-
result = compareTyped(value, min, typeCode) >= 0 && compareTyped(value, max, typeCode) <= 0;
|
|
1575
|
-
break;
|
|
1576
|
-
}
|
|
1577
|
-
case Op.IN:
|
|
1578
|
-
result = isIn(value, operandData);
|
|
1579
|
-
break;
|
|
1580
|
-
case Op.BITMASK_ALL: {
|
|
1581
|
-
const mask = toBigInt(operandData, 0);
|
|
1582
|
-
result = (value & mask) === mask;
|
|
1583
|
-
break;
|
|
1584
|
-
}
|
|
1585
|
-
case Op.BITMASK_ANY: {
|
|
1586
|
-
const mask = toBigInt(operandData, 0);
|
|
1587
|
-
result = (value & mask) !== 0n;
|
|
1588
|
-
break;
|
|
1589
|
-
}
|
|
1590
|
-
case Op.BITMASK_NONE: {
|
|
1591
|
-
const mask = toBigInt(operandData, 0);
|
|
1592
|
-
result = (value & mask) === 0n;
|
|
1593
|
-
break;
|
|
1594
|
-
}
|
|
1595
|
-
default: throw new CallciumError("INVALID_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
|
|
1596
|
-
}
|
|
1597
|
-
return negate ? !result : result;
|
|
1497
|
+
groups,
|
|
1498
|
+
span: policy.span
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
function inspect(blob) {
|
|
1502
|
+
return decodePolicy(blob).policy;
|
|
1598
1503
|
}
|
|
1504
|
+
const PolicyCoder = {
|
|
1505
|
+
encode,
|
|
1506
|
+
decode,
|
|
1507
|
+
inspect
|
|
1508
|
+
};
|
|
1599
1509
|
//#endregion
|
|
1600
1510
|
//#region src/validation-issue.ts
|
|
1601
1511
|
const ZERO = `0x${"0".repeat(64)}`;
|
|
1602
|
-
/** Create an issue from operator compatibility results. */
|
|
1603
1512
|
function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
|
|
1604
1513
|
return {
|
|
1605
1514
|
severity: "error",
|
|
@@ -1612,7 +1521,42 @@ function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
|
|
|
1612
1521
|
message
|
|
1613
1522
|
};
|
|
1614
1523
|
}
|
|
1615
|
-
|
|
1524
|
+
function unnavigablePath(groupIndex, constraintIndex) {
|
|
1525
|
+
return {
|
|
1526
|
+
severity: "error",
|
|
1527
|
+
category: "typeMismatch",
|
|
1528
|
+
groupIndex,
|
|
1529
|
+
constraintIndex,
|
|
1530
|
+
code: "UNNAVIGABLE_PATH",
|
|
1531
|
+
value1: ZERO,
|
|
1532
|
+
value2: ZERO,
|
|
1533
|
+
message: "Path does not navigate the descriptor"
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
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
|
+
}
|
|
1616
1560
|
function nonCanonicalOperand(groupIndex, constraintIndex, operand, canonical) {
|
|
1617
1561
|
return {
|
|
1618
1562
|
severity: "error",
|
|
@@ -1625,7 +1569,6 @@ function nonCanonicalOperand(groupIndex, constraintIndex, operand, canonical) {
|
|
|
1625
1569
|
message: "Operand is not the canonical encoding for the declared type"
|
|
1626
1570
|
};
|
|
1627
1571
|
}
|
|
1628
|
-
/** eq(v)/neq(v) or lengthEq(v)/lengthNeq(v) on the same path. */
|
|
1629
1572
|
function eqNeqContradiction(isLength, groupIndex, constraintIndex, value) {
|
|
1630
1573
|
return {
|
|
1631
1574
|
severity: "error",
|
|
@@ -1638,7 +1581,6 @@ function eqNeqContradiction(isLength, groupIndex, constraintIndex, value) {
|
|
|
1638
1581
|
message: isLength ? "lengthEq(v) and lengthNeq(v) on same path" : "eq(v) and neq(v) on same path"
|
|
1639
1582
|
};
|
|
1640
1583
|
}
|
|
1641
|
-
/** Multiple eq()/lengthEq() operators with different values. */
|
|
1642
1584
|
function conflictingEquality(isLength, groupIndex, constraintIndex, existing, newValue) {
|
|
1643
1585
|
return {
|
|
1644
1586
|
severity: "error",
|
|
@@ -1651,7 +1593,6 @@ function conflictingEquality(isLength, groupIndex, constraintIndex, existing, ne
|
|
|
1651
1593
|
message: isLength ? "Multiple lengthEq() operators with different values" : "Multiple eq() operators with different values"
|
|
1652
1594
|
};
|
|
1653
1595
|
}
|
|
1654
|
-
/** Value/length is outside the physical range of its type. */
|
|
1655
1596
|
function outOfPhysicalBounds(isLength, groupIndex, constraintIndex, value) {
|
|
1656
1597
|
return {
|
|
1657
1598
|
severity: "error",
|
|
@@ -1664,7 +1605,6 @@ function outOfPhysicalBounds(isLength, groupIndex, constraintIndex, value) {
|
|
|
1664
1605
|
message: isLength ? "Length value is outside the physical range" : "Value is outside the physical range of the type"
|
|
1665
1606
|
};
|
|
1666
1607
|
}
|
|
1667
|
-
/** gt()/lengthGt() on the maximum is impossible. */
|
|
1668
1608
|
function impossibleGt(isLength, groupIndex, constraintIndex, value) {
|
|
1669
1609
|
return {
|
|
1670
1610
|
severity: "error",
|
|
@@ -1677,7 +1617,6 @@ function impossibleGt(isLength, groupIndex, constraintIndex, value) {
|
|
|
1677
1617
|
message: isLength ? "lengthGt() on maximum length is impossible" : "gt() on type maximum is impossible"
|
|
1678
1618
|
};
|
|
1679
1619
|
}
|
|
1680
|
-
/** lt()/lengthLt() on the minimum is impossible. */
|
|
1681
1620
|
function impossibleLt(isLength, groupIndex, constraintIndex, value) {
|
|
1682
1621
|
return {
|
|
1683
1622
|
severity: "error",
|
|
@@ -1690,7 +1629,6 @@ function impossibleLt(isLength, groupIndex, constraintIndex, value) {
|
|
|
1690
1629
|
message: isLength ? "lengthLt() on minimum length is impossible" : "lt() on type minimum is impossible"
|
|
1691
1630
|
};
|
|
1692
1631
|
}
|
|
1693
|
-
/** eq()/lengthEq() value is excluded by a bound. */
|
|
1694
1632
|
function boundsExcludeEquality(isLength, groupIndex, constraintIndex, eq, bound) {
|
|
1695
1633
|
return {
|
|
1696
1634
|
severity: "error",
|
|
@@ -1703,7 +1641,6 @@ function boundsExcludeEquality(isLength, groupIndex, constraintIndex, eq, bound)
|
|
|
1703
1641
|
message: isLength ? "lengthEq() value is excluded by bound" : "eq() value is excluded by bound"
|
|
1704
1642
|
};
|
|
1705
1643
|
}
|
|
1706
|
-
/** Lower bound/length exceeds the upper bound. */
|
|
1707
1644
|
function impossibleRange(isLength, groupIndex, constraintIndex, lower, upper) {
|
|
1708
1645
|
return {
|
|
1709
1646
|
severity: "error",
|
|
@@ -1716,7 +1653,6 @@ function impossibleRange(isLength, groupIndex, constraintIndex, lower, upper) {
|
|
|
1716
1653
|
message: isLength ? "Lower length bound exceeds upper bound" : "Lower bound exceeds upper bound"
|
|
1717
1654
|
};
|
|
1718
1655
|
}
|
|
1719
|
-
/** Set operation excludes existing eq() value. */
|
|
1720
1656
|
function setExcludesEquality(groupIndex, constraintIndex, value) {
|
|
1721
1657
|
return {
|
|
1722
1658
|
severity: "error",
|
|
@@ -1729,7 +1665,6 @@ function setExcludesEquality(groupIndex, constraintIndex, value) {
|
|
|
1729
1665
|
message: "Set operation excludes existing eq() value"
|
|
1730
1666
|
};
|
|
1731
1667
|
}
|
|
1732
|
-
/** Multiple isIn() sets have no intersection. */
|
|
1733
1668
|
function emptySetIntersection(groupIndex, constraintIndex) {
|
|
1734
1669
|
return {
|
|
1735
1670
|
severity: "error",
|
|
@@ -1742,7 +1677,6 @@ function emptySetIntersection(groupIndex, constraintIndex) {
|
|
|
1742
1677
|
message: "Multiple isIn() sets have no intersection"
|
|
1743
1678
|
};
|
|
1744
1679
|
}
|
|
1745
|
-
/** All values in isIn() set are excluded by neq/notIn. */
|
|
1746
1680
|
function setFullyExcluded(groupIndex, constraintIndex) {
|
|
1747
1681
|
return {
|
|
1748
1682
|
severity: "error",
|
|
@@ -1755,7 +1689,6 @@ function setFullyExcluded(groupIndex, constraintIndex) {
|
|
|
1755
1689
|
message: "All values in isIn() set are excluded by neq/notIn"
|
|
1756
1690
|
};
|
|
1757
1691
|
}
|
|
1758
|
-
/** Bitmask operators conflict. */
|
|
1759
1692
|
function bitmaskContradiction(groupIndex, constraintIndex, mask, conflicting) {
|
|
1760
1693
|
return {
|
|
1761
1694
|
severity: "error",
|
|
@@ -1768,7 +1701,6 @@ function bitmaskContradiction(groupIndex, constraintIndex, mask, conflicting) {
|
|
|
1768
1701
|
message: "Bitmask operators conflict"
|
|
1769
1702
|
};
|
|
1770
1703
|
}
|
|
1771
|
-
/** bitmaskAny is impossible because all bits are forbidden. */
|
|
1772
1704
|
function bitmaskAnyImpossible(groupIndex, constraintIndex, mask, mustBeZero) {
|
|
1773
1705
|
return {
|
|
1774
1706
|
severity: "error",
|
|
@@ -1781,7 +1713,6 @@ function bitmaskAnyImpossible(groupIndex, constraintIndex, mask, mustBeZero) {
|
|
|
1781
1713
|
message: "bitmaskAny is impossible because all bits are forbidden"
|
|
1782
1714
|
};
|
|
1783
1715
|
}
|
|
1784
|
-
/** isIn/notIn set is not strictly sorted and deduplicated. */
|
|
1785
1716
|
function unsortedInSet(groupIndex, constraintIndex) {
|
|
1786
1717
|
return {
|
|
1787
1718
|
severity: "error",
|
|
@@ -1794,7 +1725,6 @@ function unsortedInSet(groupIndex, constraintIndex) {
|
|
|
1794
1725
|
message: "isIn/notIn set is not strictly sorted and deduplicated."
|
|
1795
1726
|
};
|
|
1796
1727
|
}
|
|
1797
|
-
/** Group contains zero constraints. */
|
|
1798
1728
|
function emptyGroup(groupIndex) {
|
|
1799
1729
|
return {
|
|
1800
1730
|
severity: "error",
|
|
@@ -1807,7 +1737,6 @@ function emptyGroup(groupIndex) {
|
|
|
1807
1737
|
message: "Group contains zero constraints."
|
|
1808
1738
|
};
|
|
1809
1739
|
}
|
|
1810
|
-
/** Numeric/length bound dominated by a stricter bound. */
|
|
1811
1740
|
function dominatedBound(isLength, groupIndex, constraintIndex, value) {
|
|
1812
1741
|
return {
|
|
1813
1742
|
severity: "warning",
|
|
@@ -1820,7 +1749,6 @@ function dominatedBound(isLength, groupIndex, constraintIndex, value) {
|
|
|
1820
1749
|
message: isLength ? "Length bound is dominated by a stricter bound" : "Numeric bound is dominated by a stricter bound"
|
|
1821
1750
|
};
|
|
1822
1751
|
}
|
|
1823
|
-
/** Bound redundant because eq()/lengthEq() is set. */
|
|
1824
1752
|
function redundantBound(isLength, groupIndex, constraintIndex, bound, eq) {
|
|
1825
1753
|
return {
|
|
1826
1754
|
severity: "warning",
|
|
@@ -1833,7 +1761,6 @@ function redundantBound(isLength, groupIndex, constraintIndex, bound, eq) {
|
|
|
1833
1761
|
message: isLength ? "Length bound is redundant because lengthEq() is set" : "Bound is redundant because eq() is set"
|
|
1834
1762
|
};
|
|
1835
1763
|
}
|
|
1836
|
-
/** notIn() value was present in isIn() set. */
|
|
1837
1764
|
function setReduction(groupIndex, constraintIndex, value) {
|
|
1838
1765
|
return {
|
|
1839
1766
|
severity: "warning",
|
|
@@ -1846,7 +1773,6 @@ function setReduction(groupIndex, constraintIndex, value) {
|
|
|
1846
1773
|
message: "notIn() value was present in isIn() set"
|
|
1847
1774
|
};
|
|
1848
1775
|
}
|
|
1849
|
-
/** isIn() sets partially overlap. */
|
|
1850
1776
|
function setRedundancy(groupIndex, constraintIndex, intersectionCount) {
|
|
1851
1777
|
return {
|
|
1852
1778
|
severity: "warning",
|
|
@@ -1859,7 +1785,6 @@ function setRedundancy(groupIndex, constraintIndex, intersectionCount) {
|
|
|
1859
1785
|
message: "isIn() sets partially overlap"
|
|
1860
1786
|
};
|
|
1861
1787
|
}
|
|
1862
|
-
/** Some values in isIn() set are excluded by neq/notIn. */
|
|
1863
1788
|
function setPartiallyExcluded(groupIndex, constraintIndex, excludedCount) {
|
|
1864
1789
|
return {
|
|
1865
1790
|
severity: "warning",
|
|
@@ -1872,7 +1797,6 @@ function setPartiallyExcluded(groupIndex, constraintIndex, excludedCount) {
|
|
|
1872
1797
|
message: "Some values in isIn() set are excluded by neq/notIn"
|
|
1873
1798
|
};
|
|
1874
1799
|
}
|
|
1875
|
-
/** Bitmask operation is redundant. */
|
|
1876
1800
|
function redundantBitmask(groupIndex, constraintIndex, mask, existing) {
|
|
1877
1801
|
return {
|
|
1878
1802
|
severity: "warning",
|
|
@@ -1885,7 +1809,6 @@ function redundantBitmask(groupIndex, constraintIndex, mask, existing) {
|
|
|
1885
1809
|
message: "Bitmask operation is redundant"
|
|
1886
1810
|
};
|
|
1887
1811
|
}
|
|
1888
|
-
/** gte/lte pair fusible into a single range operator. */
|
|
1889
1812
|
function fusibleRange(isLength, groupIndex, constraintIndex, low, high) {
|
|
1890
1813
|
return {
|
|
1891
1814
|
severity: "warning",
|
|
@@ -1898,7 +1821,6 @@ function fusibleRange(isLength, groupIndex, constraintIndex, low, high) {
|
|
|
1898
1821
|
message: isLength ? "lengthGte() and lengthLte() fuse into a single lengthBetween()" : "gte() and lte() fuse into a single between()"
|
|
1899
1822
|
};
|
|
1900
1823
|
}
|
|
1901
|
-
/** Duplicate operator in constraint. */
|
|
1902
1824
|
function duplicateConstraint(groupIndex, constraintIndex) {
|
|
1903
1825
|
return {
|
|
1904
1826
|
severity: "warning",
|
|
@@ -1911,7 +1833,6 @@ function duplicateConstraint(groupIndex, constraintIndex) {
|
|
|
1911
1833
|
message: "Duplicate operator in constraint"
|
|
1912
1834
|
};
|
|
1913
1835
|
}
|
|
1914
|
-
/** gte(min)/lengthGte(0) is always true. */
|
|
1915
1836
|
function vacuousGte(isLength, groupIndex, constraintIndex, value) {
|
|
1916
1837
|
return {
|
|
1917
1838
|
severity: "info",
|
|
@@ -1924,7 +1845,6 @@ function vacuousGte(isLength, groupIndex, constraintIndex, value) {
|
|
|
1924
1845
|
message: isLength ? "lengthGte(0) is always true" : "gte() bound equals type minimum (always true)"
|
|
1925
1846
|
};
|
|
1926
1847
|
}
|
|
1927
|
-
/** lte(max)/lengthLte(max) is always true. */
|
|
1928
1848
|
function vacuousLte(isLength, groupIndex, constraintIndex, value) {
|
|
1929
1849
|
return {
|
|
1930
1850
|
severity: "info",
|
|
@@ -1937,7 +1857,18 @@ function vacuousLte(isLength, groupIndex, constraintIndex, value) {
|
|
|
1937
1857
|
message: isLength ? "lengthLte() bound equals maximum (always true)" : "lte() bound equals type maximum (always true)"
|
|
1938
1858
|
};
|
|
1939
1859
|
}
|
|
1940
|
-
|
|
1860
|
+
function vacuousNegatedRange(isLength, groupIndex, constraintIndex, low, high) {
|
|
1861
|
+
return {
|
|
1862
|
+
severity: "info",
|
|
1863
|
+
category: "vacuity",
|
|
1864
|
+
groupIndex,
|
|
1865
|
+
constraintIndex,
|
|
1866
|
+
code: isLength ? "VACUOUS_NEGATED_LENGTH_RANGE" : "VACUOUS_NEGATED_RANGE",
|
|
1867
|
+
value1: low,
|
|
1868
|
+
value2: high,
|
|
1869
|
+
message: isLength ? "Negated lengthBetween() with low > high excludes nothing (always true)" : "Negated between() with low > high excludes nothing (always true)"
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1941
1872
|
function pathDepthExceeded(groupIndex, constraintIndex, depth, maxDepth) {
|
|
1942
1873
|
return {
|
|
1943
1874
|
severity: "warning",
|
|
@@ -1947,23 +1878,21 @@ function pathDepthExceeded(groupIndex, constraintIndex, depth, maxDepth) {
|
|
|
1947
1878
|
code: "PATH_DEPTH_EXCEEDED",
|
|
1948
1879
|
value1: depth,
|
|
1949
1880
|
value2: maxDepth,
|
|
1950
|
-
message: "Path depth exceeds the
|
|
1881
|
+
message: "Path depth exceeds the maximum a well-formed policy may declare"
|
|
1951
1882
|
};
|
|
1952
1883
|
}
|
|
1953
|
-
/** Quantifier over a static array beyond the reference enforcer's iteration cap. */
|
|
1954
1884
|
function quantifierOverStaticLimit(groupIndex, constraintIndex, arrayLength, maxLength) {
|
|
1955
1885
|
return {
|
|
1956
|
-
severity: "
|
|
1886
|
+
severity: "error",
|
|
1957
1887
|
category: "compatibility",
|
|
1958
1888
|
groupIndex,
|
|
1959
1889
|
constraintIndex,
|
|
1960
1890
|
code: "QUANTIFIER_OVER_STATIC_LIMIT",
|
|
1961
1891
|
value1: arrayLength,
|
|
1962
1892
|
value2: maxLength,
|
|
1963
|
-
message: "Quantifier over static array exceeds the
|
|
1893
|
+
message: "Quantifier over static array exceeds the maximum quantified length"
|
|
1964
1894
|
};
|
|
1965
1895
|
}
|
|
1966
|
-
/** Context property ID outside the assigned set. */
|
|
1967
1896
|
function unknownContextProperty(groupIndex, constraintIndex, contextId, maxContextId) {
|
|
1968
1897
|
return {
|
|
1969
1898
|
severity: "warning",
|
|
@@ -1976,7 +1905,6 @@ function unknownContextProperty(groupIndex, constraintIndex, contextId, maxConte
|
|
|
1976
1905
|
message: "Unknown context property ID"
|
|
1977
1906
|
};
|
|
1978
1907
|
}
|
|
1979
|
-
/** Negated operator under the existential quantifier. */
|
|
1980
1908
|
function negationUnderAny(groupIndex, constraintIndex, opCode) {
|
|
1981
1909
|
return {
|
|
1982
1910
|
severity: "warning",
|
|
@@ -1991,41 +1919,29 @@ function negationUnderAny(groupIndex, constraintIndex, opCode) {
|
|
|
1991
1919
|
}
|
|
1992
1920
|
//#endregion
|
|
1993
1921
|
//#region src/policy-validator.ts
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
*
|
|
2002
|
-
* Both caps are protocol-defined limits. Beyond these limits,
|
|
2003
|
-
* additional exclusions are silently ignored — the validator becomes
|
|
2004
|
-
* best-effort for set-reduction diagnostics but never produces false
|
|
2005
|
-
* positives, only potential false negatives (missed redundancy warnings).
|
|
2006
|
-
*/
|
|
2007
|
-
const MAX_HOLES = 8;
|
|
2008
|
-
const MAX_NOT_IN = 8;
|
|
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
|
+
]);
|
|
2009
1929
|
const LENGTH_MAX = 4294967295n;
|
|
2010
1930
|
const UINT256_MAX = (1n << 256n) - 1n;
|
|
2011
1931
|
const INT256_SIGN_BIT = 1n << 255n;
|
|
2012
|
-
/** Signed-aware difference of two raw uint256 values: negative if a<b, zero if equal, positive if a>b. */
|
|
2013
1932
|
function signedCompare(a, b, signed) {
|
|
2014
1933
|
if (!signed) return a - b;
|
|
2015
1934
|
const toS = (v) => v >= INT256_SIGN_BIT ? v - (1n << 256n) : v;
|
|
2016
1935
|
return toS(a) - toS(b);
|
|
2017
1936
|
}
|
|
2018
|
-
/** True for uint and int type codes. */
|
|
2019
1937
|
function isNumericType(typeCode) {
|
|
2020
|
-
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;
|
|
2021
1939
|
}
|
|
2022
|
-
/** True for uint and bytes32 type codes. */
|
|
2023
1940
|
function isBitmaskCompatible(typeCode) {
|
|
2024
|
-
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;
|
|
2025
1942
|
}
|
|
2026
|
-
/** Physical domain limits for a type code (returned as raw uint256 bigints). */
|
|
2027
1943
|
function getDomainLimits(typeCode) {
|
|
2028
|
-
if (typeCode <= TypeCode.UINT_MAX) {
|
|
1944
|
+
if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
|
|
2029
1945
|
const bits = BigInt(typeCode - TypeCode.UINT_MIN + 1) * 8n;
|
|
2030
1946
|
return {
|
|
2031
1947
|
min: 0n,
|
|
@@ -2058,19 +1974,15 @@ function getDomainLimits(typeCode) {
|
|
|
2058
1974
|
max: UINT256_MAX
|
|
2059
1975
|
};
|
|
2060
1976
|
}
|
|
2061
|
-
/** True for EQ, GT, LT, GTE, LTE, BETWEEN, IN, BITMASK_*. */
|
|
2062
1977
|
function isValueOp(opBase) {
|
|
2063
1978
|
return opBase <= Op.BITMASK_NONE;
|
|
2064
1979
|
}
|
|
2065
|
-
/** True for GT, LT, GTE, LTE, BETWEEN. */
|
|
2066
1980
|
function isComparisonOp(opBase) {
|
|
2067
1981
|
return opBase >= Op.GT && opBase <= Op.BETWEEN;
|
|
2068
1982
|
}
|
|
2069
|
-
/** True for BITMASK_ALL, BITMASK_ANY, BITMASK_NONE. */
|
|
2070
1983
|
function isBitmaskOp(opBase) {
|
|
2071
1984
|
return opBase >= Op.BITMASK_ALL && opBase <= Op.BITMASK_NONE;
|
|
2072
1985
|
}
|
|
2073
|
-
/** Return the incompatibility reason, or null if the operator is allowed. */
|
|
2074
1986
|
function getIncompat(opBase, typeInfo) {
|
|
2075
1987
|
const { typeCode, isDynamic, staticSize } = typeInfo;
|
|
2076
1988
|
if (isValueOp(opBase)) {
|
|
@@ -2108,18 +2020,12 @@ function getIncompat(opBase, typeInfo) {
|
|
|
2108
2020
|
message: "Unknown operator code"
|
|
2109
2021
|
};
|
|
2110
2022
|
}
|
|
2111
|
-
/**
|
|
2112
|
-
* True if operator `opCode` is allowed on a target with the given type info.
|
|
2113
|
-
* Tolerates the NOT flag on `opCode`.
|
|
2114
|
-
*/
|
|
2115
2023
|
function isOpAllowed(opCode, typeInfo) {
|
|
2116
2024
|
return getIncompat(opCode & ~Op.NOT, typeInfo) === null;
|
|
2117
2025
|
}
|
|
2118
|
-
/** Read a single uint256 from operator hex (bytes after the opcode byte). */
|
|
2119
2026
|
function readValue(opHex) {
|
|
2120
2027
|
return BigInt(`0x${opHex.slice(4)}`);
|
|
2121
2028
|
}
|
|
2122
|
-
/** First payload word deviating from the canonical encoding for the type, with its canonical form, or null. */
|
|
2123
2029
|
function findNonCanonicalWord(opHex, typeCode) {
|
|
2124
2030
|
const body = opHex.slice(4);
|
|
2125
2031
|
for (let i = 0; i + 64 <= body.length; i += 64) {
|
|
@@ -2132,7 +2038,6 @@ function findNonCanonicalWord(opHex, typeCode) {
|
|
|
2132
2038
|
}
|
|
2133
2039
|
return null;
|
|
2134
2040
|
}
|
|
2135
|
-
/** Read a pair of uint256 values from a BETWEEN operator. */
|
|
2136
2041
|
function readPair(opHex) {
|
|
2137
2042
|
const body = opHex.slice(4);
|
|
2138
2043
|
return {
|
|
@@ -2140,7 +2045,6 @@ function readPair(opHex) {
|
|
|
2140
2045
|
high: BigInt(`0x${body.slice(64, 128)}`)
|
|
2141
2046
|
};
|
|
2142
2047
|
}
|
|
2143
|
-
/** Unpack an IN operator's payload into an array of bigint values. */
|
|
2144
2048
|
function unpackSet(opHex) {
|
|
2145
2049
|
const body = opHex.slice(4);
|
|
2146
2050
|
const count = body.length / 64;
|
|
@@ -2148,12 +2052,10 @@ function unpackSet(opHex) {
|
|
|
2148
2052
|
for (let i = 0; i < count; i++) values.push(BigInt(`0x${body.slice(i * 64, (i + 1) * 64)}`));
|
|
2149
2053
|
return values;
|
|
2150
2054
|
}
|
|
2151
|
-
/** True if every element is strictly greater than the previous. */
|
|
2152
2055
|
function isStrictlyAscending(values) {
|
|
2153
2056
|
for (let i = 1; i < values.length; i++) if (values[i] <= values[i - 1]) return false;
|
|
2154
2057
|
return true;
|
|
2155
2058
|
}
|
|
2156
|
-
/** Convert LENGTH_* opcode to its core comparison equivalent. */
|
|
2157
2059
|
function normalizeLengthOp(base) {
|
|
2158
2060
|
if (base === Op.LENGTH_EQ) return Op.EQ;
|
|
2159
2061
|
if (base === Op.LENGTH_GT) return Op.GT;
|
|
@@ -2162,7 +2064,6 @@ function normalizeLengthOp(base) {
|
|
|
2162
2064
|
if (base === Op.LENGTH_LTE) return Op.LTE;
|
|
2163
2065
|
return base;
|
|
2164
2066
|
}
|
|
2165
|
-
/** Convert a negated bound opcode to its positive equivalent. */
|
|
2166
2067
|
function negateBoundOp(base) {
|
|
2167
2068
|
if (base === Op.GT) return Op.LTE;
|
|
2168
2069
|
if (base === Op.GTE) return Op.LT;
|
|
@@ -2170,7 +2071,6 @@ function negateBoundOp(base) {
|
|
|
2170
2071
|
if (base === Op.LTE) return Op.GT;
|
|
2171
2072
|
return base;
|
|
2172
2073
|
}
|
|
2173
|
-
/** Fresh bound domain with no constraints recorded yet. */
|
|
2174
2074
|
function emptyBoundDomain(boundIsSigned, min, max) {
|
|
2175
2075
|
return {
|
|
2176
2076
|
isSigned: boundIsSigned,
|
|
@@ -2187,12 +2087,12 @@ function emptyBoundDomain(boundIsSigned, min, max) {
|
|
|
2187
2087
|
holes: []
|
|
2188
2088
|
};
|
|
2189
2089
|
}
|
|
2190
|
-
|
|
2191
|
-
function initContext(scope, path, typeInfo) {
|
|
2090
|
+
function initContext(scope, path, steps, typeInfo) {
|
|
2192
2091
|
const { min, max } = getDomainLimits(typeInfo.typeCode);
|
|
2193
2092
|
return {
|
|
2194
2093
|
scope,
|
|
2195
2094
|
path,
|
|
2095
|
+
steps,
|
|
2196
2096
|
typeInfo,
|
|
2197
2097
|
numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
|
|
2198
2098
|
bitmask: {
|
|
@@ -2207,11 +2107,7 @@ function initContext(scope, path, typeInfo) {
|
|
|
2207
2107
|
}
|
|
2208
2108
|
};
|
|
2209
2109
|
}
|
|
2210
|
-
/** Narrow the numeric bound domain with an operator and emit issues on contradiction or redundancy. */
|
|
2211
2110
|
function updateBound(domain, base, isNegated, value, isLength, groupIndex, constraintIndex, issues) {
|
|
2212
|
-
let changedEq = false;
|
|
2213
|
-
let changedLower = false;
|
|
2214
|
-
let changedUpper = false;
|
|
2215
2111
|
if (isNegated) {
|
|
2216
2112
|
if (base === Op.EQ) {
|
|
2217
2113
|
if (domain.hasEq && domain.eq === value) issues.push(eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
@@ -2220,7 +2116,7 @@ function updateBound(domain, base, isNegated, value, isLength, groupIndex, const
|
|
|
2220
2116
|
alreadyHole = true;
|
|
2221
2117
|
break;
|
|
2222
2118
|
}
|
|
2223
|
-
if (!alreadyHole
|
|
2119
|
+
if (!alreadyHole) domain.holes.push(value);
|
|
2224
2120
|
} else updateBound(domain, negateBoundOp(base), false, value, isLength, groupIndex, constraintIndex, issues);
|
|
2225
2121
|
return;
|
|
2226
2122
|
}
|
|
@@ -2229,6 +2125,9 @@ function updateBound(domain, base, isNegated, value, isLength, groupIndex, const
|
|
|
2229
2125
|
if (signedCompare(value, domain.min, domain.isSigned) < 0n || signedCompare(value, domain.max, domain.isSigned) > 0n) issues.push(outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2230
2126
|
else if (base === Op.GT && value === domain.max) issues.push(impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2231
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;
|
|
2232
2131
|
if (base === Op.EQ) {
|
|
2233
2132
|
if (!domain.hasEq || domain.eq !== value) changedEq = true;
|
|
2234
2133
|
if (domain.hasEq) {
|
|
@@ -2300,7 +2199,6 @@ function updateBound(domain, base, isNegated, value, isLength, groupIndex, const
|
|
|
2300
2199
|
}
|
|
2301
2200
|
}
|
|
2302
2201
|
}
|
|
2303
|
-
/** Update the bitmask domain and emit issues on contradiction or redundancy. */
|
|
2304
2202
|
function updateBitmask(ctx, base, isNegated, mask, groupIndex, constraintIndex, issues) {
|
|
2305
2203
|
if (isNegated) return;
|
|
2306
2204
|
if (base === Op.BITMASK_ALL) {
|
|
@@ -2316,7 +2214,6 @@ function updateBitmask(ctx, base, isNegated, mask, groupIndex, constraintIndex,
|
|
|
2316
2214
|
if ((ctx.bitmask.mustBeOne & mask) !== 0n) issues.push(redundantBitmask(groupIndex, constraintIndex, bigintToHex(mask), bigintToHex(ctx.bitmask.mustBeOne)));
|
|
2317
2215
|
}
|
|
2318
2216
|
}
|
|
2319
|
-
/** Emit an issue if the IN set has been fully excluded by NOT_IN or NEQ holes. */
|
|
2320
2217
|
function checkSetEmpty(ctx, groupIndex, constraintIndex, issues) {
|
|
2321
2218
|
if (!ctx.set.hasIn) return;
|
|
2322
2219
|
let possibleCount = 0;
|
|
@@ -2339,7 +2236,6 @@ function checkSetEmpty(ctx, groupIndex, constraintIndex, issues) {
|
|
|
2339
2236
|
if (possibleCount === 0) issues.push(setFullyExcluded(groupIndex, constraintIndex));
|
|
2340
2237
|
else if (possibleCount < inCount) issues.push(setPartiallyExcluded(groupIndex, constraintIndex, bigintToHex(BigInt(inCount - possibleCount))));
|
|
2341
2238
|
}
|
|
2342
|
-
/** Update the set domain with IN or NOT_IN values and emit issues on contradiction or redundancy. */
|
|
2343
2239
|
function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues) {
|
|
2344
2240
|
if (isNegated) {
|
|
2345
2241
|
for (const value of values) {
|
|
@@ -2352,10 +2248,11 @@ function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues)
|
|
|
2352
2248
|
}
|
|
2353
2249
|
if (inSet) issues.push(setReduction(groupIndex, constraintIndex, bigintToHex(value)));
|
|
2354
2250
|
}
|
|
2355
|
-
|
|
2251
|
+
ctx.set.notInValues.push(value);
|
|
2356
2252
|
}
|
|
2357
2253
|
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
2358
2254
|
} else {
|
|
2255
|
+
for (const value of values) if (signedCompare(value, ctx.numeric.min, ctx.numeric.isSigned) < 0n || signedCompare(value, ctx.numeric.max, ctx.numeric.isSigned) > 0n) issues.push(outOfPhysicalBounds(false, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2359
2256
|
if (ctx.numeric.hasEq) {
|
|
2360
2257
|
let found = false;
|
|
2361
2258
|
for (const candidate of values) if (candidate === ctx.numeric.eq) {
|
|
@@ -2380,7 +2277,6 @@ function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues)
|
|
|
2380
2277
|
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
2381
2278
|
}
|
|
2382
2279
|
}
|
|
2383
|
-
/** Emit a warning if the constraint contains any identical operators. */
|
|
2384
2280
|
function checkDuplicates(issues, operators, groupIndex, constraintIndex) {
|
|
2385
2281
|
const seen = /* @__PURE__ */ new Set();
|
|
2386
2282
|
for (const op of operators) {
|
|
@@ -2392,18 +2288,12 @@ function checkDuplicates(issues, operators, groupIndex, constraintIndex) {
|
|
|
2392
2288
|
seen.add(key);
|
|
2393
2289
|
}
|
|
2394
2290
|
}
|
|
2395
|
-
/**
|
|
2396
|
-
* Report a lone unnegated gte/lte pair (value or length domain) as fusible into the
|
|
2397
|
-
* corresponding single range operator. Fires only for a satisfiable pair on a compatible
|
|
2398
|
-
* type; contradictions and dominated bounds are already reported by the domain checks.
|
|
2399
|
-
*/
|
|
2400
2291
|
function checkFusibleRange(ctx, operators, groupIndex, constraintIndex, issues) {
|
|
2401
2292
|
const numericPair = findLonePair(operators, Op.GTE, Op.LTE);
|
|
2402
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)));
|
|
2403
2294
|
const lengthPair = findLonePair(operators, Op.LENGTH_GTE, Op.LENGTH_LTE);
|
|
2404
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)));
|
|
2405
2296
|
}
|
|
2406
|
-
/** Return the operand pair when the operators contain exactly one of each unnegated bound opcode. */
|
|
2407
2297
|
function findLonePair(operators, lowerOp, upperOp) {
|
|
2408
2298
|
let lowerCount = 0;
|
|
2409
2299
|
let upperCount = 0;
|
|
@@ -2426,19 +2316,18 @@ function findLonePair(operators, lowerOp, upperOp) {
|
|
|
2426
2316
|
high
|
|
2427
2317
|
} : null;
|
|
2428
2318
|
}
|
|
2429
|
-
/** Validate all operators on a single constraint against its domain context. */
|
|
2430
2319
|
function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues) {
|
|
2431
2320
|
const operators = constraint.operators;
|
|
2432
|
-
const underAny = constraint.scope === Scope.CALLDATA &&
|
|
2321
|
+
const underAny = constraint.scope === Scope.CALLDATA && ctx.steps.includes(Quantifier.ANY);
|
|
2433
2322
|
for (const opHex of operators) {
|
|
2434
2323
|
const opCode = parseInt(opHex.slice(2, 4), 16);
|
|
2435
2324
|
const base = opCode & ~Op.NOT;
|
|
2436
|
-
const isNegated = (opCode & Op.NOT) !== 0;
|
|
2437
2325
|
const dataLength = (opHex.length - 4) / 2;
|
|
2438
2326
|
if (dataLength > 65535 || !isValidOperatorData(base, dataLength)) {
|
|
2439
2327
|
issues.push(fromOpRule("UNKNOWN_OPERATOR", "Unknown operator code", groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
2440
2328
|
continue;
|
|
2441
2329
|
}
|
|
2330
|
+
const isNegated = (opCode & Op.NOT) !== 0;
|
|
2442
2331
|
if (underAny && isNegated) issues.push(negationUnderAny(groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
2443
2332
|
const incompat = getIncompat(base, ctx.typeInfo);
|
|
2444
2333
|
if (incompat) {
|
|
@@ -2455,11 +2344,11 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
|
|
|
2455
2344
|
}
|
|
2456
2345
|
}
|
|
2457
2346
|
if (base >= Op.EQ && base <= Op.BETWEEN) if (base === Op.BETWEEN) {
|
|
2347
|
+
const { low, high } = readPair(opHex);
|
|
2458
2348
|
if (!isNegated) {
|
|
2459
|
-
const { low, high } = readPair(opHex);
|
|
2460
2349
|
updateBound(ctx.numeric, Op.GTE, false, low, false, groupIndex, constraintIndex, issues);
|
|
2461
2350
|
updateBound(ctx.numeric, Op.LTE, false, high, false, groupIndex, constraintIndex, issues);
|
|
2462
|
-
}
|
|
2351
|
+
} else if (signedCompare(low, high, ctx.numeric.isSigned) > 0n) issues.push(vacuousNegatedRange(false, groupIndex, constraintIndex, bigintToHex(low), bigintToHex(high)));
|
|
2463
2352
|
} else {
|
|
2464
2353
|
const value = readValue(opHex);
|
|
2465
2354
|
const holesBefore = ctx.numeric.holes.length;
|
|
@@ -2473,11 +2362,11 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
|
|
|
2473
2362
|
else updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues);
|
|
2474
2363
|
}
|
|
2475
2364
|
} else if (isLengthOp(base)) if (base === Op.LENGTH_BETWEEN) {
|
|
2365
|
+
const { low, high } = readPair(opHex);
|
|
2476
2366
|
if (!isNegated) {
|
|
2477
|
-
const { low, high } = readPair(opHex);
|
|
2478
2367
|
updateBound(ctx.length, Op.GTE, false, low, true, groupIndex, constraintIndex, issues);
|
|
2479
2368
|
updateBound(ctx.length, Op.LTE, false, high, true, groupIndex, constraintIndex, issues);
|
|
2480
|
-
}
|
|
2369
|
+
} else if (low > high) issues.push(vacuousNegatedRange(true, groupIndex, constraintIndex, bigintToHex(low), bigintToHex(high)));
|
|
2481
2370
|
} else {
|
|
2482
2371
|
const value = readValue(opHex);
|
|
2483
2372
|
updateBound(ctx.length, normalizeLengthOp(base), isNegated, value, true, groupIndex, constraintIndex, issues);
|
|
@@ -2485,7 +2374,6 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
|
|
|
2485
2374
|
}
|
|
2486
2375
|
checkDuplicates(issues, operators, groupIndex, constraintIndex);
|
|
2487
2376
|
}
|
|
2488
|
-
/** Validate all constraints in a group, building per-path contexts for cross-constraint analysis. */
|
|
2489
2377
|
function validateGroup(data, descBytes, groupIndex, issues) {
|
|
2490
2378
|
const constraints = data.groups[groupIndex];
|
|
2491
2379
|
const contexts = [];
|
|
@@ -2498,15 +2386,28 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2498
2386
|
break;
|
|
2499
2387
|
}
|
|
2500
2388
|
if (!ctx) {
|
|
2389
|
+
const steps = parsePathSteps(constraint.path);
|
|
2501
2390
|
let typeInfo;
|
|
2502
2391
|
if (constraint.scope === Scope.CALLDATA) {
|
|
2503
|
-
const steps = parsePathSteps(constraint.path);
|
|
2504
2392
|
if (steps.length > Limits.MAX_PATH_DEPTH) issues.push(pathDepthExceeded(groupIndex, constraintIndex, bigintToHex(BigInt(steps.length)), bigintToHex(BigInt(Limits.MAX_PATH_DEPTH))));
|
|
2505
|
-
|
|
2393
|
+
let walk;
|
|
2394
|
+
try {
|
|
2395
|
+
walk = Descriptor.walkPath(descBytes, steps);
|
|
2396
|
+
} catch (error) {
|
|
2397
|
+
if (error instanceof CallciumError && NAVIGABILITY_CODES.has(error.code)) {
|
|
2398
|
+
issues.push(unnavigablePath(groupIndex, constraintIndex));
|
|
2399
|
+
continue;
|
|
2400
|
+
}
|
|
2401
|
+
throw error;
|
|
2402
|
+
}
|
|
2403
|
+
if (steps.filter(isQuantifier).length > 1) {
|
|
2404
|
+
issues.push(nestedQuantifier(groupIndex, constraintIndex));
|
|
2405
|
+
continue;
|
|
2406
|
+
}
|
|
2506
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))));
|
|
2507
2408
|
typeInfo = walk.typeInfo;
|
|
2508
2409
|
} else {
|
|
2509
|
-
const ctxId =
|
|
2410
|
+
const ctxId = steps[0];
|
|
2510
2411
|
if (ctxId > MAX_CONTEXT_PROPERTY_ID) {
|
|
2511
2412
|
issues.push(unknownContextProperty(groupIndex, constraintIndex, bigintToHex(BigInt(ctxId)), bigintToHex(BigInt(MAX_CONTEXT_PROPERTY_ID))));
|
|
2512
2413
|
typeInfo = {
|
|
@@ -2520,21 +2421,20 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2520
2421
|
staticSize: 32
|
|
2521
2422
|
};
|
|
2522
2423
|
}
|
|
2523
|
-
ctx = initContext(constraint.scope, normalizedPath, typeInfo);
|
|
2424
|
+
ctx = initContext(constraint.scope, normalizedPath, steps, typeInfo);
|
|
2524
2425
|
contexts.push(ctx);
|
|
2525
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
|
+
}
|
|
2526
2430
|
validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues);
|
|
2527
2431
|
checkFusibleRange(ctx, constraint.operators, groupIndex, constraintIndex, issues);
|
|
2528
2432
|
}
|
|
2529
2433
|
}
|
|
2530
|
-
/**
|
|
2531
|
-
* Validate a policy for type mismatches, contradictions, redundancies, and vacuities.
|
|
2532
|
-
* @param data - The canonical policy data to validate.
|
|
2533
|
-
* @returns All validation issues found, ordered by group and constraint index.
|
|
2534
|
-
*/
|
|
2535
2434
|
function validate(data) {
|
|
2536
|
-
const issues = [];
|
|
2537
2435
|
const descBytes = hexToBytes(data.descriptor);
|
|
2436
|
+
decodeDescriptor(descBytes);
|
|
2437
|
+
const issues = [];
|
|
2538
2438
|
for (let groupIndex = 0; groupIndex < data.groups.length; groupIndex++) {
|
|
2539
2439
|
if (data.groups[groupIndex].length === 0) {
|
|
2540
2440
|
issues.push(emptyGroup(groupIndex));
|
|
@@ -2544,102 +2444,80 @@ function validate(data) {
|
|
|
2544
2444
|
}
|
|
2545
2445
|
return issues;
|
|
2546
2446
|
}
|
|
2547
|
-
/** Semantic validation for policies. */
|
|
2548
2447
|
const PolicyValidator = { validate };
|
|
2549
2448
|
//#endregion
|
|
2550
2449
|
//#region src/signature.ts
|
|
2551
|
-
/** Return true if the char code is an ASCII letter. */
|
|
2552
2450
|
function isAlpha(c) {
|
|
2553
2451
|
return c >= 65 && c <= 90 || c >= 97 && c <= 122;
|
|
2554
2452
|
}
|
|
2555
|
-
/** Return true if the char code is an ASCII alphanumeric character. */
|
|
2556
2453
|
function isAlphanum(c) {
|
|
2557
2454
|
return isAlpha(c) || c >= 48 && c <= 57;
|
|
2558
2455
|
}
|
|
2559
|
-
/**
|
|
2560
|
-
* Parse an ABI function signature into its selector and types.
|
|
2561
|
-
*
|
|
2562
|
-
* Strict mode: no whitespace, name must match `[A-Za-z_][A-Za-z0-9_]*`,
|
|
2563
|
-
* structure must be `name(types)` with the final `)` as the last character.
|
|
2564
|
-
*
|
|
2565
|
-
* @param signature - A function signature string, e.g. `"transfer(address,uint256)"`.
|
|
2566
|
-
* @returns The 4-byte selector and the comma-separated types string.
|
|
2567
|
-
* @throws {CallciumError} with code `"INVALID_SIGNATURE"` if the signature is malformed.
|
|
2568
|
-
*/
|
|
2569
2456
|
function parse(signature) {
|
|
2570
|
-
if (signature.length < 3) throw new CallciumError("
|
|
2457
|
+
if (signature.length < 3) throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: too short, got "${signature}"`);
|
|
2571
2458
|
let openParen = -1;
|
|
2572
2459
|
for (let i = 0; i < signature.length; i++) {
|
|
2573
2460
|
const c = signature.charCodeAt(i);
|
|
2574
|
-
if (c === 32 || c === 9 || c === 10 || c === 13) throw new CallciumError("
|
|
2575
|
-
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");
|
|
2576
2463
|
if (openParen === -1 && c === 40) openParen = i;
|
|
2577
2464
|
}
|
|
2578
|
-
if (openParen < 1) throw new CallciumError("
|
|
2579
|
-
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}"`);
|
|
2580
2467
|
const firstChar = signature.charCodeAt(0);
|
|
2581
|
-
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");
|
|
2582
2469
|
for (let i = 1; i < openParen; i++) {
|
|
2583
2470
|
const c = signature.charCodeAt(i);
|
|
2584
|
-
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");
|
|
2585
2472
|
}
|
|
2586
|
-
const types = signature.slice(openParen + 1, signature.length - 1);
|
|
2587
2473
|
const encoded = new Uint8Array(signature.length);
|
|
2588
2474
|
for (let i = 0; i < signature.length; i++) encoded[i] = signature.charCodeAt(i);
|
|
2589
2475
|
return {
|
|
2590
2476
|
selector: bytesToHex(keccak_256(encoded).subarray(0, 4)),
|
|
2591
|
-
types
|
|
2477
|
+
types: signature.slice(openParen + 1, signature.length - 1)
|
|
2592
2478
|
};
|
|
2593
2479
|
}
|
|
2594
|
-
/** ABI function signature parser. */
|
|
2595
2480
|
const SignatureParser = { parse };
|
|
2596
2481
|
//#endregion
|
|
2597
2482
|
//#region src/policy-builder.ts
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
/** Validate a calldata-scope path against the descriptor. */
|
|
2605
|
-
function validateCalldataPath(path, desc) {
|
|
2606
|
-
const steps = parsePathSteps(path);
|
|
2607
|
-
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) {
|
|
2608
2489
|
const argIndex = steps[0];
|
|
2609
2490
|
const paramCount = Descriptor.paramCount(desc);
|
|
2610
|
-
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).`);
|
|
2611
2492
|
let offset = Descriptor.paramOffset(desc, argIndex);
|
|
2612
2493
|
let hasQuantifier = false;
|
|
2613
2494
|
for (let i = 1; i < steps.length; i++) {
|
|
2614
2495
|
const step = steps[i];
|
|
2615
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
|
+
}
|
|
2616
2503
|
if (info.typeCode === TypeCode.TUPLE) {
|
|
2617
|
-
if (isQuantifier(step)) throw new CallciumError("INVALID_PATH", "Quantifier step is not valid on a tuple node.");
|
|
2618
2504
|
const fieldCount = Descriptor.tupleFieldCount(desc, offset);
|
|
2619
|
-
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).`);
|
|
2620
2506
|
offset = Descriptor.tupleFieldOffset(desc, offset, step);
|
|
2621
|
-
} else if (
|
|
2622
|
-
if (isQuantifier(step)) {
|
|
2623
|
-
if (hasQuantifier) throw new CallciumError("INVALID_PATH", "Nested quantifiers are not allowed.");
|
|
2624
|
-
hasQuantifier = true;
|
|
2625
|
-
} else if (info.typeCode === TypeCode.STATIC_ARRAY) {
|
|
2507
|
+
} else if (isArray) {
|
|
2508
|
+
if (!isQuantifier(step) && info.typeCode === TypeCode.STATIC_ARRAY) {
|
|
2626
2509
|
const arrayLength = Descriptor.staticArrayLength(desc, offset);
|
|
2627
|
-
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).`);
|
|
2628
2511
|
}
|
|
2629
2512
|
offset = Descriptor.arrayElementOffset(offset);
|
|
2630
|
-
} else throw new CallciumError("
|
|
2513
|
+
} else throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type.");
|
|
2631
2514
|
}
|
|
2632
2515
|
}
|
|
2633
|
-
/** Fluent builder for constructing Callcium policies. */
|
|
2634
2516
|
var PolicyBuilder = class PolicyBuilder {
|
|
2635
2517
|
draft;
|
|
2636
2518
|
constructor(draft) {
|
|
2637
2519
|
this.draft = draft;
|
|
2638
2520
|
}
|
|
2639
|
-
/**
|
|
2640
|
-
* Create a builder from a function signature.
|
|
2641
|
-
* @param signature - ABI function signature, e.g. `"transfer(address,uint256)"`.
|
|
2642
|
-
*/
|
|
2643
2521
|
static create(signature) {
|
|
2644
2522
|
const parsed = SignatureParser.parse(signature);
|
|
2645
2523
|
const descriptor = DescriptorCoder.fromTypes(parsed.types);
|
|
@@ -2651,10 +2529,6 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2651
2529
|
pathHashes: [/* @__PURE__ */ new Set()]
|
|
2652
2530
|
});
|
|
2653
2531
|
}
|
|
2654
|
-
/**
|
|
2655
|
-
* Create a selectorless builder from a raw type string.
|
|
2656
|
-
* @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256"`.
|
|
2657
|
-
*/
|
|
2658
2532
|
static createRaw(typesCsv) {
|
|
2659
2533
|
return new PolicyBuilder({
|
|
2660
2534
|
isSelectorless: true,
|
|
@@ -2664,73 +2538,50 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2664
2538
|
pathHashes: [/* @__PURE__ */ new Set()]
|
|
2665
2539
|
});
|
|
2666
2540
|
}
|
|
2667
|
-
/**
|
|
2668
|
-
* Add a constraint to the current group.
|
|
2669
|
-
* @param constraint - A `Constraint` object or a `ConstraintBuilder` instance.
|
|
2670
|
-
*/
|
|
2671
2541
|
add(constraint) {
|
|
2672
2542
|
const c = {
|
|
2673
2543
|
scope: constraint.scope,
|
|
2674
2544
|
path: constraint.path,
|
|
2675
2545
|
operators: [...constraint.operators]
|
|
2676
2546
|
};
|
|
2677
|
-
if (c.operators.length === 0) throw new CallciumError("
|
|
2678
|
-
|
|
2679
|
-
|
|
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);
|
|
2680
2552
|
else throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${c.scope}.`);
|
|
2681
2553
|
const key = `${c.scope}:${c.path.toLowerCase()}`;
|
|
2682
2554
|
const currentHashes = this.draft.pathHashes[this.draft.pathHashes.length - 1];
|
|
2683
|
-
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.`);
|
|
2684
2556
|
currentHashes.add(key);
|
|
2685
2557
|
this.draft.groups[this.draft.groups.length - 1].push(c);
|
|
2686
2558
|
return this;
|
|
2687
2559
|
}
|
|
2688
|
-
/** Start a new constraint group (OR branch). */
|
|
2689
2560
|
or() {
|
|
2690
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.");
|
|
2691
2562
|
this.draft.groups.push([]);
|
|
2692
2563
|
this.draft.pathHashes.push(/* @__PURE__ */ new Set());
|
|
2693
2564
|
return this;
|
|
2694
2565
|
}
|
|
2695
|
-
/**
|
|
2696
|
-
* Build the policy into an encoded binary blob with strict validation.
|
|
2697
|
-
* Throws on any issue, regardless of severity. Use {@link validate} to
|
|
2698
|
-
* inspect issues, or {@link buildUnsafe} to encode without validation.
|
|
2699
|
-
* @returns The policy as a 0x-prefixed hex string.
|
|
2700
|
-
* @throws {CallciumError} If any group is empty or validation finds any issue.
|
|
2701
|
-
*/
|
|
2702
2566
|
build() {
|
|
2703
2567
|
this.checkGroups();
|
|
2704
2568
|
const policyData = this.toPolicyData();
|
|
2705
2569
|
const issues = PolicyValidator.validate(policyData);
|
|
2706
|
-
if (issues.length > 0) throw new
|
|
2570
|
+
if (issues.length > 0) throw new ValidationError(issues);
|
|
2707
2571
|
return PolicyCoder.encode(policyData);
|
|
2708
2572
|
}
|
|
2709
|
-
/**
|
|
2710
|
-
* Build the policy into an encoded binary blob without validation.
|
|
2711
|
-
* The resulting policy may be invalid. Prefer {@link build}; use this
|
|
2712
|
-
* only to knowingly bypass a reported issue.
|
|
2713
|
-
* @returns The policy as a 0x-prefixed hex string.
|
|
2714
|
-
* @throws {CallciumError} If any group is empty.
|
|
2715
|
-
*/
|
|
2716
2573
|
buildUnsafe() {
|
|
2717
2574
|
this.checkGroups();
|
|
2718
2575
|
return PolicyCoder.encode(this.toPolicyData());
|
|
2719
2576
|
}
|
|
2720
|
-
/**
|
|
2721
|
-
* Validate the policy without encoding.
|
|
2722
|
-
* @returns All validation issues found.
|
|
2723
|
-
*/
|
|
2724
2577
|
validate() {
|
|
2725
2578
|
this.checkGroups();
|
|
2726
2579
|
const policyData = this.toPolicyData();
|
|
2727
2580
|
return PolicyValidator.validate(policyData);
|
|
2728
2581
|
}
|
|
2729
|
-
/** Throw if any group is empty. */
|
|
2730
2582
|
checkGroups() {
|
|
2731
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.`);
|
|
2732
2584
|
}
|
|
2733
|
-
/** Convert the draft to a PolicyData structure. */
|
|
2734
2585
|
toPolicyData() {
|
|
2735
2586
|
return {
|
|
2736
2587
|
isSelectorless: this.draft.isSelectorless,
|
|
@@ -2742,13 +2593,9 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2742
2593
|
};
|
|
2743
2594
|
//#endregion
|
|
2744
2595
|
//#region src/constraint.ts
|
|
2745
|
-
/** Strip an optional 0x prefix and validate a 40-hex-char (20-byte) address body. */
|
|
2746
2596
|
function addressBody(value) {
|
|
2747
|
-
|
|
2748
|
-
if (body.length !== 40) throw new CallciumError("INVALID_CONSTRAINT", `Invalid address length: expected 40 hex chars, got ${body.length}`);
|
|
2749
|
-
return body;
|
|
2597
|
+
return toAddress(value).slice(2);
|
|
2750
2598
|
}
|
|
2751
|
-
/** Convert a scalar value to a 32-byte big-endian word. */
|
|
2752
2599
|
function encodeWord(value) {
|
|
2753
2600
|
const word = new Uint8Array(32);
|
|
2754
2601
|
if (typeof value === "boolean") {
|
|
@@ -2757,11 +2604,7 @@ function encodeWord(value) {
|
|
|
2757
2604
|
}
|
|
2758
2605
|
if (typeof value === "string") {
|
|
2759
2606
|
const body = addressBody(value);
|
|
2760
|
-
for (let i = 0; i < 20; i++)
|
|
2761
|
-
const byte = parseInt(body.substring(i * 2, i * 2 + 2), 16);
|
|
2762
|
-
if (Number.isNaN(byte)) throw new CallciumError("INVALID_CONSTRAINT", `Invalid hex character in address at position ${i * 2}`);
|
|
2763
|
-
word[12 + i] = byte;
|
|
2764
|
-
}
|
|
2607
|
+
for (let i = 0; i < 20; i++) word[12 + i] = parseInt(body.substring(i * 2, i * 2 + 2), 16);
|
|
2765
2608
|
return word;
|
|
2766
2609
|
}
|
|
2767
2610
|
let bigValue = typeof value === "number" ? BigInt(value) : value;
|
|
@@ -2771,14 +2614,12 @@ function encodeWord(value) {
|
|
|
2771
2614
|
}
|
|
2772
2615
|
return word;
|
|
2773
2616
|
}
|
|
2774
|
-
/** Pack a scalar value as a hex operator payload (opCode byte + 32-byte word). */
|
|
2775
2617
|
function singleOp(opCode, value) {
|
|
2776
2618
|
const buffer = new Uint8Array(33);
|
|
2777
2619
|
buffer[0] = opCode;
|
|
2778
2620
|
buffer.set(encodeWord(value), 1);
|
|
2779
2621
|
return bytesToHex(buffer);
|
|
2780
2622
|
}
|
|
2781
|
-
/** Pack a range operator (opCode byte + min word + max word). */
|
|
2782
2623
|
function rangeOp(opCode, min, max) {
|
|
2783
2624
|
if (min > max) throw new CallciumError("INVALID_RANGE", `Range min (${min}) must not exceed max (${max})`);
|
|
2784
2625
|
const buffer = new Uint8Array(65);
|
|
@@ -2787,7 +2628,7 @@ function rangeOp(opCode, min, max) {
|
|
|
2787
2628
|
buffer.set(encodeWord(max), 33);
|
|
2788
2629
|
return bytesToHex(buffer);
|
|
2789
2630
|
}
|
|
2790
|
-
|
|
2631
|
+
const MAX_SET_MEMBERS = Limits.MAX_SET_MEMBERS;
|
|
2791
2632
|
function setOp(opCode, values) {
|
|
2792
2633
|
const bigs = values.map((v) => {
|
|
2793
2634
|
if (typeof v === "bigint") return v;
|
|
@@ -2799,118 +2640,81 @@ function setOp(opCode, values) {
|
|
|
2799
2640
|
const deduped = [];
|
|
2800
2641
|
for (const value of bigs) if (deduped.length === 0 || deduped[deduped.length - 1] !== value) deduped.push(value);
|
|
2801
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`);
|
|
2802
2644
|
const buffer = new Uint8Array(1 + deduped.length * 32);
|
|
2803
2645
|
buffer[0] = opCode;
|
|
2804
2646
|
for (let i = 0; i < deduped.length; i++) buffer.set(encodeWord(deduped[i]), 1 + i * 32);
|
|
2805
2647
|
return bytesToHex(buffer);
|
|
2806
2648
|
}
|
|
2807
|
-
/** Encode a sequence of uint16 path steps as a big-endian hex string. */
|
|
2808
2649
|
function encodePath(steps) {
|
|
2809
2650
|
const buffer = new Uint8Array(steps.length * 2);
|
|
2810
2651
|
for (let i = 0; i < steps.length; i++) writeBE16(buffer, i * 2, steps[i]);
|
|
2811
2652
|
return bytesToHex(buffer);
|
|
2812
2653
|
}
|
|
2813
|
-
/**
|
|
2814
|
-
* Mutable builder that accumulates operators targeting a single path.
|
|
2815
|
-
* Implements the `Constraint` interface so it can be passed directly to policy builders.
|
|
2816
|
-
*/
|
|
2817
2654
|
var ConstraintBuilder = class {
|
|
2818
2655
|
scope;
|
|
2819
2656
|
path;
|
|
2820
2657
|
operators;
|
|
2821
|
-
/** @internal */
|
|
2822
2658
|
constructor(scope, path) {
|
|
2823
2659
|
this.scope = scope;
|
|
2824
2660
|
this.path = path;
|
|
2825
2661
|
this.operators = [];
|
|
2826
2662
|
}
|
|
2827
|
-
/** Push a pre-encoded operator hex string and return this for chaining. */
|
|
2828
2663
|
push(opHex) {
|
|
2829
2664
|
this.operators.push(opHex);
|
|
2830
2665
|
return this;
|
|
2831
2666
|
}
|
|
2832
|
-
/** Assert the value equals `value`. */
|
|
2833
2667
|
eq(value) {
|
|
2834
2668
|
return this.push(singleOp(Op.EQ, value));
|
|
2835
2669
|
}
|
|
2836
|
-
/** Assert the value does not equal `value`. */
|
|
2837
2670
|
neq(value) {
|
|
2838
2671
|
return this.push(singleOp(Op.EQ | Op.NOT, value));
|
|
2839
2672
|
}
|
|
2840
|
-
/** Assert the value is greater than `bound`. */
|
|
2841
2673
|
gt(bound) {
|
|
2842
2674
|
return this.push(singleOp(Op.GT, bound));
|
|
2843
2675
|
}
|
|
2844
|
-
/** Assert the value is less than `bound`. */
|
|
2845
2676
|
lt(bound) {
|
|
2846
2677
|
return this.push(singleOp(Op.LT, bound));
|
|
2847
2678
|
}
|
|
2848
|
-
/** Assert the value is greater than or equal to `bound`. */
|
|
2849
2679
|
gte(bound) {
|
|
2850
2680
|
return this.push(singleOp(Op.GTE, bound));
|
|
2851
2681
|
}
|
|
2852
|
-
/** Assert the value is less than or equal to `bound`. */
|
|
2853
2682
|
lte(bound) {
|
|
2854
2683
|
return this.push(singleOp(Op.LTE, bound));
|
|
2855
2684
|
}
|
|
2856
|
-
/**
|
|
2857
|
-
* Assert the value is within [min, max] inclusive.
|
|
2858
|
-
* @throws {CallciumError} If min > max.
|
|
2859
|
-
*/
|
|
2860
2685
|
between(min, max) {
|
|
2861
2686
|
return this.push(rangeOp(Op.BETWEEN, BigInt(min), BigInt(max)));
|
|
2862
2687
|
}
|
|
2863
|
-
/**
|
|
2864
|
-
* Assert the value is a member of the set.
|
|
2865
|
-
* Values are sorted and deduplicated before encoding.
|
|
2866
|
-
* @throws {CallciumError} If the set is empty after deduplication.
|
|
2867
|
-
*/
|
|
2868
2688
|
isIn(values) {
|
|
2869
2689
|
return this.push(setOp(Op.IN, values));
|
|
2870
2690
|
}
|
|
2871
|
-
/**
|
|
2872
|
-
* Assert the value is not a member of the set.
|
|
2873
|
-
* @throws {CallciumError} If the set is empty after deduplication.
|
|
2874
|
-
*/
|
|
2875
2691
|
notIn(values) {
|
|
2876
2692
|
return this.push(setOp(Op.IN | Op.NOT, values));
|
|
2877
2693
|
}
|
|
2878
|
-
/** Assert all bits in `mask` are set. */
|
|
2879
2694
|
bitmaskAll(mask) {
|
|
2880
2695
|
return this.push(singleOp(Op.BITMASK_ALL, mask));
|
|
2881
2696
|
}
|
|
2882
|
-
/** Assert at least one bit in `mask` is set. */
|
|
2883
2697
|
bitmaskAny(mask) {
|
|
2884
2698
|
return this.push(singleOp(Op.BITMASK_ANY, mask));
|
|
2885
2699
|
}
|
|
2886
|
-
/** Assert no bit in `mask` is set. */
|
|
2887
2700
|
bitmaskNone(mask) {
|
|
2888
2701
|
return this.push(singleOp(Op.BITMASK_NONE, mask));
|
|
2889
2702
|
}
|
|
2890
|
-
/** Assert the runtime length equals `n`. */
|
|
2891
2703
|
lengthEq(n) {
|
|
2892
2704
|
return this.push(singleOp(Op.LENGTH_EQ, n));
|
|
2893
2705
|
}
|
|
2894
|
-
/** Assert the runtime length is greater than `n`. */
|
|
2895
2706
|
lengthGt(n) {
|
|
2896
2707
|
return this.push(singleOp(Op.LENGTH_GT, n));
|
|
2897
2708
|
}
|
|
2898
|
-
/** Assert the runtime length is less than `n`. */
|
|
2899
2709
|
lengthLt(n) {
|
|
2900
2710
|
return this.push(singleOp(Op.LENGTH_LT, n));
|
|
2901
2711
|
}
|
|
2902
|
-
/** Assert the runtime length is greater than or equal to `n`. */
|
|
2903
2712
|
lengthGte(n) {
|
|
2904
2713
|
return this.push(singleOp(Op.LENGTH_GTE, n));
|
|
2905
2714
|
}
|
|
2906
|
-
/** Assert the runtime length is less than or equal to `n`. */
|
|
2907
2715
|
lengthLte(n) {
|
|
2908
2716
|
return this.push(singleOp(Op.LENGTH_LTE, n));
|
|
2909
2717
|
}
|
|
2910
|
-
/**
|
|
2911
|
-
* Assert the runtime length is within [min, max] inclusive.
|
|
2912
|
-
* @throws {CallciumError} If min > max.
|
|
2913
|
-
*/
|
|
2914
2718
|
lengthBetween(min, max) {
|
|
2915
2719
|
return this.push(rangeOp(Op.LENGTH_BETWEEN, BigInt(min), BigInt(max)));
|
|
2916
2720
|
}
|
|
@@ -2918,418 +2722,60 @@ var ConstraintBuilder = class {
|
|
|
2918
2722
|
function arg(...steps) {
|
|
2919
2723
|
return new ConstraintBuilder(Scope.CALLDATA, encodePath(steps));
|
|
2920
2724
|
}
|
|
2921
|
-
/** Target the `msg.sender` context property. */
|
|
2922
2725
|
function msgSender() {
|
|
2923
2726
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_SENDER]));
|
|
2924
2727
|
}
|
|
2925
|
-
/** Target the `msg.value` context property. */
|
|
2926
2728
|
function msgValue() {
|
|
2927
2729
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_VALUE]));
|
|
2928
2730
|
}
|
|
2929
|
-
/** Target the `block.timestamp` context property. */
|
|
2930
2731
|
function blockTimestamp() {
|
|
2931
2732
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_TIMESTAMP]));
|
|
2932
2733
|
}
|
|
2933
|
-
/** Target the `block.number` context property. */
|
|
2934
2734
|
function blockNumber() {
|
|
2935
2735
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_NUMBER]));
|
|
2936
2736
|
}
|
|
2937
|
-
/** Target the `block.chainid` context property. */
|
|
2938
2737
|
function chainId() {
|
|
2939
2738
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.CHAIN_ID]));
|
|
2940
2739
|
}
|
|
2941
|
-
/** Target the `tx.origin` context property. */
|
|
2942
2740
|
function txOrigin() {
|
|
2943
2741
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.TX_ORIGIN]));
|
|
2944
2742
|
}
|
|
2945
|
-
/** Target the `block.basefee` context property. */
|
|
2946
2743
|
function baseFee() {
|
|
2947
2744
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BASE_FEE]));
|
|
2948
2745
|
}
|
|
2949
|
-
/** Target the `tx.gasprice` context property. */
|
|
2950
2746
|
function gasPrice() {
|
|
2951
2747
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.GAS_PRICE]));
|
|
2952
2748
|
}
|
|
2953
2749
|
//#endregion
|
|
2954
|
-
//#region src/reader.ts
|
|
2955
|
-
|
|
2956
|
-
function load32(callData, offset) {
|
|
2750
|
+
//#region src/calldata-reader.ts
|
|
2751
|
+
function loadWord(callData, offset) {
|
|
2957
2752
|
if (offset < 0 || offset + 32 > callData.length) return { code: "CALLDATA_OUT_OF_BOUNDS" };
|
|
2958
2753
|
return callData.subarray(offset, offset + 32);
|
|
2959
2754
|
}
|
|
2960
|
-
/**
|
|
2961
|
-
* Read ABI pointer (offset or length) from a 32-byte slot.
|
|
2962
|
-
* Validates high 28 bytes are zero (rejects values > 32-bit).
|
|
2963
|
-
*/
|
|
2964
2755
|
function readPointer(callData, head) {
|
|
2965
|
-
const word =
|
|
2756
|
+
const word = loadWord(callData, head);
|
|
2966
2757
|
if (word instanceof Uint8Array) {
|
|
2967
2758
|
for (let i = 0; i < 28; i++) if (word[i] !== 0) return { code: "CALLDATA_OUT_OF_BOUNDS" };
|
|
2968
2759
|
return (word[28] << 24 | word[29] << 16 | word[30] << 8 | word[31]) >>> 0;
|
|
2969
2760
|
}
|
|
2970
2761
|
return word;
|
|
2971
2762
|
}
|
|
2972
|
-
/** Return the number of bytes this node occupies in the head region. */
|
|
2973
|
-
function headContribution(node) {
|
|
2974
|
-
return node.isDynamic ? 32 : node.staticSize;
|
|
2975
|
-
}
|
|
2976
|
-
/** Read a single BE16 path step at the given step index. */
|
|
2977
|
-
function readStep(pathBytes, stepIndex) {
|
|
2978
|
-
return readU16(pathBytes, stepIndex * 2);
|
|
2979
|
-
}
|
|
2980
|
-
/** Navigate one path step through a node (tuple field, array element, or quantifier). */
|
|
2981
|
-
function descend(node, callData, head, base, childIndex) {
|
|
2982
|
-
if (node.type === "tuple") return descendTuple(node, callData, head, base, childIndex);
|
|
2983
|
-
if (node.type === "dynamicArray") return descendDynamicArray(node, callData, head, base, childIndex);
|
|
2984
|
-
if (node.type === "staticArray") return descendStaticArray(node, callData, head, base, childIndex);
|
|
2985
|
-
throw new CallciumError("INVALID_PATH", `Cannot descend into elementary type (${node.type}).`);
|
|
2986
|
-
}
|
|
2987
|
-
/** Descend into a tuple field by index. */
|
|
2988
|
-
function descendTuple(node, callData, head, base, childIndex) {
|
|
2989
|
-
if (childIndex >= node.fields.length) throw new CallciumError("INVALID_PATH", `Tuple field index ${childIndex} out of range (${node.fields.length} fields).`);
|
|
2990
|
-
let tupleBase;
|
|
2991
|
-
let cursor;
|
|
2992
|
-
if (node.isDynamic) {
|
|
2993
|
-
const ptrResult = readPointer(callData, head);
|
|
2994
|
-
if (typeof ptrResult !== "number") return {
|
|
2995
|
-
ok: false,
|
|
2996
|
-
code: ptrResult.code
|
|
2997
|
-
};
|
|
2998
|
-
tupleBase = base + ptrResult;
|
|
2999
|
-
cursor = tupleBase;
|
|
3000
|
-
} else {
|
|
3001
|
-
tupleBase = base;
|
|
3002
|
-
cursor = head;
|
|
3003
|
-
}
|
|
3004
|
-
for (let i = 0; i < childIndex; i++) cursor += headContribution(node.fields[i]);
|
|
3005
|
-
return {
|
|
3006
|
-
ok: true,
|
|
3007
|
-
location: {
|
|
3008
|
-
head: cursor,
|
|
3009
|
-
base: tupleBase,
|
|
3010
|
-
node: node.fields[childIndex]
|
|
3011
|
-
}
|
|
3012
|
-
};
|
|
3013
|
-
}
|
|
3014
|
-
/** Descend into a dynamic array element by index. */
|
|
3015
|
-
function descendDynamicArray(node, callData, head, base, childIndex) {
|
|
3016
|
-
const arrayBaseResult = readPointer(callData, head);
|
|
3017
|
-
if (typeof arrayBaseResult !== "number") return {
|
|
3018
|
-
ok: false,
|
|
3019
|
-
code: arrayBaseResult.code
|
|
3020
|
-
};
|
|
3021
|
-
const arrayBase = base + arrayBaseResult;
|
|
3022
|
-
const lengthResult = readPointer(callData, arrayBase);
|
|
3023
|
-
if (typeof lengthResult !== "number") return {
|
|
3024
|
-
ok: false,
|
|
3025
|
-
code: lengthResult.code
|
|
3026
|
-
};
|
|
3027
|
-
if (childIndex >= lengthResult) return {
|
|
3028
|
-
ok: false,
|
|
3029
|
-
code: "ARRAY_INDEX_OUT_OF_BOUNDS"
|
|
3030
|
-
};
|
|
3031
|
-
const headsBase = arrayBase + 32;
|
|
3032
|
-
const elem = node.element;
|
|
3033
|
-
if (elem.isDynamic) return {
|
|
3034
|
-
ok: true,
|
|
3035
|
-
location: {
|
|
3036
|
-
head: headsBase + childIndex * 32,
|
|
3037
|
-
base: headsBase,
|
|
3038
|
-
node: elem
|
|
3039
|
-
}
|
|
3040
|
-
};
|
|
3041
|
-
return {
|
|
3042
|
-
ok: true,
|
|
3043
|
-
location: {
|
|
3044
|
-
head: headsBase + childIndex * elem.staticSize,
|
|
3045
|
-
base: arrayBase,
|
|
3046
|
-
node: elem
|
|
3047
|
-
}
|
|
3048
|
-
};
|
|
3049
|
-
}
|
|
3050
|
-
/** Descend into a static array element by index. */
|
|
3051
|
-
function descendStaticArray(node, callData, head, base, childIndex) {
|
|
3052
|
-
if (childIndex >= node.length) throw new CallciumError("INVALID_PATH", `Static array index ${childIndex} out of range (length ${node.length}).`);
|
|
3053
|
-
const elem = node.element;
|
|
3054
|
-
if (elem.isDynamic) {
|
|
3055
|
-
const ptrResult = readPointer(callData, head);
|
|
3056
|
-
if (typeof ptrResult !== "number") return {
|
|
3057
|
-
ok: false,
|
|
3058
|
-
code: ptrResult.code
|
|
3059
|
-
};
|
|
3060
|
-
const arrayBase = base + ptrResult;
|
|
3061
|
-
return {
|
|
3062
|
-
ok: true,
|
|
3063
|
-
location: {
|
|
3064
|
-
head: arrayBase + childIndex * 32,
|
|
3065
|
-
base: arrayBase,
|
|
3066
|
-
node: elem
|
|
3067
|
-
}
|
|
3068
|
-
};
|
|
3069
|
-
}
|
|
3070
|
-
return {
|
|
3071
|
-
ok: true,
|
|
3072
|
-
location: {
|
|
3073
|
-
head: head + childIndex * elem.staticSize,
|
|
3074
|
-
base,
|
|
3075
|
-
node: elem
|
|
3076
|
-
}
|
|
3077
|
-
};
|
|
3078
|
-
}
|
|
3079
|
-
/**
|
|
3080
|
-
* Resolve a path to a calldata location, stopping at a leaf or quantifier.
|
|
3081
|
-
*
|
|
3082
|
-
* The path is a sequence of BE16 steps. The first step selects the top-level
|
|
3083
|
-
* parameter; subsequent steps descend into tuples and arrays. If a quantifier
|
|
3084
|
-
* step is encountered, returns a QuantifierResult with the array location and
|
|
3085
|
-
* remaining path suffix.
|
|
3086
|
-
*/
|
|
3087
|
-
function locate(tree, callData, pathBytes, baseOffset) {
|
|
3088
|
-
const stepCount = pathBytes.length / 2;
|
|
3089
|
-
if (stepCount === 0) throw new CallciumError("INVALID_PATH", "Path is empty.");
|
|
3090
|
-
const paramIndex = readStep(pathBytes, 0);
|
|
3091
|
-
if (paramIndex >= tree.length) throw new CallciumError("INVALID_PATH", `Param index ${paramIndex} out of range (${tree.length} params).`);
|
|
3092
|
-
let head = baseOffset;
|
|
3093
|
-
for (let i = 0; i < paramIndex; i++) head += headContribution(tree[i]);
|
|
3094
|
-
let base = baseOffset;
|
|
3095
|
-
let node = tree[paramIndex];
|
|
3096
|
-
for (let stepIndex = 1; stepIndex < stepCount; stepIndex++) {
|
|
3097
|
-
const step = readStep(pathBytes, stepIndex);
|
|
3098
|
-
if (isQuantifier(step)) {
|
|
3099
|
-
const remaining = pathBytes.subarray((stepIndex + 1) * 2);
|
|
3100
|
-
for (let remainingStepIndex = 0; remainingStepIndex < remaining.length / 2; remainingStepIndex++) if (isQuantifier(readStep(remaining, remainingStepIndex))) throw new CallciumError("INVALID_QUANTIFIER", "Nested quantifiers are not supported.");
|
|
3101
|
-
return {
|
|
3102
|
-
ok: true,
|
|
3103
|
-
type: "quantifier",
|
|
3104
|
-
quantifier: {
|
|
3105
|
-
quantifier: step,
|
|
3106
|
-
location: {
|
|
3107
|
-
head,
|
|
3108
|
-
base,
|
|
3109
|
-
node
|
|
3110
|
-
},
|
|
3111
|
-
remainingPath: remaining
|
|
3112
|
-
}
|
|
3113
|
-
};
|
|
3114
|
-
}
|
|
3115
|
-
const result = descend(node, callData, head, base, step);
|
|
3116
|
-
if (!result.ok) return {
|
|
3117
|
-
ok: false,
|
|
3118
|
-
code: result.code
|
|
3119
|
-
};
|
|
3120
|
-
head = result.location.head;
|
|
3121
|
-
base = result.location.base;
|
|
3122
|
-
node = result.location.node;
|
|
3123
|
-
}
|
|
3124
|
-
return {
|
|
3125
|
-
ok: true,
|
|
3126
|
-
type: "leaf",
|
|
3127
|
-
location: {
|
|
3128
|
-
head,
|
|
3129
|
-
base,
|
|
3130
|
-
node
|
|
3131
|
-
}
|
|
3132
|
-
};
|
|
3133
|
-
}
|
|
3134
|
-
/**
|
|
3135
|
-
* Build the element-shape fields shared by dynamic- and static-array nodes, branching only on
|
|
3136
|
-
* whether the element itself is dynamic.
|
|
3137
|
-
*/
|
|
3138
|
-
function makeArrayShape(length, elem, dynamicHeadsBase, staticDataOffset, staticCompositeBase) {
|
|
3139
|
-
return elem.isDynamic ? {
|
|
3140
|
-
length,
|
|
3141
|
-
elementNode: elem,
|
|
3142
|
-
elementIsDynamic: true,
|
|
3143
|
-
elementStaticSize: 0,
|
|
3144
|
-
headsBase: dynamicHeadsBase,
|
|
3145
|
-
dataOffset: 0,
|
|
3146
|
-
compositeBase: dynamicHeadsBase
|
|
3147
|
-
} : {
|
|
3148
|
-
length,
|
|
3149
|
-
elementNode: elem,
|
|
3150
|
-
elementIsDynamic: false,
|
|
3151
|
-
elementStaticSize: elem.staticSize,
|
|
3152
|
-
headsBase: 0,
|
|
3153
|
-
dataOffset: staticDataOffset,
|
|
3154
|
-
compositeBase: staticCompositeBase
|
|
3155
|
-
};
|
|
3156
|
-
}
|
|
3157
|
-
/** Extract array shape from a location pointing to an array node. */
|
|
3158
|
-
function arrayShape(callData, location) {
|
|
3159
|
-
const { head, base, node } = location;
|
|
3160
|
-
if (node.type === "dynamicArray") {
|
|
3161
|
-
const arrayBaseResult = readPointer(callData, head);
|
|
3162
|
-
if (typeof arrayBaseResult !== "number") return {
|
|
3163
|
-
ok: false,
|
|
3164
|
-
code: arrayBaseResult.code
|
|
3165
|
-
};
|
|
3166
|
-
const arrayBase = base + arrayBaseResult;
|
|
3167
|
-
const lengthResult = readPointer(callData, arrayBase);
|
|
3168
|
-
if (typeof lengthResult !== "number") return {
|
|
3169
|
-
ok: false,
|
|
3170
|
-
code: lengthResult.code
|
|
3171
|
-
};
|
|
3172
|
-
const headsBase = arrayBase + 32;
|
|
3173
|
-
return {
|
|
3174
|
-
ok: true,
|
|
3175
|
-
shape: makeArrayShape(lengthResult, node.element, headsBase, headsBase, arrayBase)
|
|
3176
|
-
};
|
|
3177
|
-
}
|
|
3178
|
-
if (node.type === "staticArray") {
|
|
3179
|
-
const elem = node.element;
|
|
3180
|
-
let dynamicHeadsBase = 0;
|
|
3181
|
-
if (elem.isDynamic) {
|
|
3182
|
-
const arrayBaseResult = readPointer(callData, head);
|
|
3183
|
-
if (typeof arrayBaseResult !== "number") return {
|
|
3184
|
-
ok: false,
|
|
3185
|
-
code: arrayBaseResult.code
|
|
3186
|
-
};
|
|
3187
|
-
dynamicHeadsBase = base + arrayBaseResult;
|
|
3188
|
-
}
|
|
3189
|
-
return {
|
|
3190
|
-
ok: true,
|
|
3191
|
-
shape: makeArrayShape(node.length, elem, dynamicHeadsBase, head, base)
|
|
3192
|
-
};
|
|
3193
|
-
}
|
|
3194
|
-
throw new CallciumError("INVALID_PATH", `Cannot compute array shape for non-array node (${node.type}).`);
|
|
3195
|
-
}
|
|
3196
|
-
/** Compute the location for element N within a precomputed array shape. */
|
|
3197
|
-
function arrayElementAt(shape, index, callData) {
|
|
3198
|
-
if (index >= shape.length) return {
|
|
3199
|
-
ok: false,
|
|
3200
|
-
code: "ARRAY_INDEX_OUT_OF_BOUNDS"
|
|
3201
|
-
};
|
|
3202
|
-
let head;
|
|
3203
|
-
let base;
|
|
3204
|
-
if (!shape.elementIsDynamic) {
|
|
3205
|
-
head = shape.dataOffset + index * shape.elementStaticSize;
|
|
3206
|
-
base = shape.compositeBase;
|
|
3207
|
-
} else {
|
|
3208
|
-
head = shape.headsBase + index * 32;
|
|
3209
|
-
base = shape.headsBase;
|
|
3210
|
-
}
|
|
3211
|
-
if (head + 32 > callData.length) return {
|
|
3212
|
-
ok: false,
|
|
3213
|
-
code: "CALLDATA_OUT_OF_BOUNDS"
|
|
3214
|
-
};
|
|
3215
|
-
return {
|
|
3216
|
-
ok: true,
|
|
3217
|
-
location: {
|
|
3218
|
-
head,
|
|
3219
|
-
base,
|
|
3220
|
-
node: shape.elementNode
|
|
3221
|
-
}
|
|
3222
|
-
};
|
|
3223
|
-
}
|
|
3224
|
-
/** Load a 32-byte scalar value from a resolved location. */
|
|
3225
|
-
function loadScalar(callData, location) {
|
|
3226
|
-
const result = load32(callData, location.head);
|
|
3227
|
-
if (result instanceof Uint8Array) return {
|
|
3228
|
-
ok: true,
|
|
3229
|
-
value: result
|
|
3230
|
-
};
|
|
3231
|
-
return {
|
|
3232
|
-
ok: false,
|
|
3233
|
-
code: result.code
|
|
3234
|
-
};
|
|
3235
|
-
}
|
|
3236
|
-
/** Load the data offset and logical length for a dynamic type (bytes, string, or dynamic array). */
|
|
3237
|
-
function loadSlice(callData, location) {
|
|
3238
|
-
const ptrResult = readPointer(callData, location.head);
|
|
3239
|
-
if (typeof ptrResult !== "number") return {
|
|
3240
|
-
ok: false,
|
|
3241
|
-
code: ptrResult.code
|
|
3242
|
-
};
|
|
3243
|
-
const dataStart = location.base + ptrResult;
|
|
3244
|
-
const lengthResult = readPointer(callData, dataStart);
|
|
3245
|
-
if (typeof lengthResult !== "number") return {
|
|
3246
|
-
ok: false,
|
|
3247
|
-
code: lengthResult.code
|
|
3248
|
-
};
|
|
3249
|
-
const dataOffset = dataStart + 32;
|
|
3250
|
-
const node = location.node;
|
|
3251
|
-
const stride = node.type === "dynamicArray" ? node.element.isDynamic ? 32 : node.element.staticSize : 1;
|
|
3252
|
-
if (dataOffset > callData.length || lengthResult > Math.floor((callData.length - dataOffset) / stride)) return {
|
|
3253
|
-
ok: false,
|
|
3254
|
-
code: "CALLDATA_OUT_OF_BOUNDS"
|
|
3255
|
-
};
|
|
3256
|
-
return {
|
|
3257
|
-
ok: true,
|
|
3258
|
-
dataOffset,
|
|
3259
|
-
length: lengthResult
|
|
3260
|
-
};
|
|
3261
|
-
}
|
|
3262
|
-
/** Continue navigating through additional path steps from a resolved location. */
|
|
3263
|
-
function descendPath(callData, location, pathBytes) {
|
|
3264
|
-
const stepCount = pathBytes.length / 2;
|
|
3265
|
-
let { head, base, node } = location;
|
|
3266
|
-
for (let stepIndex = 0; stepIndex < stepCount; stepIndex++) {
|
|
3267
|
-
const step = readStep(pathBytes, stepIndex);
|
|
3268
|
-
const result = descend(node, callData, head, base, step);
|
|
3269
|
-
if (!result.ok) return {
|
|
3270
|
-
ok: false,
|
|
3271
|
-
code: result.code
|
|
3272
|
-
};
|
|
3273
|
-
head = result.location.head;
|
|
3274
|
-
base = result.location.base;
|
|
3275
|
-
node = result.location.node;
|
|
3276
|
-
}
|
|
3277
|
-
return {
|
|
3278
|
-
ok: true,
|
|
3279
|
-
location: {
|
|
3280
|
-
head,
|
|
3281
|
-
base,
|
|
3282
|
-
node
|
|
3283
|
-
}
|
|
3284
|
-
};
|
|
3285
|
-
}
|
|
3286
2763
|
//#endregion
|
|
3287
2764
|
//#region src/policy-enforcer.ts
|
|
3288
|
-
/** Violations that abort evaluation instead of failing only their group (spec §10.3). */
|
|
3289
2765
|
const ABORT_VIOLATION_CODES = new Set([
|
|
3290
2766
|
"CALLDATA_OUT_OF_BOUNDS",
|
|
3291
2767
|
"ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
2768
|
+
"NON_CANONICAL_VALUE",
|
|
3292
2769
|
"QUANTIFIER_LIMIT_EXCEEDED"
|
|
3293
2770
|
]);
|
|
3294
|
-
/** Convert a hex address to a 256-bit bigint (zero-padded to 32 bytes). */
|
|
3295
2771
|
function addressToBigInt(hex) {
|
|
3296
2772
|
const bytes = hexToBytes(hex);
|
|
3297
2773
|
const padded = new Uint8Array(32);
|
|
3298
2774
|
padded.set(bytes, 32 - bytes.length);
|
|
3299
2775
|
return toBigInt(padded, 0);
|
|
3300
2776
|
}
|
|
3301
|
-
/**
|
|
3302
|
-
* Walk a descriptor subtree through path steps and return the terminal leaf type code.
|
|
3303
|
-
*
|
|
3304
|
-
* Used only for violation metadata. Valid policies have structurally valid
|
|
3305
|
-
* quantifier suffixes (enforced by `PolicyValidator`); if a hand-crafted blob
|
|
3306
|
-
* violates that invariant, return the last resolvable node's type code as
|
|
3307
|
-
* best-effort diagnostic context.
|
|
3308
|
-
*/
|
|
3309
|
-
function resolveLeafTypeCode(elementNode, remainingPath) {
|
|
3310
|
-
let node = elementNode;
|
|
3311
|
-
const stepCount = remainingPath.length / 2;
|
|
3312
|
-
for (let step = 0; step < stepCount; step++) {
|
|
3313
|
-
const childIndex = readU16(remainingPath, step * 2);
|
|
3314
|
-
if (node.type === "tuple") {
|
|
3315
|
-
const field = node.fields[childIndex];
|
|
3316
|
-
if (!field) break;
|
|
3317
|
-
node = field;
|
|
3318
|
-
} else if (node.type === "staticArray" || node.type === "dynamicArray") node = node.element;
|
|
3319
|
-
else break;
|
|
3320
|
-
}
|
|
3321
|
-
return node.typeCode;
|
|
3322
|
-
}
|
|
3323
|
-
/**
|
|
3324
|
-
* Check a policy against ABI-encoded call data without throwing on violations.
|
|
3325
|
-
* @param policy - Binary policy blob as 0x-prefixed hex string.
|
|
3326
|
-
* @param callData - ABI-encoded call data as 0x-prefixed hex string.
|
|
3327
|
-
* @param context - Optional execution context for context-scoped rules.
|
|
3328
|
-
* @returns Pass with matched group index, or fail with one violation per evaluated failing group.
|
|
3329
|
-
* @throws {CallciumError} If the policy blob is structurally malformed.
|
|
3330
|
-
*/
|
|
3331
2777
|
function check(policy, callData, context) {
|
|
3332
|
-
const { policy: decoded,
|
|
2778
|
+
const { policy: decoded, data: policyBytes } = decodePolicy(policy);
|
|
3333
2779
|
const callDataBytes = hexToBytes(callData);
|
|
3334
2780
|
if (!decoded.isSelectorless) {
|
|
3335
2781
|
if (callDataBytes.length < 4) return {
|
|
@@ -3354,7 +2800,7 @@ function check(policy, callData, context) {
|
|
|
3354
2800
|
let groupFailed = false;
|
|
3355
2801
|
for (let ruleIndex = 0; ruleIndex < group.rules.length; ruleIndex++) {
|
|
3356
2802
|
const rule = group.rules[ruleIndex];
|
|
3357
|
-
const violation = evaluateRule(rule,
|
|
2803
|
+
const violation = evaluateRule(rule, policyBytes, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
|
|
3358
2804
|
if (violation !== null) {
|
|
3359
2805
|
allViolations.push(violation);
|
|
3360
2806
|
if (ABORT_VIOLATION_CODES.has(violation.code)) return {
|
|
@@ -3375,153 +2821,183 @@ function check(policy, callData, context) {
|
|
|
3375
2821
|
violations: allViolations
|
|
3376
2822
|
};
|
|
3377
2823
|
}
|
|
3378
|
-
/**
|
|
3379
|
-
* Enforce a policy against ABI-encoded call data, throwing on violation.
|
|
3380
|
-
* @param policy - Binary policy blob as 0x-prefixed hex string.
|
|
3381
|
-
* @param callData - ABI-encoded call data as 0x-prefixed hex string.
|
|
3382
|
-
* @param context - Optional execution context for context-scoped rules.
|
|
3383
|
-
* @throws {PolicyViolationError} If the policy rejects the call data.
|
|
3384
|
-
* @throws {CallciumError} If the policy blob is structurally malformed.
|
|
3385
|
-
*/
|
|
3386
2824
|
function enforce(policy, callData, context) {
|
|
3387
2825
|
const result = check(policy, callData, context);
|
|
3388
2826
|
if (!result.ok) throw new PolicyViolationError(result.violations);
|
|
3389
2827
|
}
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
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) {
|
|
3394
2832
|
const opCode = rule.opCode.value;
|
|
3395
|
-
const operandData =
|
|
3396
|
-
if (scope === Scope.CONTEXT) return evaluateContextRule(
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
group: groupIndex,
|
|
3400
|
-
rule: ruleIndex,
|
|
3401
|
-
code: locResult.code,
|
|
3402
|
-
scope: Scope.CALLDATA,
|
|
3403
|
-
path: rule.path.value,
|
|
3404
|
-
opCode,
|
|
3405
|
-
operandData: rule.data.value
|
|
3406
|
-
};
|
|
3407
|
-
if (locResult.type === "quantifier") return evaluateQuantifier(locResult.quantifier, callDataBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value);
|
|
3408
|
-
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);
|
|
3409
2837
|
}
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
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;
|
|
3439
2876
|
}
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
if (isLengthOp(opCode)) {
|
|
3450
|
-
let valueLength;
|
|
3451
|
-
if (node.isDynamic) {
|
|
3452
|
-
const lengthResult = loadSlice(callDataBytes, location);
|
|
3453
|
-
if (!lengthResult.ok) return { error: lengthResult.code };
|
|
3454
|
-
valueLength = lengthResult.length;
|
|
3455
|
-
} 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" };
|
|
3456
2886
|
return {
|
|
3457
|
-
passed: applyOperator(opCode, 0n,
|
|
3458
|
-
|
|
2887
|
+
passed: applyOperator(opCode, 0n, length, operandData, typeCode),
|
|
2888
|
+
value: BigInt(length)
|
|
3459
2889
|
};
|
|
3460
2890
|
}
|
|
3461
|
-
if (
|
|
3462
|
-
const
|
|
3463
|
-
if (!
|
|
3464
|
-
const 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 {
|
|
2896
|
+
error: "NON_CANONICAL_VALUE",
|
|
2897
|
+
value
|
|
2898
|
+
};
|
|
3465
2899
|
return {
|
|
3466
|
-
passed: applyOperator(opCode, value, 32, operandData,
|
|
3467
|
-
|
|
2900
|
+
passed: applyOperator(opCode, value, 32, operandData, typeCode),
|
|
2901
|
+
value
|
|
3468
2902
|
};
|
|
3469
2903
|
}
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
group: groupIndex,
|
|
3476
|
-
rule: ruleIndex,
|
|
3477
|
-
code: result.error,
|
|
2904
|
+
function navigationViolation(frame, code, elementIndex) {
|
|
2905
|
+
return {
|
|
2906
|
+
group: frame.group,
|
|
2907
|
+
rule: frame.rule,
|
|
2908
|
+
code,
|
|
3478
2909
|
scope: Scope.CALLDATA,
|
|
3479
|
-
path:
|
|
3480
|
-
opCode,
|
|
3481
|
-
operandData: bytesToHex(operandData),
|
|
3482
|
-
typeCode
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
2910
|
+
path: frame.path,
|
|
2911
|
+
opCode: frame.opCode,
|
|
2912
|
+
operandData: bytesToHex(frame.operandData),
|
|
2913
|
+
typeCode: frame.typeCode,
|
|
2914
|
+
...elementIndex !== void 0 && { elementIndex }
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
function targetViolation(frame, result, elementIndex) {
|
|
2918
|
+
if ("error" in result) {
|
|
2919
|
+
if (result.error === "NON_CANONICAL_VALUE") return {
|
|
2920
|
+
group: frame.group,
|
|
2921
|
+
rule: frame.rule,
|
|
2922
|
+
code: result.error,
|
|
2923
|
+
scope: Scope.CALLDATA,
|
|
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 }
|
|
2930
|
+
};
|
|
2931
|
+
return navigationViolation(frame, result.error, elementIndex);
|
|
2932
|
+
}
|
|
2933
|
+
if (result.passed) return null;
|
|
2934
|
+
return {
|
|
2935
|
+
group: frame.group,
|
|
2936
|
+
rule: frame.rule,
|
|
3487
2937
|
code: "VALUE_MISMATCH",
|
|
3488
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,
|
|
3489
2957
|
path: pathHex,
|
|
3490
2958
|
opCode,
|
|
3491
|
-
operandData
|
|
3492
|
-
typeCode
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
return
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
function
|
|
3499
|
-
const
|
|
3500
|
-
const
|
|
3501
|
-
const
|
|
3502
|
-
const
|
|
3503
|
-
const
|
|
3504
|
-
|
|
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 = {
|
|
3505
2973
|
group: groupIndex,
|
|
3506
2974
|
rule: ruleIndex,
|
|
3507
|
-
code: shapeResult.code,
|
|
3508
|
-
scope: Scope.CALLDATA,
|
|
3509
2975
|
path: pathHex,
|
|
3510
2976
|
opCode,
|
|
3511
|
-
operandData
|
|
3512
|
-
typeCode:
|
|
3513
|
-
};
|
|
3514
|
-
const
|
|
3515
|
-
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 {
|
|
3516
2991
|
group: groupIndex,
|
|
3517
2992
|
rule: ruleIndex,
|
|
3518
2993
|
code: "QUANTIFIER_LIMIT_EXCEEDED",
|
|
3519
2994
|
scope: Scope.CALLDATA,
|
|
3520
2995
|
path: pathHex,
|
|
3521
|
-
resolvedValue: bigintToHex(BigInt(
|
|
2996
|
+
resolvedValue: bigintToHex(BigInt(count))
|
|
3522
2997
|
};
|
|
3523
|
-
|
|
3524
|
-
|
|
2998
|
+
const isUniversal = kind === PolicyFormat.HINT_KIND_ALL;
|
|
2999
|
+
if (count === 0) {
|
|
3000
|
+
if (isUniversal) return null;
|
|
3525
3001
|
return {
|
|
3526
3002
|
group: groupIndex,
|
|
3527
3003
|
rule: ruleIndex,
|
|
@@ -3530,72 +3006,28 @@ function evaluateQuantifier(qResult, callDataBytes, opCode, operandData, groupIn
|
|
|
3530
3006
|
path: pathHex
|
|
3531
3007
|
};
|
|
3532
3008
|
}
|
|
3533
|
-
const
|
|
3534
|
-
const
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
path: pathHex,
|
|
3544
|
-
opCode,
|
|
3545
|
-
operandData: operandHex,
|
|
3546
|
-
typeCode: leafTypeCode,
|
|
3547
|
-
elementIndex: elemIndex
|
|
3548
|
-
};
|
|
3549
|
-
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;
|
|
3550
3019
|
}
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
if (isUniversal) return {
|
|
3557
|
-
group: groupIndex,
|
|
3558
|
-
rule: ruleIndex,
|
|
3559
|
-
code: navResult.code,
|
|
3560
|
-
scope: Scope.CALLDATA,
|
|
3561
|
-
path: pathHex,
|
|
3562
|
-
opCode,
|
|
3563
|
-
operandData: operandHex,
|
|
3564
|
-
typeCode: leafTypeCode,
|
|
3565
|
-
elementIndex: elemIndex
|
|
3566
|
-
};
|
|
3567
|
-
continue;
|
|
3568
|
-
}
|
|
3569
|
-
leafLocation = navResult.location;
|
|
3570
|
-
} else leafLocation = elemLocation;
|
|
3571
|
-
const applied = applyLeafOperator(callDataBytes, leafLocation, opCode, operandData);
|
|
3572
|
-
if ("error" in applied) {
|
|
3573
|
-
if (isUniversal) return {
|
|
3574
|
-
group: groupIndex,
|
|
3575
|
-
rule: ruleIndex,
|
|
3576
|
-
code: applied.error,
|
|
3577
|
-
scope: Scope.CALLDATA,
|
|
3578
|
-
path: pathHex,
|
|
3579
|
-
opCode,
|
|
3580
|
-
operandData: operandHex,
|
|
3581
|
-
typeCode: leafTypeCode,
|
|
3582
|
-
elementIndex: elemIndex
|
|
3583
|
-
};
|
|
3584
|
-
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;
|
|
3585
3025
|
}
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
path: pathHex,
|
|
3592
|
-
opCode,
|
|
3593
|
-
operandData: operandHex,
|
|
3594
|
-
typeCode: leafTypeCode,
|
|
3595
|
-
resolvedValue: applied.resolvedValue,
|
|
3596
|
-
elementIndex: elemIndex
|
|
3597
|
-
};
|
|
3598
|
-
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);
|
|
3599
3031
|
}
|
|
3600
3032
|
if (isUniversal) return null;
|
|
3601
3033
|
return {
|
|
@@ -3605,16 +3037,44 @@ function evaluateQuantifier(qResult, callDataBytes, opCode, operandData, groupIn
|
|
|
3605
3037
|
scope: Scope.CALLDATA,
|
|
3606
3038
|
path: pathHex,
|
|
3607
3039
|
opCode,
|
|
3608
|
-
operandData:
|
|
3609
|
-
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)
|
|
3610
3070
|
};
|
|
3071
|
+
return null;
|
|
3611
3072
|
}
|
|
3612
|
-
/** Check and enforce policies against ABI-encoded call data. */
|
|
3613
3073
|
const PolicyEnforcer = {
|
|
3614
3074
|
check,
|
|
3615
3075
|
enforce
|
|
3616
3076
|
};
|
|
3617
3077
|
//#endregion
|
|
3618
|
-
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 };
|
|
3619
3079
|
|
|
3620
3080
|
//# sourceMappingURL=index.js.map
|