@callcium/sdk 0.9.0 → 0.11.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 +202 -145
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +763 -667
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/bytes.ts +9 -9
- package/src/calldata-reader.ts +14 -1
- package/src/constants.ts +85 -181
- package/src/constraint.ts +131 -94
- package/src/descriptor-coder.ts +53 -84
- package/src/descriptor.ts +65 -36
- package/src/errors.ts +12 -5
- package/src/index.ts +8 -17
- package/src/operators.ts +147 -11
- package/src/path.ts +80 -0
- package/src/policy-builder.ts +107 -44
- package/src/policy-coder.ts +102 -77
- package/src/policy-enforcer.ts +98 -128
- package/src/policy-validator.ts +70 -56
- package/src/signature.ts +21 -19
- package/src/type-desc.ts +15 -15
- package/src/types.ts +70 -3
- package/src/validation-issue.ts +4 -4
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
2
|
+
import { utf8ToBytes } from "@noble/hashes/utils.js";
|
|
2
3
|
//#region src/errors.ts
|
|
3
|
-
var CallciumError = class
|
|
4
|
+
var CallciumError = class extends Error {
|
|
4
5
|
code;
|
|
5
6
|
offset;
|
|
6
7
|
constructor(code, message, offset) {
|
|
@@ -8,25 +9,23 @@ var CallciumError = class CallciumError extends Error {
|
|
|
8
9
|
super(`${prefix}${message}`);
|
|
9
10
|
this.name = "CallciumError";
|
|
10
11
|
this.code = code;
|
|
11
|
-
|
|
12
|
+
if (offset !== void 0) this.offset = offset;
|
|
12
13
|
}
|
|
13
14
|
};
|
|
14
|
-
var ValidationError = class
|
|
15
|
+
var ValidationError = class extends CallciumError {
|
|
15
16
|
issues;
|
|
16
17
|
constructor(issues) {
|
|
17
18
|
super("VALIDATION_ERROR", issues[0]?.message ?? "Policy validation failed");
|
|
18
19
|
this.name = "ValidationError";
|
|
19
20
|
this.issues = issues;
|
|
20
|
-
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
-
var PolicyViolationError = class
|
|
23
|
+
var PolicyViolationError = class extends Error {
|
|
24
24
|
violations;
|
|
25
25
|
constructor(violations) {
|
|
26
26
|
super(formatDiagnostic(violations[0]));
|
|
27
27
|
this.name = "PolicyViolationError";
|
|
28
28
|
this.violations = violations;
|
|
29
|
-
Object.setPrototypeOf(this, PolicyViolationError.prototype);
|
|
30
29
|
}
|
|
31
30
|
};
|
|
32
31
|
function formatDiagnostic(violation) {
|
|
@@ -36,29 +35,29 @@ function formatDiagnostic(violation) {
|
|
|
36
35
|
}
|
|
37
36
|
//#endregion
|
|
38
37
|
//#region src/bytes.ts
|
|
39
|
-
const
|
|
38
|
+
const hexBodyPattern = /^[0-9a-fA-F]*$/;
|
|
40
39
|
const HEX_DIGITS = "0123456789abcdef";
|
|
41
|
-
const
|
|
40
|
+
const nibble = new Int8Array(128).fill(-1);
|
|
42
41
|
for (let value = 0; value < 16; value++) {
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
nibble[HEX_DIGITS.charCodeAt(value)] = value;
|
|
43
|
+
nibble[HEX_DIGITS.toUpperCase().charCodeAt(value)] = value;
|
|
45
44
|
}
|
|
46
45
|
function hexToBytes(hex) {
|
|
47
46
|
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
48
47
|
if (clean.length % 2 !== 0) throw new CallciumError("INVALID_HEX", "Odd-length hex string");
|
|
49
48
|
const bytes = new Uint8Array(clean.length / 2);
|
|
50
49
|
for (let i = 0; i < bytes.length; i++) {
|
|
51
|
-
const high =
|
|
52
|
-
const low =
|
|
50
|
+
const high = nibble[clean.charCodeAt(i * 2)] ?? -1;
|
|
51
|
+
const low = nibble[clean.charCodeAt(i * 2 + 1)] ?? -1;
|
|
53
52
|
if (high < 0 || low < 0) throw new CallciumError("INVALID_HEX", "Invalid hex characters");
|
|
54
53
|
bytes[i] = high << 4 | low;
|
|
55
54
|
}
|
|
56
55
|
return bytes;
|
|
57
56
|
}
|
|
58
|
-
const
|
|
57
|
+
const hexPairs = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
59
58
|
function bytesToHex(bytes) {
|
|
60
59
|
let body = "";
|
|
61
|
-
for (let i = 0; i < bytes.length; i++) body +=
|
|
60
|
+
for (let i = 0; i < bytes.length; i++) body += hexPairs[bytes[i]];
|
|
62
61
|
return `0x${body}`;
|
|
63
62
|
}
|
|
64
63
|
function toHex(data, start, end) {
|
|
@@ -94,7 +93,7 @@ function bigintToHex(value) {
|
|
|
94
93
|
function toAddress(hex) {
|
|
95
94
|
const body = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
96
95
|
if (body.length !== 40) throw new CallciumError("INVALID_HEX", `Invalid address length: expected 40 hex chars, got ${body.length}`);
|
|
97
|
-
if (!
|
|
96
|
+
if (!hexBodyPattern.test(body)) throw new CallciumError("INVALID_HEX", "Invalid hex characters in address");
|
|
98
97
|
return `0x${body}`;
|
|
99
98
|
}
|
|
100
99
|
//#endregion
|
|
@@ -117,6 +116,12 @@ const DescriptorFormat = {
|
|
|
117
116
|
MAX_PARAMS: 255,
|
|
118
117
|
MAX_NESTING_DEPTH: 64
|
|
119
118
|
};
|
|
119
|
+
const RULE_DATALENGTH_SIZE = 2;
|
|
120
|
+
const PATH_STEP_SIZE = 2;
|
|
121
|
+
const RULE_FIXED_OVERHEAD = 7;
|
|
122
|
+
function fieldMax(byteWidth) {
|
|
123
|
+
return 256 ** byteWidth - 1;
|
|
124
|
+
}
|
|
120
125
|
const PolicyFormat = {
|
|
121
126
|
VERSION: 2,
|
|
122
127
|
VERSION_MASK: 15,
|
|
@@ -136,11 +141,12 @@ const PolicyFormat = {
|
|
|
136
141
|
RULE_SCOPE_OFFSET: 2,
|
|
137
142
|
RULE_DEPTH_OFFSET: 3,
|
|
138
143
|
RULE_PATH_OFFSET: 4,
|
|
139
|
-
PATH_STEP_SIZE
|
|
144
|
+
PATH_STEP_SIZE,
|
|
145
|
+
PATH_STEP_MAX: fieldMax(PATH_STEP_SIZE),
|
|
140
146
|
RULE_OPCODE_SIZE: 1,
|
|
141
|
-
RULE_DATALENGTH_SIZE
|
|
142
|
-
RULE_FIXED_OVERHEAD
|
|
143
|
-
RULE_MIN_SIZE:
|
|
147
|
+
RULE_DATALENGTH_SIZE,
|
|
148
|
+
RULE_FIXED_OVERHEAD,
|
|
149
|
+
RULE_MIN_SIZE: RULE_FIXED_OVERHEAD + PATH_STEP_SIZE,
|
|
144
150
|
HINT_HEADER_SIZE: 1,
|
|
145
151
|
HINT_HOP_SIZE: 8,
|
|
146
152
|
HINT_HOP_INDEX_OFFSET: 4,
|
|
@@ -163,10 +169,17 @@ const PolicyFormat = {
|
|
|
163
169
|
HINT_META_ELEM_DYNAMIC: 32768,
|
|
164
170
|
HINT_META_DYNAMIC_ARRAY: 16384,
|
|
165
171
|
HINT_META_RESERVED_MASK: 12288,
|
|
166
|
-
HINT_META_STRIDE_MASK: 4095
|
|
172
|
+
HINT_META_STRIDE_MASK: 4095,
|
|
173
|
+
MAX_PATH_DEPTH: 32,
|
|
174
|
+
MAX_QUANTIFIED_ARRAY_LENGTH: 256,
|
|
175
|
+
MAX_SET_MEMBERS: Math.floor(fieldMax(RULE_DATALENGTH_SIZE) / 32)
|
|
167
176
|
};
|
|
177
|
+
function formatCode(code, byteWidth) {
|
|
178
|
+
if (!Number.isInteger(code) || code < 0 || code > fieldMax(byteWidth)) return String(code);
|
|
179
|
+
return `0x${code.toString(16).padStart(byteWidth * 2, "0")}`;
|
|
180
|
+
}
|
|
168
181
|
function buildCodeMap(table) {
|
|
169
|
-
return Object.fromEntries(table.map((
|
|
182
|
+
return Object.fromEntries(table.map((entry) => [entry.key, entry.code]));
|
|
170
183
|
}
|
|
171
184
|
const SCOPE_TABLE = [{
|
|
172
185
|
key: "CONTEXT",
|
|
@@ -178,7 +191,7 @@ const SCOPE_TABLE = [{
|
|
|
178
191
|
label: "calldata"
|
|
179
192
|
}];
|
|
180
193
|
const Scope = buildCodeMap(SCOPE_TABLE);
|
|
181
|
-
const scopeByCode = new Map(SCOPE_TABLE.map((
|
|
194
|
+
const scopeByCode = new Map(SCOPE_TABLE.map((entry) => [entry.code, { label: entry.label }]));
|
|
182
195
|
function lookupScope(code) {
|
|
183
196
|
const info = scopeByCode.get(code);
|
|
184
197
|
if (!info) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${code}`);
|
|
@@ -200,83 +213,79 @@ const TypeCode = {
|
|
|
200
213
|
DYNAMIC_ARRAY: 129,
|
|
201
214
|
TUPLE: 144
|
|
202
215
|
};
|
|
203
|
-
const
|
|
216
|
+
const CTX_PROPERTY_TABLE = [
|
|
204
217
|
{
|
|
205
218
|
key: "MSG_SENDER",
|
|
206
219
|
code: 0,
|
|
207
220
|
label: "msg.sender",
|
|
221
|
+
contextKey: "msgSender",
|
|
208
222
|
typeCode: TypeCode.ADDRESS
|
|
209
223
|
},
|
|
210
224
|
{
|
|
211
225
|
key: "MSG_VALUE",
|
|
212
226
|
code: 1,
|
|
213
227
|
label: "msg.value",
|
|
228
|
+
contextKey: "msgValue",
|
|
214
229
|
typeCode: TypeCode.UINT_MAX
|
|
215
230
|
},
|
|
216
231
|
{
|
|
217
232
|
key: "BLOCK_TIMESTAMP",
|
|
218
233
|
code: 2,
|
|
219
234
|
label: "block.timestamp",
|
|
235
|
+
contextKey: "blockTimestamp",
|
|
220
236
|
typeCode: TypeCode.UINT_MAX
|
|
221
237
|
},
|
|
222
238
|
{
|
|
223
239
|
key: "BLOCK_NUMBER",
|
|
224
240
|
code: 3,
|
|
225
241
|
label: "block.number",
|
|
242
|
+
contextKey: "blockNumber",
|
|
226
243
|
typeCode: TypeCode.UINT_MAX
|
|
227
244
|
},
|
|
228
245
|
{
|
|
229
246
|
key: "CHAIN_ID",
|
|
230
247
|
code: 4,
|
|
231
248
|
label: "block.chainid",
|
|
249
|
+
contextKey: "chainId",
|
|
232
250
|
typeCode: TypeCode.UINT_MAX
|
|
233
251
|
},
|
|
234
252
|
{
|
|
235
253
|
key: "TX_ORIGIN",
|
|
236
254
|
code: 5,
|
|
237
255
|
label: "tx.origin",
|
|
256
|
+
contextKey: "txOrigin",
|
|
238
257
|
typeCode: TypeCode.ADDRESS
|
|
239
258
|
},
|
|
240
259
|
{
|
|
241
260
|
key: "BASE_FEE",
|
|
242
261
|
code: 6,
|
|
243
262
|
label: "block.basefee",
|
|
263
|
+
contextKey: "baseFee",
|
|
244
264
|
typeCode: TypeCode.UINT_MAX
|
|
245
265
|
},
|
|
246
266
|
{
|
|
247
267
|
key: "GAS_PRICE",
|
|
248
268
|
code: 7,
|
|
249
269
|
label: "tx.gasprice",
|
|
270
|
+
contextKey: "gasPrice",
|
|
250
271
|
typeCode: TypeCode.UINT_MAX
|
|
251
272
|
}
|
|
252
273
|
];
|
|
253
|
-
const ContextProperty = buildCodeMap(
|
|
254
|
-
const MAX_CONTEXT_PROPERTY_ID = Math.max(...
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
BLOCK_NUMBER: "blockNumber",
|
|
260
|
-
CHAIN_ID: "chainId",
|
|
261
|
-
TX_ORIGIN: "txOrigin",
|
|
262
|
-
BASE_FEE: "baseFee",
|
|
263
|
-
GAS_PRICE: "gasPrice"
|
|
264
|
-
};
|
|
265
|
-
const ctxPropByCode = new Map(CTX_PROP_TABLE.map((e) => [e.code, {
|
|
266
|
-
label: e.label,
|
|
267
|
-
contextKey: CTX_KEY_MAP[e.key],
|
|
268
|
-
typeCode: e.typeCode
|
|
274
|
+
const ContextProperty = buildCodeMap(CTX_PROPERTY_TABLE);
|
|
275
|
+
const MAX_CONTEXT_PROPERTY_ID = Math.max(...CTX_PROPERTY_TABLE.map((entry) => entry.code));
|
|
276
|
+
const ctxPropertyByCode = new Map(CTX_PROPERTY_TABLE.map((entry) => [entry.code, {
|
|
277
|
+
label: entry.label,
|
|
278
|
+
contextKey: entry.contextKey,
|
|
279
|
+
typeCode: entry.typeCode
|
|
269
280
|
}]));
|
|
281
|
+
function findContextProperty(code) {
|
|
282
|
+
return ctxPropertyByCode.get(code);
|
|
283
|
+
}
|
|
270
284
|
function lookupContextProperty(code) {
|
|
271
|
-
const info =
|
|
272
|
-
if (!info) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ${code}`);
|
|
285
|
+
const info = ctxPropertyByCode.get(code);
|
|
286
|
+
if (!info) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ID ${formatCode(code, PATH_STEP_SIZE)}`);
|
|
273
287
|
return info;
|
|
274
288
|
}
|
|
275
|
-
const Limits = {
|
|
276
|
-
MAX_PATH_DEPTH: 32,
|
|
277
|
-
MAX_QUANTIFIED_ARRAY_LENGTH: 256,
|
|
278
|
-
MAX_SET_MEMBERS: Math.floor((256 ** PolicyFormat.RULE_DATALENGTH_SIZE - 1) / 32)
|
|
279
|
-
};
|
|
280
289
|
const OP_TABLE = [
|
|
281
290
|
{
|
|
282
291
|
key: "EQ",
|
|
@@ -385,23 +394,21 @@ const Op = {
|
|
|
385
394
|
...buildCodeMap(OP_TABLE),
|
|
386
395
|
NOT: 128
|
|
387
396
|
};
|
|
388
|
-
const opByCode = new Map(OP_TABLE.map((
|
|
389
|
-
label:
|
|
390
|
-
operands:
|
|
397
|
+
const opByCode = new Map(OP_TABLE.map((entry) => [entry.code, {
|
|
398
|
+
label: entry.label,
|
|
399
|
+
operands: entry.operands
|
|
391
400
|
}]));
|
|
392
401
|
function lookupOp(code) {
|
|
393
402
|
const base = code & ~Op.NOT;
|
|
394
403
|
const info = opByCode.get(base);
|
|
395
|
-
if (!info) throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code
|
|
404
|
+
if (!info) throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code ${formatCode(base, PolicyFormat.RULE_OPCODE_SIZE)}`);
|
|
396
405
|
return info;
|
|
397
406
|
}
|
|
398
|
-
function
|
|
399
|
-
|
|
400
|
-
if (operands === "single") return dataLength === 32;
|
|
401
|
-
if (operands === "range") return dataLength === 64;
|
|
402
|
-
if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
|
|
403
|
-
return false;
|
|
407
|
+
function operandsOf(opBase) {
|
|
408
|
+
return opByCode.get(opBase)?.operands;
|
|
404
409
|
}
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/path.ts
|
|
405
412
|
const QUANTIFIER_TABLE = [{
|
|
406
413
|
key: "ALL",
|
|
407
414
|
code: 65535,
|
|
@@ -412,23 +419,347 @@ const QUANTIFIER_TABLE = [{
|
|
|
412
419
|
label: "any"
|
|
413
420
|
}];
|
|
414
421
|
const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
|
|
415
|
-
const quantifierByCode = new Map(QUANTIFIER_TABLE.map((
|
|
422
|
+
const quantifierByCode = new Map(QUANTIFIER_TABLE.map((entry) => [entry.code, { label: entry.label }]));
|
|
416
423
|
function isQuantifier(step) {
|
|
417
424
|
return step >= Quantifier.ANY;
|
|
418
425
|
}
|
|
419
426
|
function lookupQuantifier(code) {
|
|
420
427
|
const info = quantifierByCode.get(code);
|
|
421
|
-
if (!info) throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step
|
|
428
|
+
if (!info) throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step ${formatCode(code, PolicyFormat.PATH_STEP_SIZE)}`);
|
|
422
429
|
return info;
|
|
423
430
|
}
|
|
431
|
+
function encodePath(steps) {
|
|
432
|
+
const buffer = new Uint8Array(steps.length * PolicyFormat.PATH_STEP_SIZE);
|
|
433
|
+
for (let i = 0; i < steps.length; i++) {
|
|
434
|
+
const step = steps[i];
|
|
435
|
+
if (!Number.isInteger(step) || step < 0 || step > PolicyFormat.PATH_STEP_MAX) throw new CallciumError("PATH_STEP_OVERFLOW", `Path step ${step} is outside the path step field`);
|
|
436
|
+
writeBE16(buffer, i * PolicyFormat.PATH_STEP_SIZE, step);
|
|
437
|
+
}
|
|
438
|
+
return bytesToHex(buffer);
|
|
439
|
+
}
|
|
440
|
+
function parsePathSteps(path) {
|
|
441
|
+
const bytes = hexToBytes(path);
|
|
442
|
+
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`);
|
|
443
|
+
const steps = [];
|
|
444
|
+
for (let offset = 0; offset < bytes.length; offset += PolicyFormat.PATH_STEP_SIZE) steps.push(readU16(bytes, offset));
|
|
445
|
+
return steps;
|
|
446
|
+
}
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/constraint.ts
|
|
449
|
+
const OPERAND_MIN = -(2n ** 255n);
|
|
450
|
+
const OPERAND_MAX = 2n ** 256n - 1n;
|
|
451
|
+
function checkOperandRange(value) {
|
|
452
|
+
if (value < OPERAND_MIN || value > OPERAND_MAX) throw new CallciumError("OPERAND_OVERFLOW", `Operand ${value} is outside the range a 32-byte word represents`);
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
function addressBody(value) {
|
|
456
|
+
return toAddress(value).slice(2);
|
|
457
|
+
}
|
|
458
|
+
function encodeWord(value) {
|
|
459
|
+
const word = new Uint8Array(32);
|
|
460
|
+
if (typeof value === "boolean") {
|
|
461
|
+
word[31] = value ? 1 : 0;
|
|
462
|
+
return word;
|
|
463
|
+
}
|
|
464
|
+
if (typeof value === "string") {
|
|
465
|
+
word.set(hexToBytes(addressBody(value)), 12);
|
|
466
|
+
return word;
|
|
467
|
+
}
|
|
468
|
+
let bigValue = checkOperandRange(value);
|
|
469
|
+
for (let i = 31; i >= 0; i--) {
|
|
470
|
+
word[i] = Number(bigValue & 255n);
|
|
471
|
+
bigValue >>= 8n;
|
|
472
|
+
}
|
|
473
|
+
return word;
|
|
474
|
+
}
|
|
475
|
+
function singleOp(opCode, value) {
|
|
476
|
+
const buffer = new Uint8Array(33);
|
|
477
|
+
buffer[0] = opCode;
|
|
478
|
+
buffer.set(encodeWord(value), 1);
|
|
479
|
+
return bytesToHex(buffer);
|
|
480
|
+
}
|
|
481
|
+
function rangeOp(opCode, min, max) {
|
|
482
|
+
const minValue = checkOperandRange(min);
|
|
483
|
+
const maxValue = checkOperandRange(max);
|
|
484
|
+
if (minValue > maxValue) throw new CallciumError("INVALID_RANGE", `Range min (${minValue}) must not exceed max (${maxValue})`);
|
|
485
|
+
const buffer = new Uint8Array(65);
|
|
486
|
+
buffer[0] = opCode;
|
|
487
|
+
buffer.set(encodeWord(minValue), 1);
|
|
488
|
+
buffer.set(encodeWord(maxValue), 33);
|
|
489
|
+
return bytesToHex(buffer);
|
|
490
|
+
}
|
|
491
|
+
function contextOperand(contextPropertyId) {
|
|
492
|
+
lookupContextProperty(contextPropertyId);
|
|
493
|
+
return BigInt(contextPropertyId);
|
|
494
|
+
}
|
|
495
|
+
function setOp(opCode, values) {
|
|
496
|
+
const words = values.map((value) => {
|
|
497
|
+
if (typeof value === "bigint") return BigInt.asUintN(256, checkOperandRange(value));
|
|
498
|
+
if (typeof value === "boolean") return value ? 1n : 0n;
|
|
499
|
+
return BigInt("0x" + addressBody(value));
|
|
500
|
+
});
|
|
501
|
+
const deduped = [...new Set(words)].toSorted((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
502
|
+
if (deduped.length === 0) throw new CallciumError("EMPTY_SET", "Set must contain at least one value");
|
|
503
|
+
if (deduped.length > PolicyFormat.MAX_SET_MEMBERS) throw new CallciumError("SET_TOO_LARGE", `Set must contain at most ${PolicyFormat.MAX_SET_MEMBERS} values`);
|
|
504
|
+
const buffer = new Uint8Array(1 + deduped.length * 32);
|
|
505
|
+
buffer[0] = opCode;
|
|
506
|
+
for (let i = 0; i < deduped.length; i++) buffer.set(encodeWord(deduped[i]), 1 + i * 32);
|
|
507
|
+
return bytesToHex(buffer);
|
|
508
|
+
}
|
|
509
|
+
function readOperandExtremes(constraint) {
|
|
510
|
+
return "operandExtremes" in constraint ? constraint.operandExtremes : void 0;
|
|
511
|
+
}
|
|
512
|
+
var ConstraintBuilder = class {
|
|
513
|
+
scope;
|
|
514
|
+
path;
|
|
515
|
+
operators;
|
|
516
|
+
operandExtremes;
|
|
517
|
+
constructor(scope, path) {
|
|
518
|
+
this.scope = scope;
|
|
519
|
+
this.path = path;
|
|
520
|
+
this.operators = [];
|
|
521
|
+
this.operandExtremes = {
|
|
522
|
+
leastNegative: 0n,
|
|
523
|
+
greatest: 0n
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
push(opHex, values = []) {
|
|
527
|
+
for (const value of values) {
|
|
528
|
+
if (typeof value !== "bigint") continue;
|
|
529
|
+
if (value < this.operandExtremes.leastNegative) this.operandExtremes.leastNegative = value;
|
|
530
|
+
if (value > this.operandExtremes.greatest) this.operandExtremes.greatest = value;
|
|
531
|
+
}
|
|
532
|
+
this.operators.push(opHex);
|
|
533
|
+
return this;
|
|
534
|
+
}
|
|
535
|
+
eq(value) {
|
|
536
|
+
return this.push(singleOp(Op.EQ, value), [value]);
|
|
537
|
+
}
|
|
538
|
+
neq(value) {
|
|
539
|
+
return this.push(singleOp(Op.EQ | Op.NOT, value), [value]);
|
|
540
|
+
}
|
|
541
|
+
eqCtx(contextPropertyId) {
|
|
542
|
+
return this.push(singleOp(Op.EQ_CTX, contextOperand(contextPropertyId)));
|
|
543
|
+
}
|
|
544
|
+
neqCtx(contextPropertyId) {
|
|
545
|
+
return this.push(singleOp(Op.EQ_CTX | Op.NOT, contextOperand(contextPropertyId)));
|
|
546
|
+
}
|
|
547
|
+
gt(bound) {
|
|
548
|
+
return this.push(singleOp(Op.GT, bound), [bound]);
|
|
549
|
+
}
|
|
550
|
+
lt(bound) {
|
|
551
|
+
return this.push(singleOp(Op.LT, bound), [bound]);
|
|
552
|
+
}
|
|
553
|
+
gte(bound) {
|
|
554
|
+
return this.push(singleOp(Op.GTE, bound), [bound]);
|
|
555
|
+
}
|
|
556
|
+
lte(bound) {
|
|
557
|
+
return this.push(singleOp(Op.LTE, bound), [bound]);
|
|
558
|
+
}
|
|
559
|
+
between(min, max) {
|
|
560
|
+
return this.push(rangeOp(Op.BETWEEN, min, max), [min, max]);
|
|
561
|
+
}
|
|
562
|
+
isIn(values) {
|
|
563
|
+
return this.push(setOp(Op.IN, values), values);
|
|
564
|
+
}
|
|
565
|
+
notIn(values) {
|
|
566
|
+
return this.push(setOp(Op.IN | Op.NOT, values), values);
|
|
567
|
+
}
|
|
568
|
+
bitmaskAll(mask) {
|
|
569
|
+
return this.push(singleOp(Op.BITMASK_ALL, mask));
|
|
570
|
+
}
|
|
571
|
+
bitmaskAny(mask) {
|
|
572
|
+
return this.push(singleOp(Op.BITMASK_ANY, mask));
|
|
573
|
+
}
|
|
574
|
+
bitmaskNone(mask) {
|
|
575
|
+
return this.push(singleOp(Op.BITMASK_NONE, mask));
|
|
576
|
+
}
|
|
577
|
+
lengthEq(length) {
|
|
578
|
+
return this.push(singleOp(Op.LENGTH_EQ, length));
|
|
579
|
+
}
|
|
580
|
+
lengthGt(length) {
|
|
581
|
+
return this.push(singleOp(Op.LENGTH_GT, length));
|
|
582
|
+
}
|
|
583
|
+
lengthLt(length) {
|
|
584
|
+
return this.push(singleOp(Op.LENGTH_LT, length));
|
|
585
|
+
}
|
|
586
|
+
lengthGte(length) {
|
|
587
|
+
return this.push(singleOp(Op.LENGTH_GTE, length));
|
|
588
|
+
}
|
|
589
|
+
lengthLte(length) {
|
|
590
|
+
return this.push(singleOp(Op.LENGTH_LTE, length));
|
|
591
|
+
}
|
|
592
|
+
lengthBetween(min, max) {
|
|
593
|
+
return this.push(rangeOp(Op.LENGTH_BETWEEN, min, max));
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
function arg(...steps) {
|
|
597
|
+
return new ConstraintBuilder(Scope.CALLDATA, encodePath(steps));
|
|
598
|
+
}
|
|
599
|
+
function msgSender() {
|
|
600
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_SENDER]));
|
|
601
|
+
}
|
|
602
|
+
function msgValue() {
|
|
603
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_VALUE]));
|
|
604
|
+
}
|
|
605
|
+
function blockTimestamp() {
|
|
606
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_TIMESTAMP]));
|
|
607
|
+
}
|
|
608
|
+
function blockNumber() {
|
|
609
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_NUMBER]));
|
|
610
|
+
}
|
|
611
|
+
function chainId() {
|
|
612
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.CHAIN_ID]));
|
|
613
|
+
}
|
|
614
|
+
function txOrigin() {
|
|
615
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.TX_ORIGIN]));
|
|
616
|
+
}
|
|
617
|
+
function baseFee() {
|
|
618
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BASE_FEE]));
|
|
619
|
+
}
|
|
620
|
+
function gasPrice() {
|
|
621
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.GAS_PRICE]));
|
|
622
|
+
}
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region src/operators.ts
|
|
625
|
+
function toBigInt(bytes, offset = 0) {
|
|
626
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
|
|
627
|
+
return view.getBigUint64(0, false) << 192n | view.getBigUint64(8, false) << 128n | view.getBigUint64(16, false) << 64n | view.getBigUint64(24, false);
|
|
628
|
+
}
|
|
629
|
+
function isSigned(typeCode) {
|
|
630
|
+
return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
|
|
631
|
+
}
|
|
632
|
+
function isLeftAligned(typeCode) {
|
|
633
|
+
return typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX || typeCode === TypeCode.FUNCTION;
|
|
634
|
+
}
|
|
635
|
+
function canonicalize(value, typeCode) {
|
|
636
|
+
if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
|
|
637
|
+
const bits = BigInt(typeCode * 8);
|
|
638
|
+
return bits === 256n ? value : value & (1n << bits) - 1n;
|
|
639
|
+
}
|
|
640
|
+
if (typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX) {
|
|
641
|
+
const bits = (typeCode - TypeCode.INT_MIN + 1) * 8;
|
|
642
|
+
return bits === 256 ? value : BigInt.asUintN(256, BigInt.asIntN(bits, value));
|
|
643
|
+
}
|
|
644
|
+
if (typeCode === TypeCode.ADDRESS) return value & (1n << 160n) - 1n;
|
|
645
|
+
if (typeCode === TypeCode.BOOL) return value & 1n;
|
|
646
|
+
if (typeCode === TypeCode.FUNCTION) return value >> 64n << 64n;
|
|
647
|
+
if (typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX) {
|
|
648
|
+
const n = typeCode - TypeCode.FIXED_BYTES_MIN + 1;
|
|
649
|
+
if (n === 32) return value;
|
|
650
|
+
const padBits = BigInt((32 - n) * 8);
|
|
651
|
+
return value >> padBits << padBits;
|
|
652
|
+
}
|
|
653
|
+
return value;
|
|
654
|
+
}
|
|
655
|
+
function isLengthOp(opCode) {
|
|
656
|
+
const base = opCode & ~Op.NOT;
|
|
657
|
+
return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
|
|
658
|
+
}
|
|
659
|
+
function lengthToValueOp(base) {
|
|
660
|
+
if (base === Op.LENGTH_EQ) return Op.EQ;
|
|
661
|
+
if (base === Op.LENGTH_GT) return Op.GT;
|
|
662
|
+
if (base === Op.LENGTH_LT) return Op.LT;
|
|
663
|
+
if (base === Op.LENGTH_GTE) return Op.GTE;
|
|
664
|
+
if (base === Op.LENGTH_LTE) return Op.LTE;
|
|
665
|
+
return Op.BETWEEN;
|
|
666
|
+
}
|
|
667
|
+
function isLengthValidType(typeCode) {
|
|
668
|
+
return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
|
|
669
|
+
}
|
|
670
|
+
function compareTyped(a, b, typeCode) {
|
|
671
|
+
if (isSigned(typeCode)) {
|
|
672
|
+
const sa = BigInt.asIntN(256, a);
|
|
673
|
+
const sb = BigInt.asIntN(256, b);
|
|
674
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
675
|
+
}
|
|
676
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
677
|
+
}
|
|
678
|
+
function isIn(value, data) {
|
|
679
|
+
const count = data.length / 32;
|
|
680
|
+
let low = 0;
|
|
681
|
+
let high = count - 1;
|
|
682
|
+
while (low <= high) {
|
|
683
|
+
const mid = low + high >>> 1;
|
|
684
|
+
const elem = toBigInt(data, mid * 32);
|
|
685
|
+
if (value === elem) return true;
|
|
686
|
+
if (value < elem) high = mid - 1;
|
|
687
|
+
else low = mid + 1;
|
|
688
|
+
}
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
692
|
+
let base = opCode & ~Op.NOT;
|
|
693
|
+
if (isLengthOp(base)) {
|
|
694
|
+
value = valueLength;
|
|
695
|
+
typeCode = TypeCode.UINT_MAX;
|
|
696
|
+
base = lengthToValueOp(base);
|
|
697
|
+
}
|
|
698
|
+
let result;
|
|
699
|
+
switch (base) {
|
|
700
|
+
case Op.EQ:
|
|
701
|
+
result = value === toBigInt(operandData, 0);
|
|
702
|
+
break;
|
|
703
|
+
case Op.GT:
|
|
704
|
+
result = compareTyped(value, toBigInt(operandData, 0), typeCode) > 0;
|
|
705
|
+
break;
|
|
706
|
+
case Op.LT:
|
|
707
|
+
result = compareTyped(value, toBigInt(operandData, 0), typeCode) < 0;
|
|
708
|
+
break;
|
|
709
|
+
case Op.GTE:
|
|
710
|
+
result = compareTyped(value, toBigInt(operandData, 0), typeCode) >= 0;
|
|
711
|
+
break;
|
|
712
|
+
case Op.LTE:
|
|
713
|
+
result = compareTyped(value, toBigInt(operandData, 0), typeCode) <= 0;
|
|
714
|
+
break;
|
|
715
|
+
case Op.BETWEEN: {
|
|
716
|
+
const min = toBigInt(operandData, 0);
|
|
717
|
+
const max = toBigInt(operandData, 32);
|
|
718
|
+
result = compareTyped(value, min, typeCode) >= 0 && compareTyped(value, max, typeCode) <= 0;
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
case Op.IN:
|
|
722
|
+
result = isIn(value, operandData);
|
|
723
|
+
break;
|
|
724
|
+
case Op.BITMASK_ALL: {
|
|
725
|
+
const mask = toBigInt(operandData, 0);
|
|
726
|
+
result = (value & mask) === mask;
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
case Op.BITMASK_ANY: {
|
|
730
|
+
const mask = toBigInt(operandData, 0);
|
|
731
|
+
result = (value & mask) !== 0n;
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
case Op.BITMASK_NONE: {
|
|
735
|
+
const mask = toBigInt(operandData, 0);
|
|
736
|
+
result = (value & mask) === 0n;
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
default: throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code ${formatCode(base, PolicyFormat.RULE_OPCODE_SIZE)}`);
|
|
740
|
+
}
|
|
741
|
+
return (opCode & Op.NOT) !== 0 ? !result : result;
|
|
742
|
+
}
|
|
743
|
+
function isValidOperatorData(opBase, dataLength) {
|
|
744
|
+
const operands = operandsOf(opBase);
|
|
745
|
+
if (operands === "single") return dataLength === 32;
|
|
746
|
+
if (operands === "range") return dataLength === 64;
|
|
747
|
+
if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
function toOperatorBytes(operator) {
|
|
751
|
+
const bytes = hexToBytes(operator);
|
|
752
|
+
if (bytes.length < 1) throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode)");
|
|
753
|
+
return bytes;
|
|
754
|
+
}
|
|
424
755
|
function unknownTypeCode(code) {
|
|
425
|
-
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code
|
|
756
|
+
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code ${formatCode(code, DescriptorFormat.TYPECODE_SIZE)}`);
|
|
426
757
|
}
|
|
427
758
|
const ELEMENTARY = {
|
|
428
759
|
typeClass: "elementary",
|
|
429
760
|
isDynamic: false
|
|
430
761
|
};
|
|
431
|
-
const
|
|
762
|
+
const ELEMENTARY_DYNAMIC = {
|
|
432
763
|
typeClass: "elementary",
|
|
433
764
|
isDynamic: true
|
|
434
765
|
};
|
|
@@ -450,7 +781,7 @@ function classifyTypeCode(code) {
|
|
|
450
781
|
if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
|
|
451
782
|
if (code >= 68 && code <= 79) unknownTypeCode(code);
|
|
452
783
|
if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
|
|
453
|
-
if (code === TypeCode.BYTES || code === TypeCode.STRING) return
|
|
784
|
+
if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYNAMIC;
|
|
454
785
|
if (code >= 114 && code <= 127) unknownTypeCode(code);
|
|
455
786
|
if (code === TypeCode.STATIC_ARRAY) return STATIC_ARR;
|
|
456
787
|
if (code === TypeCode.DYNAMIC_ARRAY) return DYNAMIC_ARR;
|
|
@@ -458,6 +789,13 @@ function classifyTypeCode(code) {
|
|
|
458
789
|
if (code === TypeCode.TUPLE) return TUPLE;
|
|
459
790
|
unknownTypeCode(code);
|
|
460
791
|
}
|
|
792
|
+
function isElementary(code) {
|
|
793
|
+
try {
|
|
794
|
+
return classifyTypeCode(code).typeClass === "elementary";
|
|
795
|
+
} catch {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
461
799
|
function isAddressableTarget(code) {
|
|
462
800
|
let info;
|
|
463
801
|
try {
|
|
@@ -489,9 +827,29 @@ function lookupTypeCode(code) {
|
|
|
489
827
|
}
|
|
490
828
|
//#endregion
|
|
491
829
|
//#region src/descriptor.ts
|
|
830
|
+
function readNode(desc, offset) {
|
|
831
|
+
if (offset >= desc.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
|
|
832
|
+
const typeCode = desc[offset];
|
|
833
|
+
const info = classifyTypeCode(typeCode);
|
|
834
|
+
if (info.typeClass === "elementary") return {
|
|
835
|
+
typeCode,
|
|
836
|
+
staticWords: info.isDynamic ? 0 : 1,
|
|
837
|
+
nodeLength: DescriptorFormat.TYPECODE_SIZE
|
|
838
|
+
};
|
|
839
|
+
if (offset + DescriptorFormat.TYPECODE_SIZE + DescriptorFormat.COMPOSITE_META_SIZE > desc.length) throw new CallciumError("UNEXPECTED_END", "Incomplete composite metadata", offset);
|
|
840
|
+
const meta = readU24(desc, offset + DescriptorFormat.TYPECODE_SIZE);
|
|
841
|
+
const length = meta & DescriptorFormat.META_NODE_LENGTH_MASK;
|
|
842
|
+
const minHeader = info.typeClass === "tuple" ? DescriptorFormat.TUPLE_HEADER_SIZE : DescriptorFormat.ARRAY_HEADER_SIZE;
|
|
843
|
+
if (length < minHeader) throw new CallciumError("NODE_LENGTH_TOO_SMALL", `Composite node length ${length} is smaller than minimum header ${minHeader}`, offset);
|
|
844
|
+
if (offset + length > desc.length) throw new CallciumError("NODE_OVERFLOW", "Composite node extends beyond descriptor", offset);
|
|
845
|
+
return {
|
|
846
|
+
typeCode,
|
|
847
|
+
staticWords: meta >> DescriptorFormat.META_STATIC_WORDS_SHIFT,
|
|
848
|
+
nodeLength: length
|
|
849
|
+
};
|
|
850
|
+
}
|
|
492
851
|
function nodeLength(desc, offset) {
|
|
493
|
-
|
|
494
|
-
return readU24(desc, offset + 1) & DescriptorFormat.META_NODE_LENGTH_MASK;
|
|
852
|
+
return readNode(desc, offset).nodeLength;
|
|
495
853
|
}
|
|
496
854
|
function tupleFieldOffset(desc, tupleOffset, fieldIndex) {
|
|
497
855
|
let cursor = tupleOffset + DescriptorFormat.TUPLE_HEADER_SIZE;
|
|
@@ -499,20 +857,18 @@ function tupleFieldOffset(desc, tupleOffset, fieldIndex) {
|
|
|
499
857
|
return cursor;
|
|
500
858
|
}
|
|
501
859
|
function paramCount(desc) {
|
|
860
|
+
if (desc.length < DescriptorFormat.HEADER_SIZE) throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
|
|
502
861
|
return desc[1];
|
|
503
862
|
}
|
|
504
863
|
function paramOffset(desc, index) {
|
|
505
|
-
const count = desc
|
|
506
|
-
if (index >= count) throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count})
|
|
864
|
+
const count = paramCount(desc);
|
|
865
|
+
if (index >= count) throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count})`);
|
|
507
866
|
let cursor = DescriptorFormat.HEADER_SIZE;
|
|
508
867
|
for (let i = 0; i < index; i++) cursor += nodeLength(desc, cursor);
|
|
509
868
|
return cursor;
|
|
510
869
|
}
|
|
511
870
|
function inspect$1(desc, offset) {
|
|
512
|
-
const typeCode = desc
|
|
513
|
-
let staticWords;
|
|
514
|
-
if (typeCode < TypeCode.STATIC_ARRAY) staticWords = typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING ? 0 : 1;
|
|
515
|
-
else staticWords = readU24(desc, offset + 1) >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
|
|
871
|
+
const { typeCode, staticWords } = readNode(desc, offset);
|
|
516
872
|
return {
|
|
517
873
|
typeCode,
|
|
518
874
|
isDynamic: staticWords === 0,
|
|
@@ -523,27 +879,24 @@ function typeAt(desc, steps) {
|
|
|
523
879
|
return walkPath(desc, steps).typeInfo;
|
|
524
880
|
}
|
|
525
881
|
function walkPath(desc, steps) {
|
|
526
|
-
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step
|
|
527
|
-
|
|
528
|
-
const count = desc[1];
|
|
529
|
-
if (paramIndex >= count) throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${paramIndex} out of range (paramCount=${count}).`);
|
|
530
|
-
let cursor = paramOffset(desc, paramIndex);
|
|
882
|
+
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
883
|
+
let cursor = paramOffset(desc, steps[0]);
|
|
531
884
|
let quantifiedStaticLength = 0;
|
|
532
885
|
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
|
|
533
886
|
const typeCode = desc[cursor];
|
|
534
887
|
const step = steps[stepIndex];
|
|
535
888
|
if (typeCode === TypeCode.TUPLE) {
|
|
536
889
|
const fields = tupleFieldCount(desc, cursor);
|
|
537
|
-
if (step >= fields) throw new CallciumError("TUPLE_FIELD_OUT_OF_BOUNDS", `Tuple field index ${step} out of range (tuple has ${fields} fields)
|
|
890
|
+
if (step >= fields) throw new CallciumError("TUPLE_FIELD_OUT_OF_BOUNDS", `Tuple field index ${step} out of range (tuple has ${fields} fields)`);
|
|
538
891
|
cursor = tupleFieldOffset(desc, cursor, step);
|
|
539
892
|
} else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) {
|
|
540
893
|
if (typeCode === TypeCode.STATIC_ARRAY) if (isQuantifier(step)) quantifiedStaticLength = staticArrayLength(desc, cursor);
|
|
541
894
|
else {
|
|
542
895
|
const length = staticArrayLength(desc, cursor);
|
|
543
|
-
if (step >= length) throw new CallciumError("STATIC_ARRAY_INDEX_OUT_OF_BOUNDS", `Array index ${step} out of range (length=${length})
|
|
896
|
+
if (step >= length) throw new CallciumError("STATIC_ARRAY_INDEX_OUT_OF_BOUNDS", `Array index ${step} out of range (length=${length})`);
|
|
544
897
|
}
|
|
545
898
|
cursor = arrayElementOffset(cursor);
|
|
546
|
-
} else throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}
|
|
899
|
+
} else throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}`);
|
|
547
900
|
}
|
|
548
901
|
return {
|
|
549
902
|
typeInfo: inspect$1(desc, cursor),
|
|
@@ -561,7 +914,7 @@ function arrayElementOffset(offset) {
|
|
|
561
914
|
return offset + DescriptorFormat.ARRAY_HEADER_SIZE;
|
|
562
915
|
}
|
|
563
916
|
function uncompilablePath(stepIndex) {
|
|
564
|
-
return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target
|
|
917
|
+
return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target`);
|
|
565
918
|
}
|
|
566
919
|
function headSpan(desc, offset, count) {
|
|
567
920
|
let span = 0;
|
|
@@ -668,7 +1021,7 @@ function encodeHint(walk, targetMeta, typeCode, depth) {
|
|
|
668
1021
|
return out;
|
|
669
1022
|
}
|
|
670
1023
|
function compileHint(desc, steps) {
|
|
671
|
-
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step
|
|
1024
|
+
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
672
1025
|
const argIndex = steps[0];
|
|
673
1026
|
if (argIndex >= paramCount(desc)) throw uncompilablePath(0);
|
|
674
1027
|
const arg = headSpan(desc, DescriptorFormat.HEADER_SIZE, argIndex);
|
|
@@ -720,11 +1073,11 @@ function function_() {
|
|
|
720
1073
|
return new Uint8Array([TypeCode.FUNCTION]);
|
|
721
1074
|
}
|
|
722
1075
|
function uintN(bits) {
|
|
723
|
-
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
|
|
1076
|
+
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`);
|
|
724
1077
|
return new Uint8Array([TypeCode.UINT_MIN + (bits / 8 - 1)]);
|
|
725
1078
|
}
|
|
726
1079
|
function intN(bits) {
|
|
727
|
-
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
|
|
1080
|
+
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`);
|
|
728
1081
|
return new Uint8Array([TypeCode.INT_MIN + (bits / 8 - 1)]);
|
|
729
1082
|
}
|
|
730
1083
|
function bytes() {
|
|
@@ -733,14 +1086,14 @@ function bytes() {
|
|
|
733
1086
|
function string_() {
|
|
734
1087
|
return new Uint8Array([TypeCode.STRING]);
|
|
735
1088
|
}
|
|
736
|
-
function bytesN(
|
|
737
|
-
if (
|
|
738
|
-
return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (
|
|
1089
|
+
function bytesN(length) {
|
|
1090
|
+
if (length < 1 || length > 32) throw new CallciumError("UNKNOWN_TYPE", `Invalid bytesN size: ${length}. Must be 1–32`);
|
|
1091
|
+
return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (length - 1)]);
|
|
739
1092
|
}
|
|
740
1093
|
function array(elemDesc, length) {
|
|
741
1094
|
if (length === void 0) {
|
|
742
1095
|
const nodeLength = DescriptorFormat.ARRAY_HEADER_SIZE + elemDesc.length;
|
|
743
|
-
if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("NODE_LENGTH_TOO_LARGE", `Dynamic array node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}
|
|
1096
|
+
if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("NODE_LENGTH_TOO_LARGE", `Dynamic array node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}`);
|
|
744
1097
|
const meta24 = 0 << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
|
|
745
1098
|
const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + elemDesc.length);
|
|
746
1099
|
buf[0] = TypeCode.DYNAMIC_ARRAY;
|
|
@@ -748,12 +1101,12 @@ function array(elemDesc, length) {
|
|
|
748
1101
|
buf.set(elemDesc, 4);
|
|
749
1102
|
return buf;
|
|
750
1103
|
}
|
|
751
|
-
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}
|
|
1104
|
+
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}`);
|
|
752
1105
|
const nodeLength = DescriptorFormat.ARRAY_HEADER_SIZE + elemDesc.length + DescriptorFormat.ARRAY_LENGTH_SIZE;
|
|
753
|
-
if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("NODE_LENGTH_TOO_LARGE", `Static array node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}
|
|
1106
|
+
if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("NODE_LENGTH_TOO_LARGE", `Static array node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}`);
|
|
754
1107
|
const elemStaticWords = extractStaticWords(elemDesc);
|
|
755
1108
|
const staticWords = elemStaticWords === 0 ? 0 : length * elemStaticWords;
|
|
756
|
-
if (staticWords > DescriptorFormat.MAX_STATIC_WORDS) throw new CallciumError("STATIC_WORDS_TOO_LARGE", `Static array static words ${staticWords} exceeds maximum ${DescriptorFormat.MAX_STATIC_WORDS}
|
|
1109
|
+
if (staticWords > DescriptorFormat.MAX_STATIC_WORDS) throw new CallciumError("STATIC_WORDS_TOO_LARGE", `Static array static words ${staticWords} exceeds maximum ${DescriptorFormat.MAX_STATIC_WORDS}`);
|
|
757
1110
|
const meta24 = staticWords << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
|
|
758
1111
|
const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + elemDesc.length + DescriptorFormat.ARRAY_LENGTH_SIZE);
|
|
759
1112
|
buf[0] = TypeCode.STATIC_ARRAY;
|
|
@@ -763,10 +1116,10 @@ function array(elemDesc, length) {
|
|
|
763
1116
|
return buf;
|
|
764
1117
|
}
|
|
765
1118
|
function tuple(fieldDescs) {
|
|
766
|
-
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}
|
|
1119
|
+
if (fieldDescs.length === 0 || fieldDescs.length > DescriptorFormat.MAX_TUPLE_FIELDS) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", `Tuple field count ${fieldDescs.length} out of range. Must be 1–${DescriptorFormat.MAX_TUPLE_FIELDS}`);
|
|
767
1120
|
const totalFieldBytes = fieldDescs.reduce((sum, f) => sum + f.length, 0);
|
|
768
1121
|
const nodeLength = DescriptorFormat.TUPLE_HEADER_SIZE + totalFieldBytes;
|
|
769
|
-
if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("NODE_LENGTH_TOO_LARGE", `Tuple node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}
|
|
1122
|
+
if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("NODE_LENGTH_TOO_LARGE", `Tuple node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}`);
|
|
770
1123
|
let anyDynamic = false;
|
|
771
1124
|
let staticWordsSum = 0;
|
|
772
1125
|
for (const field of fieldDescs) {
|
|
@@ -775,7 +1128,7 @@ function tuple(fieldDescs) {
|
|
|
775
1128
|
staticWordsSum += staticWords;
|
|
776
1129
|
}
|
|
777
1130
|
const staticWords = anyDynamic ? 0 : staticWordsSum;
|
|
778
|
-
if (staticWords > DescriptorFormat.MAX_STATIC_WORDS) throw new CallciumError("STATIC_WORDS_TOO_LARGE", `Tuple static words ${staticWords} exceeds maximum ${DescriptorFormat.MAX_STATIC_WORDS}
|
|
1131
|
+
if (staticWords > DescriptorFormat.MAX_STATIC_WORDS) throw new CallciumError("STATIC_WORDS_TOO_LARGE", `Tuple static words ${staticWords} exceeds maximum ${DescriptorFormat.MAX_STATIC_WORDS}`);
|
|
779
1132
|
const meta24 = staticWords << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
|
|
780
1133
|
const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + DescriptorFormat.TUPLE_FIELDCOUNT_SIZE + totalFieldBytes);
|
|
781
1134
|
buf[0] = TypeCode.TUPLE;
|
|
@@ -796,9 +1149,12 @@ function commaPositions(input, start, end) {
|
|
|
796
1149
|
for (let i = start; i < end; i++) {
|
|
797
1150
|
const char = input[i];
|
|
798
1151
|
if (char === "(" || char === "[") depth++;
|
|
799
|
-
else if (char === ")" || char === "]")
|
|
800
|
-
|
|
1152
|
+
else if (char === ")" || char === "]") {
|
|
1153
|
+
if (depth === 0) throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '${char}' at position ${i}`);
|
|
1154
|
+
depth--;
|
|
1155
|
+
} else if (char === "," && depth === 0) positions.push(i);
|
|
801
1156
|
}
|
|
1157
|
+
if (depth !== 0) throw new CallciumError("MALFORMED_TYPE_STRING", `Unclosed '${input.slice(start, end)}'`);
|
|
802
1158
|
return positions;
|
|
803
1159
|
}
|
|
804
1160
|
const MAX_TYPE_STRING_NUMBER = 256 ** DescriptorFormat.ARRAY_LENGTH_SIZE - 1;
|
|
@@ -817,27 +1173,27 @@ function parseUint(input, start, end) {
|
|
|
817
1173
|
function parseTuple(input, start, end) {
|
|
818
1174
|
if (input[start] !== "(") throw new CallciumError("MALFORMED_TYPE_STRING", `Expected '(' at position ${start}`);
|
|
819
1175
|
let depth = 0;
|
|
820
|
-
let
|
|
1176
|
+
let closeIndex = -1;
|
|
821
1177
|
for (let i = start; i < end; i++) if (input[i] === "(") depth++;
|
|
822
1178
|
else if (input[i] === ")") {
|
|
823
1179
|
depth--;
|
|
824
1180
|
if (depth === 0) {
|
|
825
|
-
|
|
1181
|
+
closeIndex = i;
|
|
826
1182
|
break;
|
|
827
1183
|
}
|
|
828
1184
|
}
|
|
829
|
-
if (
|
|
830
|
-
if (
|
|
1185
|
+
if (closeIndex === -1) throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '(' at position ${start}`);
|
|
1186
|
+
if (closeIndex !== end - 1) throw new CallciumError("MALFORMED_TYPE_STRING", `Unexpected characters after ')' at position ${closeIndex + 1}`);
|
|
831
1187
|
const innerStart = start + 1;
|
|
832
|
-
const innerEnd =
|
|
1188
|
+
const innerEnd = closeIndex;
|
|
833
1189
|
const commas = commaPositions(input, innerStart, innerEnd);
|
|
834
1190
|
const segments = [];
|
|
835
|
-
let
|
|
1191
|
+
let segmentStart = innerStart;
|
|
836
1192
|
for (const comma of commas) {
|
|
837
|
-
segments.push([
|
|
838
|
-
|
|
1193
|
+
segments.push([segmentStart, comma]);
|
|
1194
|
+
segmentStart = comma + 1;
|
|
839
1195
|
}
|
|
840
|
-
segments.push([
|
|
1196
|
+
segments.push([segmentStart, innerEnd]);
|
|
841
1197
|
if (segments.length === 1 && segments[0][0] === segments[0][1]) return tuple([]);
|
|
842
1198
|
return tuple(segments.map(([fieldStart, fieldEnd]) => parseType(input, fieldStart, fieldEnd)));
|
|
843
1199
|
}
|
|
@@ -849,28 +1205,25 @@ function parseBaseType(input, start, end) {
|
|
|
849
1205
|
if (segment === "bytes") return bytes();
|
|
850
1206
|
if (segment === "string") return string_();
|
|
851
1207
|
if (segment.startsWith("(")) return parseTuple(input, start, end);
|
|
852
|
-
if (segment.startsWith("uint")) return uintN(parseUint(input, start + 4, end));
|
|
853
|
-
if (segment.startsWith("int")) return intN(parseUint(input, start + 3, end));
|
|
854
|
-
if (segment.startsWith("bytes")) return bytesN(parseUint(input, start + 5, end));
|
|
1208
|
+
if (segment.length > 4 && segment.startsWith("uint")) return uintN(parseUint(input, start + 4, end));
|
|
1209
|
+
if (segment.length > 3 && segment.startsWith("int")) return intN(parseUint(input, start + 3, end));
|
|
1210
|
+
if (segment.length > 5 && segment.startsWith("bytes")) return bytesN(parseUint(input, start + 5, end));
|
|
855
1211
|
throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
|
|
856
1212
|
}
|
|
857
1213
|
function parseType(input, start, end) {
|
|
858
1214
|
if (start >= end) throw new CallciumError("MALFORMED_TYPE_STRING", "Empty type segment");
|
|
859
|
-
const
|
|
1215
|
+
const spans = [];
|
|
860
1216
|
let baseEnd = end;
|
|
861
1217
|
while (baseEnd > start && input[baseEnd - 1] === "]") {
|
|
862
|
-
const
|
|
863
|
-
let
|
|
864
|
-
while (
|
|
865
|
-
if (input[
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
baseEnd = openPos;
|
|
870
|
-
}
|
|
871
|
-
if (input[start] !== "(" && input.slice(start, baseEnd).includes("[")) throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '[' in '${input.slice(start, baseEnd)}'`);
|
|
1218
|
+
const closeIndex = baseEnd - 1;
|
|
1219
|
+
let openIndex = closeIndex - 1;
|
|
1220
|
+
while (openIndex > start && input[openIndex] !== "[") openIndex--;
|
|
1221
|
+
if (input[openIndex] !== "[") throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched ']' at position ${closeIndex}`);
|
|
1222
|
+
spans.unshift([openIndex + 1, closeIndex]);
|
|
1223
|
+
baseEnd = openIndex;
|
|
1224
|
+
}
|
|
872
1225
|
let desc = parseBaseType(input, start, baseEnd);
|
|
873
|
-
for (const
|
|
1226
|
+
for (const [innerStart, innerEnd] of spans) desc = array(desc, innerStart === innerEnd ? void 0 : parseUint(input, innerStart, innerEnd));
|
|
874
1227
|
return desc;
|
|
875
1228
|
}
|
|
876
1229
|
function fromTypes(typesCsv) {
|
|
@@ -884,7 +1237,7 @@ function fromTypes(typesCsv) {
|
|
|
884
1237
|
}
|
|
885
1238
|
segments.push([segStart, typesCsv.length]);
|
|
886
1239
|
const paramDescs = segments.map(([start, end]) => parseType(typesCsv, start, end));
|
|
887
|
-
if (paramDescs.length > DescriptorFormat.MAX_PARAMS) throw new CallciumError("TOO_MANY_PARAMS", `Parameter count ${paramDescs.length} exceeds maximum ${DescriptorFormat.MAX_PARAMS}
|
|
1240
|
+
if (paramDescs.length > DescriptorFormat.MAX_PARAMS) throw new CallciumError("TOO_MANY_PARAMS", `Parameter count ${paramDescs.length} exceeds maximum ${DescriptorFormat.MAX_PARAMS}`);
|
|
888
1241
|
const totalBytes = paramDescs.reduce((sum, d) => sum + d.length, 0);
|
|
889
1242
|
const result = new Uint8Array(DescriptorFormat.HEADER_SIZE + totalBytes);
|
|
890
1243
|
result[0] = DescriptorFormat.VERSION;
|
|
@@ -894,10 +1247,11 @@ function fromTypes(typesCsv) {
|
|
|
894
1247
|
result.set(desc, offset);
|
|
895
1248
|
offset += desc.length;
|
|
896
1249
|
}
|
|
1250
|
+
decodeDescriptor(result);
|
|
897
1251
|
return result;
|
|
898
1252
|
}
|
|
899
1253
|
function nodeToTypeString(desc, offset) {
|
|
900
|
-
const typeCode = desc
|
|
1254
|
+
const { typeCode } = Descriptor.inspect(desc, offset);
|
|
901
1255
|
if (typeCode === TypeCode.TUPLE) {
|
|
902
1256
|
const fieldCount = Descriptor.tupleFieldCount(desc, offset);
|
|
903
1257
|
const fieldTypes = [];
|
|
@@ -926,30 +1280,20 @@ function toTypes(desc) {
|
|
|
926
1280
|
return types.join(",");
|
|
927
1281
|
}
|
|
928
1282
|
function parseNode(data, offset, depth) {
|
|
929
|
-
|
|
930
|
-
const code = data[offset];
|
|
1283
|
+
const { typeCode: code, staticWords, nodeLength } = readNode(data, offset);
|
|
931
1284
|
const info = classifyTypeCode(code);
|
|
932
|
-
const
|
|
1285
|
+
const isDynamic = staticWords === 0;
|
|
1286
|
+
const staticSize = isDynamic ? 0 : staticWords * 32;
|
|
1287
|
+
const nodeEnd = offset + nodeLength;
|
|
933
1288
|
if (info.typeClass === "elementary") return {
|
|
934
1289
|
typeCode: code,
|
|
935
|
-
isDynamic
|
|
936
|
-
staticSize
|
|
937
|
-
next:
|
|
1290
|
+
isDynamic,
|
|
1291
|
+
staticSize,
|
|
1292
|
+
next: nodeEnd
|
|
938
1293
|
};
|
|
939
1294
|
if (depth > DescriptorFormat.MAX_NESTING_DEPTH) throw new CallciumError("NESTING_TOO_DEEP", `Composite nesting exceeds maximum depth ${DescriptorFormat.MAX_NESTING_DEPTH}`, offset);
|
|
940
|
-
const metaEnd = metaOffset + DescriptorFormat.COMPOSITE_META_SIZE;
|
|
941
|
-
if (metaEnd > data.length) throw new CallciumError("UNEXPECTED_END", "Incomplete composite metadata", offset);
|
|
942
|
-
const meta = readU24(data, metaOffset);
|
|
943
|
-
const staticWords = meta >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
|
|
944
|
-
const nodeLength = meta & DescriptorFormat.META_NODE_LENGTH_MASK;
|
|
945
|
-
const minHeader = info.typeClass === "tuple" ? DescriptorFormat.TUPLE_HEADER_SIZE : DescriptorFormat.ARRAY_HEADER_SIZE;
|
|
946
|
-
if (nodeLength < minHeader) throw new CallciumError("NODE_LENGTH_TOO_SMALL", `Composite node length ${nodeLength} is smaller than minimum header ${minHeader}`, offset);
|
|
947
|
-
if (offset + nodeLength > data.length) throw new CallciumError("NODE_OVERFLOW", "Composite node extends beyond descriptor", offset);
|
|
948
|
-
const isDynamic = staticWords === 0;
|
|
949
|
-
const staticSize = isDynamic ? 0 : staticWords * 32;
|
|
950
|
-
const nodeEnd = offset + nodeLength;
|
|
951
1295
|
if (info.typeClass === "tuple") {
|
|
952
|
-
const fieldCountOffset =
|
|
1296
|
+
const fieldCountOffset = offset + DescriptorFormat.TYPECODE_SIZE + DescriptorFormat.COMPOSITE_META_SIZE;
|
|
953
1297
|
if (fieldCountOffset + 2 > data.length) throw new CallciumError("UNEXPECTED_END", "Incomplete tuple header", offset);
|
|
954
1298
|
const fieldCount = readU16(data, fieldCountOffset);
|
|
955
1299
|
if (fieldCount === 0) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", "Tuple must have at least one field", offset);
|
|
@@ -973,183 +1317,56 @@ function parseNode(data, offset, depth) {
|
|
|
973
1317
|
typeCode: code,
|
|
974
1318
|
isDynamic,
|
|
975
1319
|
staticSize,
|
|
976
|
-
next: nodeEnd
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
|
|
980
|
-
return {
|
|
981
|
-
typeCode: code,
|
|
982
|
-
isDynamic: true,
|
|
983
|
-
staticSize: 0,
|
|
984
|
-
next: nodeEnd
|
|
985
|
-
};
|
|
986
|
-
}
|
|
987
|
-
function indexToPath(index) {
|
|
988
|
-
return `0x${index.toString(16).padStart(4, "0")}`;
|
|
989
|
-
}
|
|
990
|
-
function decodeDescriptor(data) {
|
|
991
|
-
if (data.length < 1) throw new CallciumError("MALFORMED_HEADER", "Descriptor is empty");
|
|
992
|
-
const version = data[0];
|
|
993
|
-
if (version !== DescriptorFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${DescriptorFormat.VERSION})`);
|
|
994
|
-
if (data.length < DescriptorFormat.HEADER_SIZE) throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
|
|
995
|
-
const params = [];
|
|
996
|
-
let cursor = DescriptorFormat.HEADER_SIZE;
|
|
997
|
-
while (cursor < data.length) {
|
|
998
|
-
if (params.length >= DescriptorFormat.MAX_PARAMS) throw new CallciumError("TOO_MANY_PARAMS", `Descriptor exceeds the maximum of ${DescriptorFormat.MAX_PARAMS} top-level params`);
|
|
999
|
-
const { typeCode, isDynamic, staticSize, next } = parseNode(data, cursor, 1);
|
|
1000
|
-
params.push({
|
|
1001
|
-
index: params.length,
|
|
1002
|
-
typeCode,
|
|
1003
|
-
isDynamic,
|
|
1004
|
-
staticSize,
|
|
1005
|
-
path: indexToPath(params.length),
|
|
1006
|
-
span: {
|
|
1007
|
-
start: cursor,
|
|
1008
|
-
end: next
|
|
1009
|
-
}
|
|
1010
|
-
});
|
|
1011
|
-
cursor = next;
|
|
1012
|
-
}
|
|
1013
|
-
const declaredCount = data[1];
|
|
1014
|
-
if (params.length !== declaredCount) throw new CallciumError("PARAM_COUNT_MISMATCH", `Header declares ${declaredCount} params but ${params.length} were parsed`);
|
|
1015
|
-
return { descriptor: {
|
|
1016
|
-
version,
|
|
1017
|
-
params
|
|
1018
|
-
} };
|
|
1019
|
-
}
|
|
1020
|
-
const DescriptorCoder = {
|
|
1021
|
-
fromTypes,
|
|
1022
|
-
toTypes
|
|
1023
|
-
};
|
|
1024
|
-
//#endregion
|
|
1025
|
-
//#region src/operators.ts
|
|
1026
|
-
function toBigInt(bytes, offset = 0) {
|
|
1027
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
|
|
1028
|
-
return view.getBigUint64(0, false) << 192n | view.getBigUint64(8, false) << 128n | view.getBigUint64(16, false) << 64n | view.getBigUint64(24, false);
|
|
1029
|
-
}
|
|
1030
|
-
function isSigned(typeCode) {
|
|
1031
|
-
return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
|
|
1032
|
-
}
|
|
1033
|
-
function isLeftAligned(typeCode) {
|
|
1034
|
-
return typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX || typeCode === TypeCode.FUNCTION;
|
|
1035
|
-
}
|
|
1036
|
-
function canonicalize(value, typeCode) {
|
|
1037
|
-
if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
|
|
1038
|
-
const bits = BigInt(typeCode * 8);
|
|
1039
|
-
return bits === 256n ? value : value & (1n << bits) - 1n;
|
|
1040
|
-
}
|
|
1041
|
-
if (typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX) {
|
|
1042
|
-
const bits = (typeCode - TypeCode.INT_MIN + 1) * 8;
|
|
1043
|
-
return bits === 256 ? value : BigInt.asUintN(256, BigInt.asIntN(bits, value));
|
|
1044
|
-
}
|
|
1045
|
-
if (typeCode === TypeCode.ADDRESS) return value & (1n << 160n) - 1n;
|
|
1046
|
-
if (typeCode === TypeCode.BOOL) return value & 1n;
|
|
1047
|
-
if (typeCode === TypeCode.FUNCTION) return value >> 64n << 64n;
|
|
1048
|
-
if (typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX) {
|
|
1049
|
-
const n = typeCode - TypeCode.FIXED_BYTES_MIN + 1;
|
|
1050
|
-
if (n === 32) return value;
|
|
1051
|
-
const padBits = BigInt((32 - n) * 8);
|
|
1052
|
-
return value >> padBits << padBits;
|
|
1053
|
-
}
|
|
1054
|
-
return value;
|
|
1055
|
-
}
|
|
1056
|
-
function isLengthOp(opCode) {
|
|
1057
|
-
const base = opCode & ~Op.NOT;
|
|
1058
|
-
return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
|
|
1059
|
-
}
|
|
1060
|
-
function lengthToValueOp(base) {
|
|
1061
|
-
if (base === Op.LENGTH_EQ) return Op.EQ;
|
|
1062
|
-
if (base === Op.LENGTH_GT) return Op.GT;
|
|
1063
|
-
if (base === Op.LENGTH_LT) return Op.LT;
|
|
1064
|
-
if (base === Op.LENGTH_GTE) return Op.GTE;
|
|
1065
|
-
if (base === Op.LENGTH_LTE) return Op.LTE;
|
|
1066
|
-
return Op.BETWEEN;
|
|
1067
|
-
}
|
|
1068
|
-
function isLengthValidType(typeCode) {
|
|
1069
|
-
return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
|
|
1070
|
-
}
|
|
1071
|
-
function compareTyped(a, b, typeCode) {
|
|
1072
|
-
if (isSigned(typeCode)) {
|
|
1073
|
-
const sa = BigInt.asIntN(256, a);
|
|
1074
|
-
const sb = BigInt.asIntN(256, b);
|
|
1075
|
-
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
1076
|
-
}
|
|
1077
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
1078
|
-
}
|
|
1079
|
-
function isIn(value, data) {
|
|
1080
|
-
const count = data.length / 32;
|
|
1081
|
-
let lo = 0;
|
|
1082
|
-
let hi = count - 1;
|
|
1083
|
-
while (lo <= hi) {
|
|
1084
|
-
const mid = lo + hi >>> 1;
|
|
1085
|
-
const elem = toBigInt(data, mid * 32);
|
|
1086
|
-
if (value === elem) return true;
|
|
1087
|
-
if (value < elem) hi = mid - 1;
|
|
1088
|
-
else lo = mid + 1;
|
|
1089
|
-
}
|
|
1090
|
-
return false;
|
|
1091
|
-
}
|
|
1092
|
-
function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
1093
|
-
let base = opCode & ~Op.NOT;
|
|
1094
|
-
if (isLengthOp(base)) {
|
|
1095
|
-
value = BigInt(valueLength);
|
|
1096
|
-
typeCode = TypeCode.UINT_MAX;
|
|
1097
|
-
base = lengthToValueOp(base);
|
|
1098
|
-
}
|
|
1099
|
-
let result;
|
|
1100
|
-
switch (base) {
|
|
1101
|
-
case Op.EQ:
|
|
1102
|
-
result = value === toBigInt(operandData, 0);
|
|
1103
|
-
break;
|
|
1104
|
-
case Op.GT:
|
|
1105
|
-
result = compareTyped(value, toBigInt(operandData, 0), typeCode) > 0;
|
|
1106
|
-
break;
|
|
1107
|
-
case Op.LT:
|
|
1108
|
-
result = compareTyped(value, toBigInt(operandData, 0), typeCode) < 0;
|
|
1109
|
-
break;
|
|
1110
|
-
case Op.GTE:
|
|
1111
|
-
result = compareTyped(value, toBigInt(operandData, 0), typeCode) >= 0;
|
|
1112
|
-
break;
|
|
1113
|
-
case Op.LTE:
|
|
1114
|
-
result = compareTyped(value, toBigInt(operandData, 0), typeCode) <= 0;
|
|
1115
|
-
break;
|
|
1116
|
-
case Op.BETWEEN: {
|
|
1117
|
-
const min = toBigInt(operandData, 0);
|
|
1118
|
-
const max = toBigInt(operandData, 32);
|
|
1119
|
-
result = compareTyped(value, min, typeCode) >= 0 && compareTyped(value, max, typeCode) <= 0;
|
|
1120
|
-
break;
|
|
1121
|
-
}
|
|
1122
|
-
case Op.IN:
|
|
1123
|
-
result = isIn(value, operandData);
|
|
1124
|
-
break;
|
|
1125
|
-
case Op.BITMASK_ALL: {
|
|
1126
|
-
const mask = toBigInt(operandData, 0);
|
|
1127
|
-
result = (value & mask) === mask;
|
|
1128
|
-
break;
|
|
1129
|
-
}
|
|
1130
|
-
case Op.BITMASK_ANY: {
|
|
1131
|
-
const mask = toBigInt(operandData, 0);
|
|
1132
|
-
result = (value & mask) !== 0n;
|
|
1133
|
-
break;
|
|
1134
|
-
}
|
|
1135
|
-
case Op.BITMASK_NONE: {
|
|
1136
|
-
const mask = toBigInt(operandData, 0);
|
|
1137
|
-
result = (value & mask) === 0n;
|
|
1138
|
-
break;
|
|
1139
|
-
}
|
|
1140
|
-
default: throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
|
|
1320
|
+
next: nodeEnd
|
|
1321
|
+
};
|
|
1141
1322
|
}
|
|
1142
|
-
|
|
1323
|
+
parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
|
|
1324
|
+
return {
|
|
1325
|
+
typeCode: code,
|
|
1326
|
+
isDynamic: true,
|
|
1327
|
+
staticSize: 0,
|
|
1328
|
+
next: nodeEnd
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
function indexToPath(index) {
|
|
1332
|
+
return `0x${index.toString(16).padStart(4, "0")}`;
|
|
1333
|
+
}
|
|
1334
|
+
function decodeDescriptor(data) {
|
|
1335
|
+
if (data.length < 1) throw new CallciumError("MALFORMED_HEADER", "Descriptor is empty");
|
|
1336
|
+
const version = data[0];
|
|
1337
|
+
if (version !== DescriptorFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${DescriptorFormat.VERSION})`);
|
|
1338
|
+
if (data.length < DescriptorFormat.HEADER_SIZE) throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
|
|
1339
|
+
const params = [];
|
|
1340
|
+
let cursor = DescriptorFormat.HEADER_SIZE;
|
|
1341
|
+
while (cursor < data.length) {
|
|
1342
|
+
const { typeCode, isDynamic, staticSize, next } = parseNode(data, cursor, 1);
|
|
1343
|
+
if (params.length === DescriptorFormat.MAX_PARAMS) throw new CallciumError("TOO_MANY_PARAMS", `Descriptor exceeds the maximum of ${DescriptorFormat.MAX_PARAMS} top-level params`);
|
|
1344
|
+
params.push({
|
|
1345
|
+
index: params.length,
|
|
1346
|
+
typeCode,
|
|
1347
|
+
isDynamic,
|
|
1348
|
+
staticSize,
|
|
1349
|
+
path: indexToPath(params.length),
|
|
1350
|
+
span: {
|
|
1351
|
+
start: cursor,
|
|
1352
|
+
end: next
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
cursor = next;
|
|
1356
|
+
}
|
|
1357
|
+
const declaredCount = data[1];
|
|
1358
|
+
if (params.length !== declaredCount) throw new CallciumError("PARAM_COUNT_MISMATCH", `Header declares ${declaredCount} params but ${params.length} were parsed`);
|
|
1359
|
+
return { descriptor: {
|
|
1360
|
+
version,
|
|
1361
|
+
params
|
|
1362
|
+
} };
|
|
1143
1363
|
}
|
|
1364
|
+
const DescriptorCoder = {
|
|
1365
|
+
fromTypes,
|
|
1366
|
+
toTypes
|
|
1367
|
+
};
|
|
1144
1368
|
//#endregion
|
|
1145
1369
|
//#region src/policy-coder.ts
|
|
1146
|
-
function parsePathSteps(path) {
|
|
1147
|
-
const bytes = hexToBytes(path);
|
|
1148
|
-
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`);
|
|
1149
|
-
const steps = [];
|
|
1150
|
-
for (let offset = 0; offset < bytes.length; offset += PolicyFormat.PATH_STEP_SIZE) steps.push(readU16(bytes, offset));
|
|
1151
|
-
return steps;
|
|
1152
|
-
}
|
|
1153
1370
|
function field(value, start, end) {
|
|
1154
1371
|
return {
|
|
1155
1372
|
value,
|
|
@@ -1159,6 +1376,21 @@ function field(value, start, end) {
|
|
|
1159
1376
|
}
|
|
1160
1377
|
};
|
|
1161
1378
|
}
|
|
1379
|
+
function hexField(data, start, end) {
|
|
1380
|
+
let hex;
|
|
1381
|
+
return {
|
|
1382
|
+
get value() {
|
|
1383
|
+
return hex ??= toHex(data, start, end);
|
|
1384
|
+
},
|
|
1385
|
+
set value(replacement) {
|
|
1386
|
+
hex = replacement;
|
|
1387
|
+
},
|
|
1388
|
+
span: {
|
|
1389
|
+
start,
|
|
1390
|
+
end
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1162
1394
|
function resolveHintSize(data, hintStart, ruleEnd, ruleOffset) {
|
|
1163
1395
|
const overrun = () => {
|
|
1164
1396
|
throw new CallciumError("RULE_FIELD_OUT_OF_BOUNDS", "Rule is too small to hold a hint block", ruleOffset);
|
|
@@ -1215,7 +1447,7 @@ function hintTargetTypeCode(data, hintStart, hintSize) {
|
|
|
1215
1447
|
}
|
|
1216
1448
|
function decodePolicy(blob) {
|
|
1217
1449
|
const data = hexToBytes(blob);
|
|
1218
|
-
if (data.length < PolicyFormat.DESC_OFFSET
|
|
1450
|
+
if (data.length < PolicyFormat.DESC_OFFSET) throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short");
|
|
1219
1451
|
const headerByte = data[0];
|
|
1220
1452
|
const version = headerByte & PolicyFormat.VERSION_MASK;
|
|
1221
1453
|
if (version !== PolicyFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${PolicyFormat.VERSION})`);
|
|
@@ -1240,7 +1472,7 @@ function decodePolicy(blob) {
|
|
|
1240
1472
|
end: descStart + param.span.end
|
|
1241
1473
|
}
|
|
1242
1474
|
}));
|
|
1243
|
-
const descriptorRaw =
|
|
1475
|
+
const descriptorRaw = hexField(data, descStart, descEnd);
|
|
1244
1476
|
const groupCountStart = groupCountOffset;
|
|
1245
1477
|
const groupCountEnd = groupCountStart + PolicyFormat.GROUP_COUNT_SIZE;
|
|
1246
1478
|
const groupCountValue = data[groupCountOffset];
|
|
@@ -1253,18 +1485,18 @@ function decodePolicy(blob) {
|
|
|
1253
1485
|
const groupSizeValue = readU32(data, groupSizeStart);
|
|
1254
1486
|
const groupBodyStart = offset + PolicyFormat.GROUP_HEADER_SIZE;
|
|
1255
1487
|
const groupEnd = groupBodyStart + groupSizeValue;
|
|
1488
|
+
if (groupEnd > data.length) throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
|
|
1256
1489
|
const ruleCountStart = offset;
|
|
1257
1490
|
const ruleCountValue = readU16(data, ruleCountStart);
|
|
1258
1491
|
if (ruleCountValue === 0) throw new CallciumError("EMPTY_GROUP", "Group must contain at least one rule", offset);
|
|
1259
1492
|
if (groupSizeValue < ruleCountValue * PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("GROUP_TOO_SMALL", "Declared group size is too small for its rule count", offset);
|
|
1260
|
-
if (groupEnd > data.length) throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
|
|
1261
1493
|
const rules = [];
|
|
1262
1494
|
let ruleOffset = groupBodyStart;
|
|
1263
1495
|
for (let ruleIndex = 0; ruleIndex < ruleCountValue; ruleIndex++) {
|
|
1264
1496
|
if (ruleOffset + PolicyFormat.RULE_SIZE_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading rule size", ruleOffset);
|
|
1265
1497
|
const ruleSizeValue = readU16(data, ruleOffset);
|
|
1266
1498
|
if (ruleSizeValue < PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("RULE_TOO_SMALL", `Rule size ${ruleSizeValue} is below minimum ${PolicyFormat.RULE_MIN_SIZE}`, ruleOffset);
|
|
1267
|
-
if (ruleOffset + ruleSizeValue >
|
|
1499
|
+
if (ruleOffset + ruleSizeValue > data.length) throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond policy blob", ruleOffset);
|
|
1268
1500
|
const scopeOffset = ruleOffset + PolicyFormat.RULE_SCOPE_OFFSET;
|
|
1269
1501
|
const scopeValue = data[scopeOffset];
|
|
1270
1502
|
if (scopeValue !== Scope.CONTEXT && scopeValue !== Scope.CALLDATA) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${scopeValue}`, ruleOffset);
|
|
@@ -1293,17 +1525,22 @@ function decodePolicy(blob) {
|
|
|
1293
1525
|
const dataStart = dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE;
|
|
1294
1526
|
if (opBase === Op.IN) {
|
|
1295
1527
|
const wordCount = dataLengthValue / 32;
|
|
1296
|
-
|
|
1528
|
+
let prev = toBigInt(data, dataStart);
|
|
1529
|
+
for (let word = 1; word < wordCount; word++) {
|
|
1530
|
+
const cur = toBigInt(data, dataStart + word * 32);
|
|
1531
|
+
if (cur <= prev) throw new CallciumError("UNSORTED_IN_SET", "IN operands must be strictly ascending", ruleOffset);
|
|
1532
|
+
prev = cur;
|
|
1533
|
+
}
|
|
1297
1534
|
}
|
|
1298
1535
|
if (opBase === Op.EQ_CTX) {
|
|
1299
1536
|
if (toBigInt(data, dataStart) > BigInt(MAX_CONTEXT_PROPERTY_ID)) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", "EQ_CTX operand references an undefined context property", ruleOffset);
|
|
1300
1537
|
}
|
|
1301
1538
|
if (depthValue === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
|
|
1302
|
-
if (depthValue >
|
|
1539
|
+
if (depthValue > PolicyFormat.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depthValue} exceeds maximum ${PolicyFormat.MAX_PATH_DEPTH}`, ruleOffset);
|
|
1303
1540
|
if (scopeValue === Scope.CONTEXT) {
|
|
1304
1541
|
if (depthValue !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step", ruleOffset);
|
|
1305
1542
|
const contextPropertyId = readU16(data, pathStart);
|
|
1306
|
-
if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Context rule references undefined context property
|
|
1543
|
+
if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Context rule references undefined context property ${formatCode(contextPropertyId, PolicyFormat.PATH_STEP_SIZE)}`, ruleOffset);
|
|
1307
1544
|
}
|
|
1308
1545
|
const pathHex = toHex(data, pathStart, pathStart + pathLength);
|
|
1309
1546
|
rules.push({
|
|
@@ -1311,15 +1548,16 @@ function decodePolicy(blob) {
|
|
|
1311
1548
|
scope: field(scopeValue, scopeOffset, scopeOffset + 1),
|
|
1312
1549
|
pathDepth: field(depthValue, depthOffset, depthOffset + 1),
|
|
1313
1550
|
path: field(pathHex, pathStart, pathStart + pathLength),
|
|
1314
|
-
...hintSize > 0 && { hint:
|
|
1551
|
+
...hintSize > 0 && { hint: hexField(data, hintStart, hintStart + hintSize) },
|
|
1315
1552
|
opCode: field(opCodeValue, opCodeOffset, opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE),
|
|
1316
1553
|
dataLength: field(dataLengthValue, dataLengthOffset, dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE),
|
|
1317
|
-
data:
|
|
1554
|
+
data: hexField(data, dataStart, dataStart + dataLengthValue),
|
|
1318
1555
|
span: {
|
|
1319
1556
|
start: ruleOffset,
|
|
1320
1557
|
end: ruleOffset + ruleSizeValue
|
|
1321
1558
|
}
|
|
1322
1559
|
});
|
|
1560
|
+
if (ruleOffset + ruleSizeValue > groupEnd) throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
|
|
1323
1561
|
ruleOffset += ruleSizeValue;
|
|
1324
1562
|
}
|
|
1325
1563
|
if (ruleOffset !== groupEnd) throw new CallciumError("GROUP_SIZE_MISMATCH", "Rules do not exactly fill the declared group size", offset);
|
|
@@ -1341,7 +1579,12 @@ function decodePolicy(blob) {
|
|
|
1341
1579
|
selector: field(selectorHex, selectorStart, selectorEnd),
|
|
1342
1580
|
descLength: field(descLengthValue, descLengthStart, descLengthStart + PolicyFormat.DESC_LENGTH_SIZE),
|
|
1343
1581
|
descriptor: {
|
|
1344
|
-
raw
|
|
1582
|
+
get raw() {
|
|
1583
|
+
return descriptorRaw.value;
|
|
1584
|
+
},
|
|
1585
|
+
set raw(replacement) {
|
|
1586
|
+
descriptorRaw.value = replacement;
|
|
1587
|
+
},
|
|
1345
1588
|
header: field({
|
|
1346
1589
|
version: desc.version,
|
|
1347
1590
|
paramCount: desc.params.length
|
|
@@ -1364,21 +1607,28 @@ function decodePolicy(blob) {
|
|
|
1364
1607
|
data
|
|
1365
1608
|
};
|
|
1366
1609
|
}
|
|
1367
|
-
function
|
|
1368
|
-
const operators = constraint.operators.map(hexToBytes);
|
|
1369
|
-
for (const operator of operators) if (operator.length < 1) throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode).");
|
|
1610
|
+
function readConstraintTarget(constraint, desc) {
|
|
1370
1611
|
const path = hexToBytes(constraint.path);
|
|
1371
|
-
if (path.length === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step
|
|
1372
|
-
if ((path.length & 1) !== 0) throw new CallciumError("MALFORMED_PATH", "Path byte length must be even
|
|
1612
|
+
if (path.length === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step");
|
|
1613
|
+
if ((path.length & 1) !== 0) throw new CallciumError("MALFORMED_PATH", "Path byte length must be even");
|
|
1373
1614
|
const depth = path.length / 2;
|
|
1374
|
-
if (depth >
|
|
1375
|
-
|
|
1376
|
-
return operators.map((operator) => ({
|
|
1377
|
-
scope: constraint.scope,
|
|
1615
|
+
if (depth > PolicyFormat.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${PolicyFormat.MAX_PATH_DEPTH}`);
|
|
1616
|
+
return {
|
|
1378
1617
|
path,
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1618
|
+
hint: constraint.scope === Scope.CALLDATA ? Descriptor.compileHint(desc, parsePathSteps(constraint.path)) : new Uint8Array(0)
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
function flattenConstraint(constraint, desc) {
|
|
1622
|
+
let target;
|
|
1623
|
+
return constraint.operators.map((operatorHex) => {
|
|
1624
|
+
const operator = toOperatorBytes(operatorHex);
|
|
1625
|
+
target ??= readConstraintTarget(constraint, desc);
|
|
1626
|
+
return {
|
|
1627
|
+
scope: constraint.scope,
|
|
1628
|
+
operator,
|
|
1629
|
+
...target
|
|
1630
|
+
};
|
|
1631
|
+
});
|
|
1382
1632
|
}
|
|
1383
1633
|
function compareBytes(a, b) {
|
|
1384
1634
|
const length = Math.min(a.length, b.length);
|
|
@@ -1396,12 +1646,16 @@ function sortRules(rules) {
|
|
|
1396
1646
|
return compareBytes(a.operator, b.operator);
|
|
1397
1647
|
});
|
|
1398
1648
|
}
|
|
1649
|
+
function requireFieldFits(value, byteWidth, code, subject) {
|
|
1650
|
+
const max = fieldMax(byteWidth);
|
|
1651
|
+
if (value > max) throw new CallciumError(code, `${subject} ${value} exceeds maximum ${max}`);
|
|
1652
|
+
}
|
|
1399
1653
|
function encodeRule(rule) {
|
|
1400
1654
|
const depth = rule.path.length / 2;
|
|
1401
|
-
if (rule.scope === Scope.CONTEXT && depth !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step
|
|
1655
|
+
if (rule.scope === Scope.CONTEXT && depth !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step");
|
|
1402
1656
|
const data = rule.operator.subarray(1);
|
|
1403
1657
|
const ruleSize = PolicyFormat.RULE_FIXED_OVERHEAD + rule.path.length + rule.hint.length + data.length;
|
|
1404
|
-
|
|
1658
|
+
requireFieldFits(ruleSize, PolicyFormat.RULE_SIZE_SIZE, "RULE_SIZE_OVERFLOW", "Rule size");
|
|
1405
1659
|
const buf = new Uint8Array(ruleSize);
|
|
1406
1660
|
writeBE16(buf, 0, ruleSize);
|
|
1407
1661
|
buf[2] = rule.scope;
|
|
@@ -1431,16 +1685,16 @@ function buildOperatorHex(rule) {
|
|
|
1431
1685
|
function encode(data) {
|
|
1432
1686
|
const descBytes = hexToBytes(data.descriptor);
|
|
1433
1687
|
if (data.groups.length === 0) throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
|
|
1434
|
-
|
|
1688
|
+
requireFieldFits(data.groups.length, PolicyFormat.GROUP_COUNT_SIZE, "GROUP_COUNT_OVERFLOW", "Group count");
|
|
1435
1689
|
const sortedGroups = data.groups.map((group) => {
|
|
1436
1690
|
const rules = group.flatMap((constraint) => flattenConstraint(constraint, descBytes));
|
|
1437
1691
|
sortRules(rules);
|
|
1438
1692
|
return rules;
|
|
1439
1693
|
});
|
|
1440
|
-
|
|
1694
|
+
requireFieldFits(descBytes.length, PolicyFormat.DESC_LENGTH_SIZE, "DESC_LENGTH_OVERFLOW", "Descriptor length");
|
|
1441
1695
|
const encodedGroups = sortedGroups.map((rules, groupIndex) => {
|
|
1442
1696
|
if (rules.length === 0) throw new CallciumError("EMPTY_GROUP", `Group ${groupIndex} is empty`);
|
|
1443
|
-
|
|
1697
|
+
requireFieldFits(rules.length, PolicyFormat.GROUP_RULECOUNT_SIZE, "RULE_COUNT_OVERFLOW", `Group ${groupIndex} rule count`);
|
|
1444
1698
|
return {
|
|
1445
1699
|
wireBytes: encodeGroupRules(rules),
|
|
1446
1700
|
ruleCount: rules.length
|
|
@@ -1450,7 +1704,8 @@ function encode(data) {
|
|
|
1450
1704
|
const hashes = new Map(encodedGroups.map((group) => [group, keccak_256(group.wireBytes)]));
|
|
1451
1705
|
encodedGroups.sort((a, b) => compareBytes(hashes.get(a), hashes.get(b)));
|
|
1452
1706
|
}
|
|
1453
|
-
const selectorBytes = data.isSelectorless ? new Uint8Array(
|
|
1707
|
+
const selectorBytes = data.isSelectorless ? new Uint8Array(PolicyFormat.SELECTOR_SIZE) : hexToBytes(data.selector);
|
|
1708
|
+
if (selectorBytes.length !== PolicyFormat.SELECTOR_SIZE) throw new CallciumError("MALFORMED_SELECTOR", `Selector must be ${PolicyFormat.SELECTOR_SIZE} bytes, got ${selectorBytes.length}`);
|
|
1454
1709
|
const headerByte = PolicyFormat.VERSION | (data.isSelectorless ? PolicyFormat.FLAG_NO_SELECTOR : 0);
|
|
1455
1710
|
let totalSize = PolicyFormat.HEADER_SIZE + PolicyFormat.SELECTOR_SIZE + PolicyFormat.DESC_LENGTH_SIZE + descBytes.length + PolicyFormat.GROUP_COUNT_SIZE;
|
|
1456
1711
|
for (const g of encodedGroups) totalSize += PolicyFormat.GROUP_HEADER_SIZE + g.wireBytes.length;
|
|
@@ -1478,7 +1733,6 @@ function decode(blob) {
|
|
|
1478
1733
|
const { policy } = decodePolicy(blob);
|
|
1479
1734
|
const groups = policy.groups.map((group) => {
|
|
1480
1735
|
const constraintMap = /* @__PURE__ */ new Map();
|
|
1481
|
-
const constraintOrder = [];
|
|
1482
1736
|
for (const rule of group.rules) {
|
|
1483
1737
|
const hintHex = rule.hint?.value;
|
|
1484
1738
|
const key = `${rule.scope.value}:${rule.path.value}:${hintHex ?? ""}`;
|
|
@@ -1494,10 +1748,9 @@ function decode(blob) {
|
|
|
1494
1748
|
span: rule.span
|
|
1495
1749
|
};
|
|
1496
1750
|
constraintMap.set(key, constraint);
|
|
1497
|
-
constraintOrder.push(key);
|
|
1498
1751
|
}
|
|
1499
1752
|
}
|
|
1500
|
-
return
|
|
1753
|
+
return [...constraintMap.values()];
|
|
1501
1754
|
});
|
|
1502
1755
|
return {
|
|
1503
1756
|
isSelectorless: policy.isSelectorless,
|
|
@@ -1743,7 +1996,7 @@ function unsortedInSet(groupIndex, constraintIndex) {
|
|
|
1743
1996
|
code: "UNSORTED_IN_SET",
|
|
1744
1997
|
value1: ZERO,
|
|
1745
1998
|
value2: ZERO,
|
|
1746
|
-
message: "isIn/notIn set is not strictly sorted and deduplicated
|
|
1999
|
+
message: "isIn/notIn set is not strictly sorted and deduplicated"
|
|
1747
2000
|
};
|
|
1748
2001
|
}
|
|
1749
2002
|
function emptyGroup(groupIndex) {
|
|
@@ -1755,7 +2008,7 @@ function emptyGroup(groupIndex) {
|
|
|
1755
2008
|
code: "EMPTY_GROUP",
|
|
1756
2009
|
value1: ZERO,
|
|
1757
2010
|
value2: ZERO,
|
|
1758
|
-
message: "Group contains zero constraints
|
|
2011
|
+
message: "Group contains zero constraints"
|
|
1759
2012
|
};
|
|
1760
2013
|
}
|
|
1761
2014
|
function dominatedBound(isLength, groupIndex, constraintIndex, value) {
|
|
@@ -1940,8 +2193,7 @@ function negationUnderAny(groupIndex, constraintIndex, opCode) {
|
|
|
1940
2193
|
}
|
|
1941
2194
|
//#endregion
|
|
1942
2195
|
//#region src/policy-validator.ts
|
|
1943
|
-
const
|
|
1944
|
-
"EMPTY_PATH",
|
|
2196
|
+
const navigabilityCodes = new Set([
|
|
1945
2197
|
"PARAM_INDEX_OUT_OF_BOUNDS",
|
|
1946
2198
|
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
1947
2199
|
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
@@ -1952,8 +2204,11 @@ const UINT256_MAX = (1n << 256n) - 1n;
|
|
|
1952
2204
|
const INT256_SIGN_BIT = 1n << 255n;
|
|
1953
2205
|
function signedCompare(a, b, signed) {
|
|
1954
2206
|
if (!signed) return a - b;
|
|
1955
|
-
const
|
|
1956
|
-
return
|
|
2207
|
+
const toSigned = (word) => word >= INT256_SIGN_BIT ? word - (1n << 256n) : word;
|
|
2208
|
+
return toSigned(a) - toSigned(b);
|
|
2209
|
+
}
|
|
2210
|
+
function isOutsideDomain(domain, value) {
|
|
2211
|
+
return signedCompare(value, domain.min, domain.isSigned) < 0n || signedCompare(value, domain.max, domain.isSigned) > 0n;
|
|
1957
2212
|
}
|
|
1958
2213
|
function isNumericType(typeCode) {
|
|
1959
2214
|
return typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX || typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
|
|
@@ -2011,7 +2266,7 @@ function getIncompat(opBase, typeInfo) {
|
|
|
2011
2266
|
code: "VALUE_OP_ON_DYNAMIC",
|
|
2012
2267
|
message: "Value operator used on dynamic type"
|
|
2013
2268
|
};
|
|
2014
|
-
if (
|
|
2269
|
+
if (!isElementary(typeCode)) return {
|
|
2015
2270
|
code: "VALUE_OP_ON_COMPOSITE",
|
|
2016
2271
|
message: "Value operator used on composite type"
|
|
2017
2272
|
};
|
|
@@ -2122,7 +2377,7 @@ function initContext(scope, path, steps, typeInfo) {
|
|
|
2122
2377
|
numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
|
|
2123
2378
|
bitmask: {
|
|
2124
2379
|
mustBeOne: 0n,
|
|
2125
|
-
mustBeZero: 0n
|
|
2380
|
+
mustBeZero: isBitmaskCompatible(typeInfo.typeCode) ? UINT256_MAX ^ max : 0n
|
|
2126
2381
|
},
|
|
2127
2382
|
length: emptyBoundDomain(false, 0n, LENGTH_MAX),
|
|
2128
2383
|
set: {
|
|
@@ -2135,6 +2390,7 @@ function initContext(scope, path, steps, typeInfo) {
|
|
|
2135
2390
|
function updateBound(domain, base, isNegated, value, isLength, groupIndex, constraintIndex, issues) {
|
|
2136
2391
|
if (isNegated) {
|
|
2137
2392
|
if (base === Op.EQ) {
|
|
2393
|
+
if (isOutsideDomain(domain, value)) issues.push(outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2138
2394
|
if (domain.hasEq && domain.eq === value) issues.push(eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2139
2395
|
let alreadyHole = false;
|
|
2140
2396
|
for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) {
|
|
@@ -2147,7 +2403,7 @@ function updateBound(domain, base, isNegated, value, isLength, groupIndex, const
|
|
|
2147
2403
|
}
|
|
2148
2404
|
if (base === Op.GTE && value === domain.min) issues.push(vacuousGte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2149
2405
|
else if (base === Op.LTE && value === domain.max) issues.push(vacuousLte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2150
|
-
if (
|
|
2406
|
+
if (isOutsideDomain(domain, value)) issues.push(outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2151
2407
|
else if (base === Op.GT && value === domain.max) issues.push(impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2152
2408
|
else if (base === Op.LT && value === domain.min) issues.push(impossibleLt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2153
2409
|
let changedEq = false;
|
|
@@ -2262,6 +2518,7 @@ function checkSetEmpty(ctx, groupIndex, constraintIndex, issues) {
|
|
|
2262
2518
|
else if (possibleCount < inCount) issues.push(setPartiallyExcluded(groupIndex, constraintIndex, bigintToHex(BigInt(inCount - possibleCount))));
|
|
2263
2519
|
}
|
|
2264
2520
|
function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues) {
|
|
2521
|
+
for (const value of values) if (isOutsideDomain(ctx.numeric, value)) issues.push(outOfPhysicalBounds(false, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2265
2522
|
if (isNegated) {
|
|
2266
2523
|
for (const value of values) {
|
|
2267
2524
|
if (ctx.numeric.hasEq && ctx.numeric.eq === value) issues.push(setExcludesEquality(groupIndex, constraintIndex, bigintToHex(value)));
|
|
@@ -2277,7 +2534,6 @@ function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues)
|
|
|
2277
2534
|
}
|
|
2278
2535
|
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
2279
2536
|
} else {
|
|
2280
|
-
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)));
|
|
2281
2537
|
if (ctx.numeric.hasEq) {
|
|
2282
2538
|
let found = false;
|
|
2283
2539
|
for (const candidate of values) if (candidate === ctx.numeric.eq) {
|
|
@@ -2319,6 +2575,9 @@ function checkFusibleRange(ctx, operators, groupIndex, constraintIndex, issues)
|
|
|
2319
2575
|
const lengthPair = findLonePair(operators, Op.LENGTH_GTE, Op.LENGTH_LTE);
|
|
2320
2576
|
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)));
|
|
2321
2577
|
}
|
|
2578
|
+
function hasValidPayload(opBase, dataLength) {
|
|
2579
|
+
return dataLength <= fieldMax(PolicyFormat.RULE_DATALENGTH_SIZE) && isValidOperatorData(opBase, dataLength);
|
|
2580
|
+
}
|
|
2322
2581
|
function findLonePair(operators, lowerOp, upperOp) {
|
|
2323
2582
|
let lowerCount = 0;
|
|
2324
2583
|
let upperCount = 0;
|
|
@@ -2327,7 +2586,7 @@ function findLonePair(operators, lowerOp, upperOp) {
|
|
|
2327
2586
|
for (const opHex of operators) {
|
|
2328
2587
|
const opCode = parseInt(opHex.slice(2, 4), 16);
|
|
2329
2588
|
const dataLength = (opHex.length - 4) / 2;
|
|
2330
|
-
if (
|
|
2589
|
+
if (!hasValidPayload(opCode & ~Op.NOT, dataLength)) continue;
|
|
2331
2590
|
if (opCode === lowerOp) {
|
|
2332
2591
|
lowerCount++;
|
|
2333
2592
|
low = readValue(opHex);
|
|
@@ -2345,10 +2604,10 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
|
|
|
2345
2604
|
const operators = constraint.operators;
|
|
2346
2605
|
const underAny = constraint.scope === Scope.CALLDATA && ctx.steps.includes(Quantifier.ANY);
|
|
2347
2606
|
for (const opHex of operators) {
|
|
2348
|
-
const
|
|
2607
|
+
const opBytes = toOperatorBytes(opHex);
|
|
2608
|
+
const opCode = opBytes[0];
|
|
2349
2609
|
const base = opCode & ~Op.NOT;
|
|
2350
|
-
|
|
2351
|
-
if (dataLength > 65535 || !isValidOperatorData(base, dataLength)) {
|
|
2610
|
+
if (!hasValidPayload(base, opBytes.length - 1)) {
|
|
2352
2611
|
issues.push(fromOpRule("UNKNOWN_OPERATOR", "Unknown operator code", groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
2353
2612
|
continue;
|
|
2354
2613
|
}
|
|
@@ -2361,8 +2620,9 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
|
|
|
2361
2620
|
}
|
|
2362
2621
|
if (base === Op.EQ_CTX) {
|
|
2363
2622
|
const ctxOperand = readValue(opHex);
|
|
2364
|
-
|
|
2365
|
-
|
|
2623
|
+
const property = findContextProperty(Number(ctxOperand));
|
|
2624
|
+
if (!property) issues.push(unknownContextProperty(groupIndex, constraintIndex, bigintToHex(ctxOperand), bigintToHex(BigInt(MAX_CONTEXT_PROPERTY_ID))));
|
|
2625
|
+
else if (property.typeCode === TypeCode.ADDRESS !== (ctx.typeInfo.typeCode === TypeCode.ADDRESS)) issues.push(contextTypeMismatch(groupIndex, constraintIndex, bigintToHex(ctxOperand), bigintToHex(BigInt(ctx.typeInfo.typeCode))));
|
|
2366
2626
|
} else if (isValueOp(base)) {
|
|
2367
2627
|
const typeCode = ctx.typeInfo.typeCode;
|
|
2368
2628
|
if (isLeftAligned(typeCode)) {
|
|
@@ -2416,14 +2676,15 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2416
2676
|
}
|
|
2417
2677
|
if (!ctx) {
|
|
2418
2678
|
const steps = parsePathSteps(constraint.path);
|
|
2679
|
+
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
2419
2680
|
let typeInfo;
|
|
2420
2681
|
if (constraint.scope === Scope.CALLDATA) {
|
|
2421
|
-
if (steps.length >
|
|
2682
|
+
if (steps.length > PolicyFormat.MAX_PATH_DEPTH) issues.push(pathDepthExceeded(groupIndex, constraintIndex, bigintToHex(BigInt(steps.length)), bigintToHex(BigInt(PolicyFormat.MAX_PATH_DEPTH))));
|
|
2422
2683
|
let walk;
|
|
2423
2684
|
try {
|
|
2424
2685
|
walk = Descriptor.walkPath(descBytes, steps);
|
|
2425
2686
|
} catch (error) {
|
|
2426
|
-
if (error instanceof CallciumError &&
|
|
2687
|
+
if (error instanceof CallciumError && navigabilityCodes.has(error.code)) {
|
|
2427
2688
|
issues.push(unnavigablePath(groupIndex, constraintIndex));
|
|
2428
2689
|
continue;
|
|
2429
2690
|
}
|
|
@@ -2433,19 +2694,14 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2433
2694
|
issues.push(nestedQuantifier(groupIndex, constraintIndex));
|
|
2434
2695
|
continue;
|
|
2435
2696
|
}
|
|
2436
|
-
if (walk.quantifiedStaticLength >
|
|
2697
|
+
if (walk.quantifiedStaticLength > PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH) issues.push(quantifierOverStaticLimit(groupIndex, constraintIndex, bigintToHex(BigInt(walk.quantifiedStaticLength)), bigintToHex(BigInt(PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH))));
|
|
2437
2698
|
typeInfo = walk.typeInfo;
|
|
2438
2699
|
} else {
|
|
2439
2700
|
const ctxId = steps[0];
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
isDynamic: false,
|
|
2445
|
-
staticSize: 32
|
|
2446
|
-
};
|
|
2447
|
-
} else typeInfo = {
|
|
2448
|
-
typeCode: lookupContextProperty(ctxId).typeCode,
|
|
2701
|
+
const property = findContextProperty(ctxId);
|
|
2702
|
+
if (!property) issues.push(unknownContextProperty(groupIndex, constraintIndex, bigintToHex(BigInt(ctxId)), bigintToHex(BigInt(MAX_CONTEXT_PROPERTY_ID))));
|
|
2703
|
+
typeInfo = {
|
|
2704
|
+
typeCode: property?.typeCode ?? TypeCode.UINT_MAX,
|
|
2449
2705
|
isDynamic: false,
|
|
2450
2706
|
staticSize: 32
|
|
2451
2707
|
};
|
|
@@ -2453,7 +2709,7 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2453
2709
|
ctx = initContext(constraint.scope, normalizedPath, steps, typeInfo);
|
|
2454
2710
|
contexts.push(ctx);
|
|
2455
2711
|
}
|
|
2456
|
-
if (constraint.scope === Scope.CALLDATA && constraint.hint !== void 0 && ctx.steps.length <=
|
|
2712
|
+
if (constraint.scope === Scope.CALLDATA && constraint.hint !== void 0 && constraint.hint !== "0x" && ctx.steps.length <= PolicyFormat.MAX_PATH_DEPTH) {
|
|
2457
2713
|
if (bytesToHex(Descriptor.compileHint(descBytes, ctx.steps)) !== constraint.hint.toLowerCase()) issues.push(hintMismatch(groupIndex, constraintIndex));
|
|
2458
2714
|
}
|
|
2459
2715
|
validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues);
|
|
@@ -2476,33 +2732,31 @@ function validate(data) {
|
|
|
2476
2732
|
const PolicyValidator = { validate };
|
|
2477
2733
|
//#endregion
|
|
2478
2734
|
//#region src/signature.ts
|
|
2479
|
-
function isAlpha(
|
|
2480
|
-
return
|
|
2735
|
+
function isAlpha(char) {
|
|
2736
|
+
return char >= 65 && char <= 90 || char >= 97 && char <= 122;
|
|
2481
2737
|
}
|
|
2482
|
-
function isAlphanum(
|
|
2483
|
-
return isAlpha(
|
|
2738
|
+
function isAlphanum(char) {
|
|
2739
|
+
return isAlpha(char) || char >= 48 && char <= 57;
|
|
2484
2740
|
}
|
|
2485
2741
|
function parse(signature) {
|
|
2486
2742
|
if (signature.length < 3) throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: too short, got "${signature}"`);
|
|
2487
2743
|
let openParen = -1;
|
|
2488
2744
|
for (let i = 0; i < signature.length; i++) {
|
|
2489
|
-
const
|
|
2490
|
-
if (
|
|
2491
|
-
if (
|
|
2492
|
-
if (openParen === -1 && c === 40) openParen = i;
|
|
2745
|
+
const char = signature.charCodeAt(i);
|
|
2746
|
+
if (char === 32 || char === 9 || char === 10 || char === 13) throw new CallciumError("SIGNATURE_CONTAINS_WHITESPACE", "Signature must not contain whitespace");
|
|
2747
|
+
if (openParen === -1 && char === 40) openParen = i;
|
|
2493
2748
|
}
|
|
2494
|
-
if (openParen
|
|
2749
|
+
if (openParen === -1) throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: must have a function name followed by parentheses, got "${signature}"`);
|
|
2495
2750
|
if (!signature.endsWith(")")) throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: must end with ")", got "${signature}"`);
|
|
2751
|
+
if (openParen === 0) throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must not be empty");
|
|
2496
2752
|
const firstChar = signature.charCodeAt(0);
|
|
2497
2753
|
if (!isAlpha(firstChar) && firstChar !== 95) throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must start with a letter or underscore");
|
|
2498
2754
|
for (let i = 1; i < openParen; i++) {
|
|
2499
|
-
const
|
|
2500
|
-
if (!isAlphanum(
|
|
2755
|
+
const char = signature.charCodeAt(i);
|
|
2756
|
+
if (!isAlphanum(char) && char !== 95) throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must contain only alphanumeric characters or underscores");
|
|
2501
2757
|
}
|
|
2502
|
-
const encoded = new Uint8Array(signature.length);
|
|
2503
|
-
for (let i = 0; i < signature.length; i++) encoded[i] = signature.charCodeAt(i);
|
|
2504
2758
|
return {
|
|
2505
|
-
selector: bytesToHex(keccak_256(
|
|
2759
|
+
selector: bytesToHex(keccak_256(utf8ToBytes(signature)).subarray(0, 4)),
|
|
2506
2760
|
types: signature.slice(openParen + 1, signature.length - 1)
|
|
2507
2761
|
};
|
|
2508
2762
|
}
|
|
@@ -2510,14 +2764,13 @@ const SignatureParser = { parse };
|
|
|
2510
2764
|
//#endregion
|
|
2511
2765
|
//#region src/policy-builder.ts
|
|
2512
2766
|
function validateContextPath(steps) {
|
|
2513
|
-
if (steps.length !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step
|
|
2514
|
-
|
|
2515
|
-
if (step > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ID 0x${step.toString(16).padStart(4, "0")}.`);
|
|
2767
|
+
if (steps.length !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step");
|
|
2768
|
+
return lookupContextProperty(steps[0]).typeCode;
|
|
2516
2769
|
}
|
|
2517
2770
|
function validateCalldataPath(steps, desc) {
|
|
2518
2771
|
const argIndex = steps[0];
|
|
2519
2772
|
const paramCount = Descriptor.paramCount(desc);
|
|
2520
|
-
if (argIndex >= paramCount) throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${argIndex} out of range (descriptor has ${paramCount} params)
|
|
2773
|
+
if (argIndex >= paramCount) throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${argIndex} out of range (descriptor has ${paramCount} params)`);
|
|
2521
2774
|
let offset = Descriptor.paramOffset(desc, argIndex);
|
|
2522
2775
|
let hasQuantifier = false;
|
|
2523
2776
|
for (let i = 1; i < steps.length; i++) {
|
|
@@ -2525,22 +2778,49 @@ function validateCalldataPath(steps, desc) {
|
|
|
2525
2778
|
const info = Descriptor.inspect(desc, offset);
|
|
2526
2779
|
const isArray = info.typeCode === TypeCode.STATIC_ARRAY || info.typeCode === TypeCode.DYNAMIC_ARRAY;
|
|
2527
2780
|
if (isQuantifier(step)) {
|
|
2528
|
-
if (!isArray) throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node
|
|
2529
|
-
if (hasQuantifier) throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed
|
|
2781
|
+
if (!isArray) throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node");
|
|
2782
|
+
if (hasQuantifier) throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed");
|
|
2530
2783
|
hasQuantifier = true;
|
|
2531
2784
|
}
|
|
2532
2785
|
if (info.typeCode === TypeCode.TUPLE) {
|
|
2533
2786
|
const fieldCount = Descriptor.tupleFieldCount(desc, offset);
|
|
2534
|
-
if (step >= fieldCount) throw new CallciumError("TUPLE_FIELD_OUT_OF_BOUNDS", `Tuple field index ${step} out of range (tuple has ${fieldCount} fields)
|
|
2787
|
+
if (step >= fieldCount) throw new CallciumError("TUPLE_FIELD_OUT_OF_BOUNDS", `Tuple field index ${step} out of range (tuple has ${fieldCount} fields)`);
|
|
2535
2788
|
offset = Descriptor.tupleFieldOffset(desc, offset, step);
|
|
2536
2789
|
} else if (isArray) {
|
|
2537
2790
|
if (!isQuantifier(step) && info.typeCode === TypeCode.STATIC_ARRAY) {
|
|
2538
2791
|
const arrayLength = Descriptor.staticArrayLength(desc, offset);
|
|
2539
|
-
if (step >= arrayLength) throw new CallciumError("STATIC_ARRAY_INDEX_OUT_OF_BOUNDS", `Array index ${step} out of range (static array has ${arrayLength} elements)
|
|
2792
|
+
if (step >= arrayLength) throw new CallciumError("STATIC_ARRAY_INDEX_OUT_OF_BOUNDS", `Array index ${step} out of range (static array has ${arrayLength} elements)`);
|
|
2540
2793
|
}
|
|
2541
2794
|
offset = Descriptor.arrayElementOffset(offset);
|
|
2542
|
-
} else throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type
|
|
2795
|
+
} else throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type");
|
|
2796
|
+
}
|
|
2797
|
+
return Descriptor.inspect(desc, offset).typeCode;
|
|
2798
|
+
}
|
|
2799
|
+
const WORD_VALUES = 1n << 256n;
|
|
2800
|
+
function targetBounds(typeCode) {
|
|
2801
|
+
if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) return {
|
|
2802
|
+
min: 0n,
|
|
2803
|
+
max: (1n << BigInt(typeCode - TypeCode.UINT_MIN + 1) * 8n) - 1n
|
|
2804
|
+
};
|
|
2805
|
+
if (typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX) {
|
|
2806
|
+
const half = 1n << BigInt(typeCode - TypeCode.INT_MIN + 1) * 8n - 1n;
|
|
2807
|
+
return {
|
|
2808
|
+
min: -half,
|
|
2809
|
+
max: half - 1n
|
|
2810
|
+
};
|
|
2543
2811
|
}
|
|
2812
|
+
return null;
|
|
2813
|
+
}
|
|
2814
|
+
function reject(written, folded) {
|
|
2815
|
+
throw new CallciumError("OUT_OF_PHYSICAL_BOUNDS", `Operand ${written} is outside the physical range of the type and encodes as ${folded}`);
|
|
2816
|
+
}
|
|
2817
|
+
function checkOperandDomain(constraint, typeCode) {
|
|
2818
|
+
const bounds = targetBounds(typeCode);
|
|
2819
|
+
const extremes = readOperandExtremes(constraint);
|
|
2820
|
+
if (bounds === null || extremes === void 0) return;
|
|
2821
|
+
if (extremes.leastNegative < 0n && typeCode === TypeCode.UINT_MAX) reject(extremes.leastNegative, extremes.leastNegative + WORD_VALUES);
|
|
2822
|
+
if (!isSigned(typeCode)) return;
|
|
2823
|
+
if (extremes.greatest >= WORD_VALUES + bounds.min) reject(extremes.greatest, extremes.greatest - WORD_VALUES);
|
|
2544
2824
|
}
|
|
2545
2825
|
var PolicyBuilder = class PolicyBuilder {
|
|
2546
2826
|
draft;
|
|
@@ -2555,7 +2835,7 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2555
2835
|
selector: parsed.selector,
|
|
2556
2836
|
descriptor,
|
|
2557
2837
|
groups: [[]],
|
|
2558
|
-
|
|
2838
|
+
groupPathKeys: /* @__PURE__ */ new Set()
|
|
2559
2839
|
});
|
|
2560
2840
|
}
|
|
2561
2841
|
static createRaw(typesCsv) {
|
|
@@ -2564,32 +2844,34 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2564
2844
|
selector: "0x00000000",
|
|
2565
2845
|
descriptor: DescriptorCoder.fromTypes(typesCsv),
|
|
2566
2846
|
groups: [[]],
|
|
2567
|
-
|
|
2847
|
+
groupPathKeys: /* @__PURE__ */ new Set()
|
|
2568
2848
|
});
|
|
2569
2849
|
}
|
|
2570
2850
|
add(constraint) {
|
|
2571
|
-
const
|
|
2851
|
+
const added = {
|
|
2572
2852
|
scope: constraint.scope,
|
|
2573
2853
|
path: constraint.path,
|
|
2574
|
-
operators: [...constraint.operators]
|
|
2854
|
+
operators: [...constraint.operators],
|
|
2855
|
+
..."hint" in constraint && constraint.hint !== void 0 && { hint: constraint.hint }
|
|
2575
2856
|
};
|
|
2576
|
-
if (
|
|
2577
|
-
const steps = parsePathSteps(
|
|
2578
|
-
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
else
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
this.draft.
|
|
2857
|
+
if (added.operators.length === 0) throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator");
|
|
2858
|
+
const steps = parsePathSteps(added.path);
|
|
2859
|
+
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
2860
|
+
let targetTypeCode;
|
|
2861
|
+
if (added.scope === Scope.CONTEXT) targetTypeCode = validateContextPath(steps);
|
|
2862
|
+
else if (added.scope === Scope.CALLDATA) targetTypeCode = validateCalldataPath(steps, this.draft.descriptor);
|
|
2863
|
+
else throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${added.scope}`);
|
|
2864
|
+
checkOperandDomain(constraint, targetTypeCode);
|
|
2865
|
+
const key = `${added.scope}:${added.path.toLowerCase()}`;
|
|
2866
|
+
if (this.draft.groupPathKeys.has(key)) throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${added.path} in the same group`);
|
|
2867
|
+
this.draft.groupPathKeys.add(key);
|
|
2868
|
+
this.draft.groups[this.draft.groups.length - 1].push(added);
|
|
2587
2869
|
return this;
|
|
2588
2870
|
}
|
|
2589
2871
|
or() {
|
|
2590
|
-
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
|
|
2872
|
+
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");
|
|
2591
2873
|
this.draft.groups.push([]);
|
|
2592
|
-
this.draft.
|
|
2874
|
+
this.draft.groupPathKeys.clear();
|
|
2593
2875
|
return this;
|
|
2594
2876
|
}
|
|
2595
2877
|
build() {
|
|
@@ -2609,7 +2891,7 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2609
2891
|
return PolicyValidator.validate(policyData);
|
|
2610
2892
|
}
|
|
2611
2893
|
checkGroups() {
|
|
2612
|
-
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
|
|
2894
|
+
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`);
|
|
2613
2895
|
}
|
|
2614
2896
|
toPolicyData() {
|
|
2615
2897
|
return {
|
|
@@ -2621,176 +2903,16 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2621
2903
|
}
|
|
2622
2904
|
};
|
|
2623
2905
|
//#endregion
|
|
2624
|
-
//#region src/constraint.ts
|
|
2625
|
-
function addressBody(value) {
|
|
2626
|
-
return toAddress(value).slice(2);
|
|
2627
|
-
}
|
|
2628
|
-
function encodeWord(value) {
|
|
2629
|
-
const word = new Uint8Array(32);
|
|
2630
|
-
if (typeof value === "boolean") {
|
|
2631
|
-
word[31] = value ? 1 : 0;
|
|
2632
|
-
return word;
|
|
2633
|
-
}
|
|
2634
|
-
if (typeof value === "string") {
|
|
2635
|
-
const body = addressBody(value);
|
|
2636
|
-
for (let i = 0; i < 20; i++) word[12 + i] = parseInt(body.substring(i * 2, i * 2 + 2), 16);
|
|
2637
|
-
return word;
|
|
2638
|
-
}
|
|
2639
|
-
let bigValue = typeof value === "number" ? BigInt(value) : value;
|
|
2640
|
-
for (let i = 31; i >= 0; i--) {
|
|
2641
|
-
word[i] = Number(bigValue & 255n);
|
|
2642
|
-
bigValue >>= 8n;
|
|
2643
|
-
}
|
|
2644
|
-
return word;
|
|
2645
|
-
}
|
|
2646
|
-
function singleOp(opCode, value) {
|
|
2647
|
-
const buffer = new Uint8Array(33);
|
|
2648
|
-
buffer[0] = opCode;
|
|
2649
|
-
buffer.set(encodeWord(value), 1);
|
|
2650
|
-
return bytesToHex(buffer);
|
|
2651
|
-
}
|
|
2652
|
-
function rangeOp(opCode, min, max) {
|
|
2653
|
-
if (min > max) throw new CallciumError("INVALID_RANGE", `Range min (${min}) must not exceed max (${max})`);
|
|
2654
|
-
const buffer = new Uint8Array(65);
|
|
2655
|
-
buffer[0] = opCode;
|
|
2656
|
-
buffer.set(encodeWord(min), 1);
|
|
2657
|
-
buffer.set(encodeWord(max), 33);
|
|
2658
|
-
return bytesToHex(buffer);
|
|
2659
|
-
}
|
|
2660
|
-
function checkContextPropertyId(contextPropertyId) {
|
|
2661
|
-
if (!Number.isInteger(contextPropertyId) || contextPropertyId < 0 || contextPropertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ID ${contextPropertyId}`);
|
|
2662
|
-
return contextPropertyId;
|
|
2663
|
-
}
|
|
2664
|
-
const MAX_SET_MEMBERS = Limits.MAX_SET_MEMBERS;
|
|
2665
|
-
function setOp(opCode, values) {
|
|
2666
|
-
const bigs = values.map((v) => {
|
|
2667
|
-
if (typeof v === "bigint") return v;
|
|
2668
|
-
if (typeof v === "number") return BigInt(v);
|
|
2669
|
-
if (typeof v === "boolean") return v ? 1n : 0n;
|
|
2670
|
-
return BigInt("0x" + addressBody(v));
|
|
2671
|
-
});
|
|
2672
|
-
bigs.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
2673
|
-
const deduped = [];
|
|
2674
|
-
for (const value of bigs) if (deduped.length === 0 || deduped[deduped.length - 1] !== value) deduped.push(value);
|
|
2675
|
-
if (deduped.length === 0) throw new CallciumError("EMPTY_SET", "Set must contain at least one value");
|
|
2676
|
-
if (deduped.length > MAX_SET_MEMBERS) throw new CallciumError("SET_TOO_LARGE", `Set must contain at most ${MAX_SET_MEMBERS} values`);
|
|
2677
|
-
const buffer = new Uint8Array(1 + deduped.length * 32);
|
|
2678
|
-
buffer[0] = opCode;
|
|
2679
|
-
for (let i = 0; i < deduped.length; i++) buffer.set(encodeWord(deduped[i]), 1 + i * 32);
|
|
2680
|
-
return bytesToHex(buffer);
|
|
2681
|
-
}
|
|
2682
|
-
function encodePath(steps) {
|
|
2683
|
-
const buffer = new Uint8Array(steps.length * 2);
|
|
2684
|
-
for (let i = 0; i < steps.length; i++) writeBE16(buffer, i * 2, steps[i]);
|
|
2685
|
-
return bytesToHex(buffer);
|
|
2686
|
-
}
|
|
2687
|
-
var ConstraintBuilder = class {
|
|
2688
|
-
scope;
|
|
2689
|
-
path;
|
|
2690
|
-
operators;
|
|
2691
|
-
constructor(scope, path) {
|
|
2692
|
-
this.scope = scope;
|
|
2693
|
-
this.path = path;
|
|
2694
|
-
this.operators = [];
|
|
2695
|
-
}
|
|
2696
|
-
push(opHex) {
|
|
2697
|
-
this.operators.push(opHex);
|
|
2698
|
-
return this;
|
|
2699
|
-
}
|
|
2700
|
-
eq(value) {
|
|
2701
|
-
return this.push(singleOp(Op.EQ, value));
|
|
2702
|
-
}
|
|
2703
|
-
neq(value) {
|
|
2704
|
-
return this.push(singleOp(Op.EQ | Op.NOT, value));
|
|
2705
|
-
}
|
|
2706
|
-
eqCtx(contextPropertyId) {
|
|
2707
|
-
return this.push(singleOp(Op.EQ_CTX, checkContextPropertyId(contextPropertyId)));
|
|
2708
|
-
}
|
|
2709
|
-
neqCtx(contextPropertyId) {
|
|
2710
|
-
return this.push(singleOp(Op.EQ_CTX | Op.NOT, checkContextPropertyId(contextPropertyId)));
|
|
2711
|
-
}
|
|
2712
|
-
gt(bound) {
|
|
2713
|
-
return this.push(singleOp(Op.GT, bound));
|
|
2714
|
-
}
|
|
2715
|
-
lt(bound) {
|
|
2716
|
-
return this.push(singleOp(Op.LT, bound));
|
|
2717
|
-
}
|
|
2718
|
-
gte(bound) {
|
|
2719
|
-
return this.push(singleOp(Op.GTE, bound));
|
|
2720
|
-
}
|
|
2721
|
-
lte(bound) {
|
|
2722
|
-
return this.push(singleOp(Op.LTE, bound));
|
|
2723
|
-
}
|
|
2724
|
-
between(min, max) {
|
|
2725
|
-
return this.push(rangeOp(Op.BETWEEN, BigInt(min), BigInt(max)));
|
|
2726
|
-
}
|
|
2727
|
-
isIn(values) {
|
|
2728
|
-
return this.push(setOp(Op.IN, values));
|
|
2729
|
-
}
|
|
2730
|
-
notIn(values) {
|
|
2731
|
-
return this.push(setOp(Op.IN | Op.NOT, values));
|
|
2732
|
-
}
|
|
2733
|
-
bitmaskAll(mask) {
|
|
2734
|
-
return this.push(singleOp(Op.BITMASK_ALL, mask));
|
|
2735
|
-
}
|
|
2736
|
-
bitmaskAny(mask) {
|
|
2737
|
-
return this.push(singleOp(Op.BITMASK_ANY, mask));
|
|
2738
|
-
}
|
|
2739
|
-
bitmaskNone(mask) {
|
|
2740
|
-
return this.push(singleOp(Op.BITMASK_NONE, mask));
|
|
2741
|
-
}
|
|
2742
|
-
lengthEq(n) {
|
|
2743
|
-
return this.push(singleOp(Op.LENGTH_EQ, n));
|
|
2744
|
-
}
|
|
2745
|
-
lengthGt(n) {
|
|
2746
|
-
return this.push(singleOp(Op.LENGTH_GT, n));
|
|
2747
|
-
}
|
|
2748
|
-
lengthLt(n) {
|
|
2749
|
-
return this.push(singleOp(Op.LENGTH_LT, n));
|
|
2750
|
-
}
|
|
2751
|
-
lengthGte(n) {
|
|
2752
|
-
return this.push(singleOp(Op.LENGTH_GTE, n));
|
|
2753
|
-
}
|
|
2754
|
-
lengthLte(n) {
|
|
2755
|
-
return this.push(singleOp(Op.LENGTH_LTE, n));
|
|
2756
|
-
}
|
|
2757
|
-
lengthBetween(min, max) {
|
|
2758
|
-
return this.push(rangeOp(Op.LENGTH_BETWEEN, BigInt(min), BigInt(max)));
|
|
2759
|
-
}
|
|
2760
|
-
};
|
|
2761
|
-
function arg(...steps) {
|
|
2762
|
-
return new ConstraintBuilder(Scope.CALLDATA, encodePath(steps));
|
|
2763
|
-
}
|
|
2764
|
-
function msgSender() {
|
|
2765
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_SENDER]));
|
|
2766
|
-
}
|
|
2767
|
-
function msgValue() {
|
|
2768
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_VALUE]));
|
|
2769
|
-
}
|
|
2770
|
-
function blockTimestamp() {
|
|
2771
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_TIMESTAMP]));
|
|
2772
|
-
}
|
|
2773
|
-
function blockNumber() {
|
|
2774
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_NUMBER]));
|
|
2775
|
-
}
|
|
2776
|
-
function chainId() {
|
|
2777
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.CHAIN_ID]));
|
|
2778
|
-
}
|
|
2779
|
-
function txOrigin() {
|
|
2780
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.TX_ORIGIN]));
|
|
2781
|
-
}
|
|
2782
|
-
function baseFee() {
|
|
2783
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BASE_FEE]));
|
|
2784
|
-
}
|
|
2785
|
-
function gasPrice() {
|
|
2786
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.GAS_PRICE]));
|
|
2787
|
-
}
|
|
2788
|
-
//#endregion
|
|
2789
2906
|
//#region src/calldata-reader.ts
|
|
2790
2907
|
function loadWord(callData, offset) {
|
|
2791
2908
|
if (offset < 0 || offset + 32 > callData.length) return { code: "CALLDATA_OUT_OF_BOUNDS" };
|
|
2792
2909
|
return callData.subarray(offset, offset + 32);
|
|
2793
2910
|
}
|
|
2911
|
+
function readLength(callData, head) {
|
|
2912
|
+
const word = loadWord(callData, head);
|
|
2913
|
+
if (word instanceof Uint8Array) return toBigInt(word, 0);
|
|
2914
|
+
return word;
|
|
2915
|
+
}
|
|
2794
2916
|
function readPointer(callData, head) {
|
|
2795
2917
|
const word = loadWord(callData, head);
|
|
2796
2918
|
if (word instanceof Uint8Array) {
|
|
@@ -2801,17 +2923,16 @@ function readPointer(callData, head) {
|
|
|
2801
2923
|
}
|
|
2802
2924
|
//#endregion
|
|
2803
2925
|
//#region src/policy-enforcer.ts
|
|
2804
|
-
const
|
|
2926
|
+
const abortViolationCodes = new Set([
|
|
2805
2927
|
"CALLDATA_OUT_OF_BOUNDS",
|
|
2806
2928
|
"ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
2807
2929
|
"NON_CANONICAL_VALUE",
|
|
2808
2930
|
"QUANTIFIER_LIMIT_EXCEEDED"
|
|
2809
2931
|
]);
|
|
2810
|
-
function
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
return toBigInt(padded, 0);
|
|
2932
|
+
function contextValueToWord(value) {
|
|
2933
|
+
if (typeof value === "string") return BigInt(toAddress(value));
|
|
2934
|
+
if (BigInt.asUintN(256, value) !== value) throw new CallciumError("CONTEXT_VALUE_OVERFLOW", `Context value ${value} is outside the range a 32-byte word represents`);
|
|
2935
|
+
return value;
|
|
2815
2936
|
}
|
|
2816
2937
|
function check(policy, callData, context) {
|
|
2817
2938
|
const { policy: decoded, data: policyBytes } = decodePolicy(policy);
|
|
@@ -2842,7 +2963,7 @@ function check(policy, callData, context) {
|
|
|
2842
2963
|
const violation = evaluateRule(rule, policyBytes, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
|
|
2843
2964
|
if (violation !== null) {
|
|
2844
2965
|
allViolations.push(violation);
|
|
2845
|
-
if (
|
|
2966
|
+
if (abortViolationCodes.has(violation.code)) return {
|
|
2846
2967
|
ok: false,
|
|
2847
2968
|
violations: allViolations
|
|
2848
2969
|
};
|
|
@@ -2875,10 +2996,14 @@ function evaluateRule(rule, policyBytes, callDataBytes, baseOffset, groupIndex,
|
|
|
2875
2996
|
return evaluateCalldataRule(fieldBytes(policyBytes, rule.hint), callDataBytes, baseOffset, opCode, operandData, groupIndex, ruleIndex, rule.path.value, context);
|
|
2876
2997
|
}
|
|
2877
2998
|
function resolveContextOperand(operandData, context) {
|
|
2878
|
-
const
|
|
2879
|
-
const value = context?.[
|
|
2880
|
-
if (value === void 0) return { ctxTypeCode:
|
|
2881
|
-
return
|
|
2999
|
+
const propertyInfo = lookupContextProperty(Number(toBigInt(operandData, 0)));
|
|
3000
|
+
const value = context?.[propertyInfo.contextKey];
|
|
3001
|
+
if (value === void 0) return { ctxTypeCode: propertyInfo.typeCode };
|
|
3002
|
+
return contextValueToWord(value);
|
|
3003
|
+
}
|
|
3004
|
+
function referenceOperand(opCode, operandData, context) {
|
|
3005
|
+
if ((opCode & ~Op.NOT) !== Op.EQ_CTX) return void 0;
|
|
3006
|
+
return resolveContextOperand(operandData, context);
|
|
2882
3007
|
}
|
|
2883
3008
|
function readTargetBlock(hint, targetOffset) {
|
|
2884
3009
|
return {
|
|
@@ -2905,8 +3030,8 @@ function chainResolve(hint, callData, base, hopsOffset, hopCount) {
|
|
|
2905
3030
|
const meta = readU16(hint, hop + PolicyFormat.HINT_HOP_META_OFFSET);
|
|
2906
3031
|
let elems = base;
|
|
2907
3032
|
if ((meta & PolicyFormat.HINT_META_DYNAMIC_ARRAY) !== 0) {
|
|
2908
|
-
const length =
|
|
2909
|
-
if (typeof length !== "
|
|
3033
|
+
const length = readLength(callData, elems);
|
|
3034
|
+
if (typeof length !== "bigint") return length;
|
|
2910
3035
|
if (index >= length) return { code: "ARRAY_INDEX_OUT_OF_BOUNDS" };
|
|
2911
3036
|
elems += 32;
|
|
2912
3037
|
}
|
|
@@ -2919,21 +3044,19 @@ function chainResolve(hint, callData, base, hopsOffset, hopCount) {
|
|
|
2919
3044
|
}
|
|
2920
3045
|
return base;
|
|
2921
3046
|
}
|
|
2922
|
-
function evalTarget(callData, target, block, opCode, operandData,
|
|
3047
|
+
function evalTarget(callData, target, block, opCode, operandData, ctxOperand) {
|
|
2923
3048
|
const typeCode = block.typeCode;
|
|
2924
3049
|
if (isLengthValidType(typeCode)) {
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
if (typeof length !== "number") return { error: length.code };
|
|
3050
|
+
const length = readLength(callData, target);
|
|
3051
|
+
if (typeof length !== "bigint") return { error: length.code };
|
|
2928
3052
|
const stride = typeCode === TypeCode.DYNAMIC_ARRAY ? (block.targetMeta & PolicyFormat.HINT_META_STRIDE_MASK) * 32 : 1;
|
|
2929
3053
|
const room = callData.length - (target + 32);
|
|
2930
|
-
if (stride !== 0 && length > Math.floor(room / stride)) return { error: "CALLDATA_OUT_OF_BOUNDS" };
|
|
3054
|
+
if (stride !== 0 && length > BigInt(Math.floor(room / stride))) return { error: "CALLDATA_OUT_OF_BOUNDS" };
|
|
2931
3055
|
return {
|
|
2932
3056
|
passed: applyOperator(opCode, 0n, length, operandData, typeCode),
|
|
2933
|
-
value:
|
|
3057
|
+
value: length
|
|
2934
3058
|
};
|
|
2935
3059
|
}
|
|
2936
|
-
if (classifyTypeCode(typeCode).typeClass !== "elementary") throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Operator target does not carry a scalar word.");
|
|
2937
3060
|
const word = loadWord(callData, target);
|
|
2938
3061
|
if (!(word instanceof Uint8Array)) return { error: word.code };
|
|
2939
3062
|
const value = toBigInt(word, 0);
|
|
@@ -2941,57 +3064,50 @@ function evalTarget(callData, target, block, opCode, operandData, context) {
|
|
|
2941
3064
|
error: "NON_CANONICAL_VALUE",
|
|
2942
3065
|
value
|
|
2943
3066
|
};
|
|
2944
|
-
if (
|
|
2945
|
-
|
|
2946
|
-
if (typeof operand !== "bigint") return {
|
|
3067
|
+
if (ctxOperand !== void 0) {
|
|
3068
|
+
if (typeof ctxOperand !== "bigint") return {
|
|
2947
3069
|
error: "MISSING_CONTEXT",
|
|
2948
|
-
ctxTypeCode:
|
|
3070
|
+
ctxTypeCode: ctxOperand.ctxTypeCode
|
|
2949
3071
|
};
|
|
2950
3072
|
return {
|
|
2951
|
-
passed: value ===
|
|
3073
|
+
passed: value === ctxOperand !== ((opCode & Op.NOT) !== 0),
|
|
2952
3074
|
value,
|
|
2953
|
-
ctxOperand
|
|
3075
|
+
ctxOperand
|
|
2954
3076
|
};
|
|
2955
3077
|
}
|
|
2956
3078
|
return {
|
|
2957
|
-
passed: applyOperator(opCode, value,
|
|
3079
|
+
passed: applyOperator(opCode, value, 32n, operandData, typeCode),
|
|
2958
3080
|
value
|
|
2959
3081
|
};
|
|
2960
3082
|
}
|
|
2961
|
-
function
|
|
3083
|
+
function ruleFields(frame) {
|
|
2962
3084
|
return {
|
|
2963
3085
|
group: frame.group,
|
|
2964
3086
|
rule: frame.rule,
|
|
2965
|
-
code,
|
|
2966
3087
|
scope: Scope.CALLDATA,
|
|
2967
3088
|
path: frame.path,
|
|
2968
3089
|
opCode: frame.opCode,
|
|
2969
3090
|
operandData: bytesToHex(frame.operandData),
|
|
2970
|
-
typeCode: frame.typeCode
|
|
3091
|
+
typeCode: frame.typeCode
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
function navigationViolation(frame, code, elementIndex) {
|
|
3095
|
+
return {
|
|
3096
|
+
code,
|
|
3097
|
+
...ruleFields(frame),
|
|
2971
3098
|
...elementIndex !== void 0 && { elementIndex }
|
|
2972
3099
|
};
|
|
2973
3100
|
}
|
|
2974
3101
|
function targetViolation(frame, result, elementIndex) {
|
|
2975
3102
|
if ("error" in result) {
|
|
2976
3103
|
if (result.error === "MISSING_CONTEXT") return {
|
|
2977
|
-
group: frame.group,
|
|
2978
|
-
rule: frame.rule,
|
|
2979
3104
|
code: result.error,
|
|
2980
|
-
|
|
2981
|
-
path: frame.path,
|
|
2982
|
-
opCode: frame.opCode,
|
|
2983
|
-
operandData: bytesToHex(frame.operandData),
|
|
3105
|
+
...ruleFields(frame),
|
|
2984
3106
|
typeCode: result.ctxTypeCode
|
|
2985
3107
|
};
|
|
2986
3108
|
if (result.error === "NON_CANONICAL_VALUE") return {
|
|
2987
|
-
group: frame.group,
|
|
2988
|
-
rule: frame.rule,
|
|
2989
3109
|
code: result.error,
|
|
2990
|
-
|
|
2991
|
-
path: frame.path,
|
|
2992
|
-
opCode: frame.opCode,
|
|
2993
|
-
operandData: bytesToHex(frame.operandData),
|
|
2994
|
-
typeCode: frame.typeCode,
|
|
3110
|
+
...ruleFields(frame),
|
|
2995
3111
|
resolvedValue: bigintToHex(result.value),
|
|
2996
3112
|
...elementIndex !== void 0 && { elementIndex }
|
|
2997
3113
|
};
|
|
@@ -2999,14 +3115,8 @@ function targetViolation(frame, result, elementIndex) {
|
|
|
2999
3115
|
}
|
|
3000
3116
|
if (result.passed) return null;
|
|
3001
3117
|
return {
|
|
3002
|
-
group: frame.group,
|
|
3003
|
-
rule: frame.rule,
|
|
3004
3118
|
code: "VALUE_MISMATCH",
|
|
3005
|
-
|
|
3006
|
-
path: frame.path,
|
|
3007
|
-
opCode: frame.opCode,
|
|
3008
|
-
operandData: bytesToHex(frame.operandData),
|
|
3009
|
-
typeCode: frame.typeCode,
|
|
3119
|
+
...ruleFields(frame),
|
|
3010
3120
|
resolvedValue: bigintToHex(result.value),
|
|
3011
3121
|
...result.ctxOperand !== void 0 && { resolvedOperand: bigintToHex(result.ctxOperand) },
|
|
3012
3122
|
...elementIndex !== void 0 && { elementIndex }
|
|
@@ -3029,7 +3139,7 @@ function evaluateCalldataRule(hint, callDataBytes, baseOffset, opCode, operandDa
|
|
|
3029
3139
|
};
|
|
3030
3140
|
const base = chainResolve(hint, callDataBytes, baseOffset, hopsOffset, hopCount);
|
|
3031
3141
|
if (typeof base !== "number") return navigationViolation(frame, base.code);
|
|
3032
|
-
return targetViolation(frame, evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData, context));
|
|
3142
|
+
return targetViolation(frame, evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData, referenceOperand(opCode, operandData, context)));
|
|
3033
3143
|
}
|
|
3034
3144
|
function evaluateQuantified(hint, callDataBytes, baseOffset, kind, hopsOffset, hopCount, opCode, operandData, groupIndex, ruleIndex, pathHex, context) {
|
|
3035
3145
|
const frameOffset = hopsOffset + hopCount * PolicyFormat.HINT_HOP_SIZE;
|
|
@@ -3048,23 +3158,23 @@ function evaluateQuantified(hint, callDataBytes, baseOffset, kind, hopsOffset, h
|
|
|
3048
3158
|
const chained = chainResolve(hint, callDataBytes, baseOffset, hopsOffset, hopCount);
|
|
3049
3159
|
if (typeof chained !== "number") return navigationViolation(frame, chained.code);
|
|
3050
3160
|
let elems = chained + readU32(hint, frameOffset);
|
|
3051
|
-
let count = readU16(hint, frameOffset + PolicyFormat.HINT_FRAME_COUNT_OFFSET);
|
|
3052
|
-
if (count ===
|
|
3053
|
-
const length =
|
|
3054
|
-
if (typeof length !== "
|
|
3161
|
+
let count = BigInt(readU16(hint, frameOffset + PolicyFormat.HINT_FRAME_COUNT_OFFSET));
|
|
3162
|
+
if (count === 0n) {
|
|
3163
|
+
const length = readLength(callDataBytes, elems);
|
|
3164
|
+
if (typeof length !== "bigint") return navigationViolation(frame, length.code);
|
|
3055
3165
|
count = length;
|
|
3056
3166
|
elems += 32;
|
|
3057
3167
|
}
|
|
3058
|
-
if (count >
|
|
3168
|
+
if (count > BigInt(PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH)) return {
|
|
3059
3169
|
group: groupIndex,
|
|
3060
3170
|
rule: ruleIndex,
|
|
3061
3171
|
code: "QUANTIFIER_LIMIT_EXCEEDED",
|
|
3062
3172
|
scope: Scope.CALLDATA,
|
|
3063
3173
|
path: pathHex,
|
|
3064
|
-
resolvedValue: bigintToHex(
|
|
3174
|
+
resolvedValue: bigintToHex(count)
|
|
3065
3175
|
};
|
|
3066
3176
|
const isUniversal = kind === PolicyFormat.HINT_KIND_ALL;
|
|
3067
|
-
if (count ===
|
|
3177
|
+
if (count === 0n) {
|
|
3068
3178
|
if (isUniversal) return null;
|
|
3069
3179
|
return {
|
|
3070
3180
|
group: groupIndex,
|
|
@@ -3077,7 +3187,7 @@ function evaluateQuantified(hint, callDataBytes, baseOffset, kind, hopsOffset, h
|
|
|
3077
3187
|
const meta = readU16(hint, frameOffset + PolicyFormat.HINT_FRAME_META_OFFSET);
|
|
3078
3188
|
const elemStride = (meta & PolicyFormat.HINT_META_STRIDE_MASK) * 32;
|
|
3079
3189
|
const elemIsDynamic = (meta & PolicyFormat.HINT_META_ELEM_DYNAMIC) !== 0;
|
|
3080
|
-
|
|
3190
|
+
const reference = referenceOperand(opCode, operandData, context);
|
|
3081
3191
|
for (let elemIndex = 0; elemIndex < count; elemIndex++) {
|
|
3082
3192
|
const slot = elems + elemIndex * elemStride;
|
|
3083
3193
|
let elem = slot;
|
|
@@ -3092,68 +3202,54 @@ function evaluateQuantified(hint, callDataBytes, baseOffset, kind, hopsOffset, h
|
|
|
3092
3202
|
if (typeof chainedElem !== "number") return navigationViolation(frame, chainedElem.code, elemIndex);
|
|
3093
3203
|
base = chainedElem;
|
|
3094
3204
|
}
|
|
3095
|
-
const applied = evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData,
|
|
3205
|
+
const applied = evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData, reference);
|
|
3096
3206
|
if ("error" in applied) return targetViolation(frame, applied, elemIndex);
|
|
3097
3207
|
if (applied.passed) {
|
|
3098
3208
|
if (!isUniversal) return null;
|
|
3099
3209
|
} else if (isUniversal) return targetViolation(frame, applied, elemIndex);
|
|
3100
|
-
ctxOperand = applied.ctxOperand;
|
|
3101
3210
|
}
|
|
3102
3211
|
if (isUniversal) return null;
|
|
3103
3212
|
return {
|
|
3104
|
-
group: groupIndex,
|
|
3105
|
-
rule: ruleIndex,
|
|
3106
3213
|
code: "VALUE_MISMATCH",
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
opCode,
|
|
3110
|
-
operandData: bytesToHex(operandData),
|
|
3111
|
-
typeCode: block.typeCode,
|
|
3112
|
-
...ctxOperand !== void 0 && { resolvedOperand: bigintToHex(ctxOperand) }
|
|
3214
|
+
...ruleFields(frame),
|
|
3215
|
+
...typeof reference === "bigint" && { resolvedOperand: bigintToHex(reference) }
|
|
3113
3216
|
};
|
|
3114
3217
|
}
|
|
3115
3218
|
function evaluateContextRule(pathBytes, opCode, operandData, groupIndex, ruleIndex, pathHex, context) {
|
|
3116
|
-
const
|
|
3117
|
-
|
|
3118
|
-
const
|
|
3119
|
-
const contextValue = context?.[propInfo.contextKey];
|
|
3120
|
-
if (contextValue === void 0) return {
|
|
3219
|
+
const propertyInfo = lookupContextProperty(readU16(pathBytes, 0));
|
|
3220
|
+
const contextValue = context?.[propertyInfo.contextKey];
|
|
3221
|
+
const site = {
|
|
3121
3222
|
group: groupIndex,
|
|
3122
3223
|
rule: ruleIndex,
|
|
3123
|
-
code: "MISSING_CONTEXT",
|
|
3124
3224
|
scope: Scope.CONTEXT,
|
|
3125
|
-
path: pathHex
|
|
3126
|
-
|
|
3225
|
+
path: pathHex
|
|
3226
|
+
};
|
|
3227
|
+
if (contextValue === void 0) return {
|
|
3228
|
+
code: "MISSING_CONTEXT",
|
|
3229
|
+
...site,
|
|
3230
|
+
typeCode: propertyInfo.typeCode
|
|
3127
3231
|
};
|
|
3128
|
-
|
|
3129
|
-
if (typeof contextValue === "string") value = addressToBigInt(contextValue);
|
|
3130
|
-
else value = contextValue;
|
|
3232
|
+
const value = contextValueToWord(contextValue);
|
|
3131
3233
|
let result;
|
|
3132
3234
|
let ctxOperand;
|
|
3133
3235
|
if ((opCode & ~Op.NOT) === Op.EQ_CTX) {
|
|
3134
3236
|
const operand = resolveContextOperand(operandData, context);
|
|
3135
3237
|
if (typeof operand !== "bigint") return {
|
|
3136
|
-
group: groupIndex,
|
|
3137
|
-
rule: ruleIndex,
|
|
3138
3238
|
code: "MISSING_CONTEXT",
|
|
3139
|
-
|
|
3140
|
-
path: pathHex,
|
|
3239
|
+
...site,
|
|
3141
3240
|
opCode,
|
|
3142
3241
|
operandData: bytesToHex(operandData),
|
|
3143
3242
|
typeCode: operand.ctxTypeCode
|
|
3144
3243
|
};
|
|
3145
3244
|
result = value === operand !== ((opCode & Op.NOT) !== 0);
|
|
3146
3245
|
ctxOperand = operand;
|
|
3147
|
-
} else result = applyOperator(opCode, value,
|
|
3246
|
+
} else result = applyOperator(opCode, value, 32n, operandData, propertyInfo.typeCode);
|
|
3148
3247
|
if (!result) return {
|
|
3149
|
-
group: groupIndex,
|
|
3150
|
-
rule: ruleIndex,
|
|
3151
3248
|
code: "VALUE_MISMATCH",
|
|
3152
|
-
|
|
3153
|
-
path: pathHex,
|
|
3249
|
+
...site,
|
|
3154
3250
|
opCode,
|
|
3155
3251
|
operandData: bytesToHex(operandData),
|
|
3156
|
-
typeCode:
|
|
3252
|
+
typeCode: propertyInfo.typeCode,
|
|
3157
3253
|
resolvedValue: bigintToHex(value),
|
|
3158
3254
|
...ctxOperand !== void 0 && { resolvedOperand: bigintToHex(ctxOperand) }
|
|
3159
3255
|
};
|
|
@@ -3164,6 +3260,6 @@ const PolicyEnforcer = {
|
|
|
3164
3260
|
enforce
|
|
3165
3261
|
};
|
|
3166
3262
|
//#endregion
|
|
3167
|
-
export { CallciumError, ContextProperty, Descriptor, DescriptorCoder,
|
|
3263
|
+
export { CallciumError, ContextProperty, Descriptor, DescriptorCoder, MAX_CONTEXT_PROPERTY_ID, Op, PolicyBuilder, PolicyCoder, PolicyEnforcer, PolicyFormat, PolicyValidator, PolicyViolationError, Quantifier, Scope, TypeCode, ValidationError, arg, baseFee, blockNumber, blockTimestamp, bytesToHex, chainId, findContextProperty, gasPrice, hexToBytes, isLengthOp, isOpAllowed, isQuantifier, lookupContextProperty, lookupOp, lookupQuantifier, lookupScope, lookupTypeCode, msgSender, msgValue, parsePathSteps, toAddress, txOrigin };
|
|
3168
3264
|
|
|
3169
3265
|
//# sourceMappingURL=index.js.map
|