@callcium/sdk 0.10.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 +150 -103
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +683 -579
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/calldata-reader.ts +14 -1
- package/src/constants.ts +54 -11
- package/src/constraint.ts +131 -69
- package/src/descriptor-coder.ts +37 -66
- package/src/descriptor.ts +54 -26
- package/src/errors.ts +12 -2
- package/src/index.ts +1 -0
- package/src/operators.ts +36 -5
- package/src/path.ts +15 -6
- package/src/policy-builder.ts +94 -32
- package/src/policy-coder.ts +100 -45
- package/src/policy-enforcer.ts +85 -103
- package/src/policy-validator.ts +52 -35
- package/src/signature.ts +21 -19
- package/src/type-desc.ts +6 -6
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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
4
|
var CallciumError = class extends Error {
|
|
4
5
|
code;
|
|
@@ -8,6 +9,7 @@ var CallciumError = class extends Error {
|
|
|
8
9
|
super(`${prefix}${message}`);
|
|
9
10
|
this.name = "CallciumError";
|
|
10
11
|
this.code = code;
|
|
12
|
+
if (offset !== void 0) this.offset = offset;
|
|
11
13
|
}
|
|
12
14
|
};
|
|
13
15
|
var ValidationError = class extends CallciumError {
|
|
@@ -115,6 +117,11 @@ const DescriptorFormat = {
|
|
|
115
117
|
MAX_NESTING_DEPTH: 64
|
|
116
118
|
};
|
|
117
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
|
+
}
|
|
118
125
|
const PolicyFormat = {
|
|
119
126
|
VERSION: 2,
|
|
120
127
|
VERSION_MASK: 15,
|
|
@@ -134,11 +141,12 @@ const PolicyFormat = {
|
|
|
134
141
|
RULE_SCOPE_OFFSET: 2,
|
|
135
142
|
RULE_DEPTH_OFFSET: 3,
|
|
136
143
|
RULE_PATH_OFFSET: 4,
|
|
137
|
-
PATH_STEP_SIZE
|
|
144
|
+
PATH_STEP_SIZE,
|
|
145
|
+
PATH_STEP_MAX: fieldMax(PATH_STEP_SIZE),
|
|
138
146
|
RULE_OPCODE_SIZE: 1,
|
|
139
147
|
RULE_DATALENGTH_SIZE,
|
|
140
|
-
RULE_FIXED_OVERHEAD
|
|
141
|
-
RULE_MIN_SIZE:
|
|
148
|
+
RULE_FIXED_OVERHEAD,
|
|
149
|
+
RULE_MIN_SIZE: RULE_FIXED_OVERHEAD + PATH_STEP_SIZE,
|
|
142
150
|
HINT_HEADER_SIZE: 1,
|
|
143
151
|
HINT_HOP_SIZE: 8,
|
|
144
152
|
HINT_HOP_INDEX_OFFSET: 4,
|
|
@@ -164,10 +172,14 @@ const PolicyFormat = {
|
|
|
164
172
|
HINT_META_STRIDE_MASK: 4095,
|
|
165
173
|
MAX_PATH_DEPTH: 32,
|
|
166
174
|
MAX_QUANTIFIED_ARRAY_LENGTH: 256,
|
|
167
|
-
MAX_SET_MEMBERS: Math.floor((
|
|
175
|
+
MAX_SET_MEMBERS: Math.floor(fieldMax(RULE_DATALENGTH_SIZE) / 32)
|
|
168
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
|
+
}
|
|
169
181
|
function buildCodeMap(table) {
|
|
170
|
-
return Object.fromEntries(table.map((
|
|
182
|
+
return Object.fromEntries(table.map((entry) => [entry.key, entry.code]));
|
|
171
183
|
}
|
|
172
184
|
const SCOPE_TABLE = [{
|
|
173
185
|
key: "CONTEXT",
|
|
@@ -179,7 +191,7 @@ const SCOPE_TABLE = [{
|
|
|
179
191
|
label: "calldata"
|
|
180
192
|
}];
|
|
181
193
|
const Scope = buildCodeMap(SCOPE_TABLE);
|
|
182
|
-
const scopeByCode = new Map(SCOPE_TABLE.map((
|
|
194
|
+
const scopeByCode = new Map(SCOPE_TABLE.map((entry) => [entry.code, { label: entry.label }]));
|
|
183
195
|
function lookupScope(code) {
|
|
184
196
|
const info = scopeByCode.get(code);
|
|
185
197
|
if (!info) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${code}`);
|
|
@@ -260,15 +272,18 @@ const CTX_PROPERTY_TABLE = [
|
|
|
260
272
|
}
|
|
261
273
|
];
|
|
262
274
|
const ContextProperty = buildCodeMap(CTX_PROPERTY_TABLE);
|
|
263
|
-
const MAX_CONTEXT_PROPERTY_ID = Math.max(...CTX_PROPERTY_TABLE.map((
|
|
264
|
-
const ctxPropertyByCode = new Map(CTX_PROPERTY_TABLE.map((
|
|
265
|
-
label:
|
|
266
|
-
contextKey:
|
|
267
|
-
typeCode:
|
|
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
|
|
268
280
|
}]));
|
|
281
|
+
function findContextProperty(code) {
|
|
282
|
+
return ctxPropertyByCode.get(code);
|
|
283
|
+
}
|
|
269
284
|
function lookupContextProperty(code) {
|
|
270
285
|
const info = ctxPropertyByCode.get(code);
|
|
271
|
-
if (!info) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ${code}`);
|
|
286
|
+
if (!info) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ID ${formatCode(code, PATH_STEP_SIZE)}`);
|
|
272
287
|
return info;
|
|
273
288
|
}
|
|
274
289
|
const OP_TABLE = [
|
|
@@ -379,14 +394,14 @@ const Op = {
|
|
|
379
394
|
...buildCodeMap(OP_TABLE),
|
|
380
395
|
NOT: 128
|
|
381
396
|
};
|
|
382
|
-
const opByCode = new Map(OP_TABLE.map((
|
|
383
|
-
label:
|
|
384
|
-
operands:
|
|
397
|
+
const opByCode = new Map(OP_TABLE.map((entry) => [entry.code, {
|
|
398
|
+
label: entry.label,
|
|
399
|
+
operands: entry.operands
|
|
385
400
|
}]));
|
|
386
401
|
function lookupOp(code) {
|
|
387
402
|
const base = code & ~Op.NOT;
|
|
388
403
|
const info = opByCode.get(base);
|
|
389
|
-
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)}`);
|
|
390
405
|
return info;
|
|
391
406
|
}
|
|
392
407
|
function operandsOf(opBase) {
|
|
@@ -404,18 +419,22 @@ const QUANTIFIER_TABLE = [{
|
|
|
404
419
|
label: "any"
|
|
405
420
|
}];
|
|
406
421
|
const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
|
|
407
|
-
const quantifierByCode = new Map(QUANTIFIER_TABLE.map((
|
|
422
|
+
const quantifierByCode = new Map(QUANTIFIER_TABLE.map((entry) => [entry.code, { label: entry.label }]));
|
|
408
423
|
function isQuantifier(step) {
|
|
409
424
|
return step >= Quantifier.ANY;
|
|
410
425
|
}
|
|
411
426
|
function lookupQuantifier(code) {
|
|
412
427
|
const info = quantifierByCode.get(code);
|
|
413
|
-
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)}`);
|
|
414
429
|
return info;
|
|
415
430
|
}
|
|
416
431
|
function encodePath(steps) {
|
|
417
|
-
const buffer = new Uint8Array(steps.length *
|
|
418
|
-
for (let i = 0; i < steps.length; i++)
|
|
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
|
+
}
|
|
419
438
|
return bytesToHex(buffer);
|
|
420
439
|
}
|
|
421
440
|
function parsePathSteps(path) {
|
|
@@ -426,221 +445,181 @@ function parsePathSteps(path) {
|
|
|
426
445
|
return steps;
|
|
427
446
|
}
|
|
428
447
|
//#endregion
|
|
429
|
-
//#region src/
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
let cursor = tupleOffset + DescriptorFormat.TUPLE_HEADER_SIZE;
|
|
436
|
-
for (let i = 0; i < fieldIndex; i++) cursor += nodeLength(desc, cursor);
|
|
437
|
-
return cursor;
|
|
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;
|
|
438
454
|
}
|
|
439
|
-
function
|
|
440
|
-
return
|
|
455
|
+
function addressBody(value) {
|
|
456
|
+
return toAddress(value).slice(2);
|
|
441
457
|
}
|
|
442
|
-
function
|
|
443
|
-
const
|
|
444
|
-
if (
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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;
|
|
448
474
|
}
|
|
449
|
-
function
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return {
|
|
455
|
-
typeCode,
|
|
456
|
-
isDynamic: staticWords === 0,
|
|
457
|
-
staticSize: staticWords * 32
|
|
458
|
-
};
|
|
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);
|
|
459
480
|
}
|
|
460
|
-
function
|
|
461
|
-
|
|
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);
|
|
462
490
|
}
|
|
463
|
-
function
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const count = desc[1];
|
|
467
|
-
if (paramIndex >= count) throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${paramIndex} out of range (paramCount=${count})`);
|
|
468
|
-
let cursor = paramOffset(desc, paramIndex);
|
|
469
|
-
let quantifiedStaticLength = 0;
|
|
470
|
-
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
|
|
471
|
-
const typeCode = desc[cursor];
|
|
472
|
-
const step = steps[stepIndex];
|
|
473
|
-
if (typeCode === TypeCode.TUPLE) {
|
|
474
|
-
const fields = tupleFieldCount(desc, cursor);
|
|
475
|
-
if (step >= fields) throw new CallciumError("TUPLE_FIELD_OUT_OF_BOUNDS", `Tuple field index ${step} out of range (tuple has ${fields} fields)`);
|
|
476
|
-
cursor = tupleFieldOffset(desc, cursor, step);
|
|
477
|
-
} else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) {
|
|
478
|
-
if (typeCode === TypeCode.STATIC_ARRAY) if (isQuantifier(step)) quantifiedStaticLength = staticArrayLength(desc, cursor);
|
|
479
|
-
else {
|
|
480
|
-
const length = staticArrayLength(desc, cursor);
|
|
481
|
-
if (step >= length) throw new CallciumError("STATIC_ARRAY_INDEX_OUT_OF_BOUNDS", `Array index ${step} out of range (length=${length})`);
|
|
482
|
-
}
|
|
483
|
-
cursor = arrayElementOffset(cursor);
|
|
484
|
-
} else throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}`);
|
|
485
|
-
}
|
|
486
|
-
return {
|
|
487
|
-
typeInfo: inspect$1(desc, cursor),
|
|
488
|
-
quantifiedStaticLength
|
|
489
|
-
};
|
|
491
|
+
function contextOperand(contextPropertyId) {
|
|
492
|
+
lookupContextProperty(contextPropertyId);
|
|
493
|
+
return BigInt(contextPropertyId);
|
|
490
494
|
}
|
|
491
|
-
function
|
|
492
|
-
|
|
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);
|
|
493
508
|
}
|
|
494
|
-
function
|
|
495
|
-
|
|
496
|
-
return readU16(desc, elemOffset + nodeLength(desc, elemOffset));
|
|
509
|
+
function readOperandExtremes(constraint) {
|
|
510
|
+
return "operandExtremes" in constraint ? constraint.operandExtremes : void 0;
|
|
497
511
|
}
|
|
498
|
-
|
|
499
|
-
|
|
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));
|
|
500
598
|
}
|
|
501
|
-
function
|
|
502
|
-
return new
|
|
599
|
+
function msgSender() {
|
|
600
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_SENDER]));
|
|
503
601
|
}
|
|
504
|
-
function
|
|
505
|
-
|
|
506
|
-
let nodeOffset = offset;
|
|
507
|
-
for (let i = 0; i < count; i++) {
|
|
508
|
-
const node = inspect$1(desc, nodeOffset);
|
|
509
|
-
span += node.isDynamic ? 32 : node.staticSize;
|
|
510
|
-
nodeOffset += nodeLength(desc, nodeOffset);
|
|
511
|
-
}
|
|
512
|
-
return {
|
|
513
|
-
span,
|
|
514
|
-
nodeOffset
|
|
515
|
-
};
|
|
602
|
+
function msgValue() {
|
|
603
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_VALUE]));
|
|
516
604
|
}
|
|
517
|
-
function
|
|
518
|
-
|
|
519
|
-
let meta = element.isDynamic ? 1 : element.staticSize >> 5;
|
|
520
|
-
if (element.isDynamic) meta |= PolicyFormat.HINT_META_ELEM_DYNAMIC;
|
|
521
|
-
if (code === TypeCode.DYNAMIC_ARRAY) meta |= PolicyFormat.HINT_META_DYNAMIC_ARRAY;
|
|
522
|
-
return {
|
|
523
|
-
meta,
|
|
524
|
-
elemIsDynamic: element.isDynamic
|
|
525
|
-
};
|
|
605
|
+
function blockTimestamp() {
|
|
606
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_TIMESTAMP]));
|
|
526
607
|
}
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
walk.chain.push([
|
|
530
|
-
walk.delta,
|
|
531
|
-
PolicyFormat.HINT_NO_INDEX,
|
|
532
|
-
0
|
|
533
|
-
]);
|
|
534
|
-
walk.delta = 0;
|
|
608
|
+
function blockNumber() {
|
|
609
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_NUMBER]));
|
|
535
610
|
}
|
|
536
|
-
function
|
|
537
|
-
|
|
538
|
-
if (node.typeCode === TypeCode.TUPLE) {
|
|
539
|
-
if (step >= tupleFieldCount(desc, descOffset)) throw uncompilablePath(stepIndex);
|
|
540
|
-
enterNode(walk, node.isDynamic && !walk.entered);
|
|
541
|
-
const field = headSpan(desc, descOffset + DescriptorFormat.TUPLE_HEADER_SIZE, step);
|
|
542
|
-
walk.delta += field.span;
|
|
543
|
-
walk.entered = false;
|
|
544
|
-
return field.nodeOffset;
|
|
545
|
-
}
|
|
546
|
-
if (node.typeCode !== TypeCode.STATIC_ARRAY && node.typeCode !== TypeCode.DYNAMIC_ARRAY) throw uncompilablePath(stepIndex);
|
|
547
|
-
if (node.typeCode === TypeCode.STATIC_ARRAY && !isQuantifier(step) && step >= staticArrayLength(desc, descOffset)) throw uncompilablePath(stepIndex);
|
|
548
|
-
const { meta, elemIsDynamic } = arrayMeta(desc, descOffset, node.typeCode);
|
|
549
|
-
if (isQuantifier(step)) {
|
|
550
|
-
if (walk.kind !== PolicyFormat.HINT_KIND_NONE) throw uncompilablePath(stepIndex);
|
|
551
|
-
enterNode(walk, node.isDynamic && !walk.entered);
|
|
552
|
-
const count = node.typeCode === TypeCode.DYNAMIC_ARRAY ? 0 : staticArrayLength(desc, descOffset);
|
|
553
|
-
walk.frame = {
|
|
554
|
-
arrayDelta: walk.delta,
|
|
555
|
-
count,
|
|
556
|
-
meta
|
|
557
|
-
};
|
|
558
|
-
walk.kind = step === Quantifier.ALL ? PolicyFormat.HINT_KIND_ALL : PolicyFormat.HINT_KIND_ANY;
|
|
559
|
-
walk.mainHops = walk.chain;
|
|
560
|
-
walk.chain = [];
|
|
561
|
-
walk.delta = 0;
|
|
562
|
-
} else if (node.isDynamic) {
|
|
563
|
-
enterNode(walk, !walk.entered);
|
|
564
|
-
walk.chain.push([
|
|
565
|
-
0,
|
|
566
|
-
step,
|
|
567
|
-
meta
|
|
568
|
-
]);
|
|
569
|
-
walk.delta = 0;
|
|
570
|
-
} else walk.delta += step * (meta & PolicyFormat.HINT_META_STRIDE_MASK) * 32;
|
|
571
|
-
walk.entered = elemIsDynamic;
|
|
572
|
-
return arrayElementOffset(descOffset);
|
|
611
|
+
function chainId() {
|
|
612
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.CHAIN_ID]));
|
|
573
613
|
}
|
|
574
|
-
function
|
|
575
|
-
|
|
576
|
-
writeBE32(out, offset, delta);
|
|
577
|
-
writeBE16(out, offset + PolicyFormat.HINT_HOP_INDEX_OFFSET, index);
|
|
578
|
-
writeBE16(out, offset + PolicyFormat.HINT_HOP_META_OFFSET, meta);
|
|
579
|
-
offset += PolicyFormat.HINT_HOP_SIZE;
|
|
580
|
-
}
|
|
581
|
-
return offset;
|
|
614
|
+
function txOrigin() {
|
|
615
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.TX_ORIGIN]));
|
|
582
616
|
}
|
|
583
|
-
function
|
|
584
|
-
|
|
585
|
-
if (walk.kind === PolicyFormat.HINT_KIND_NONE) walk.mainHops = walk.chain;
|
|
586
|
-
else suffixHops = walk.chain;
|
|
587
|
-
const mainHopCount = walk.mainHops.length;
|
|
588
|
-
const suffixHopCount = suffixHops.length;
|
|
589
|
-
if (mainHopCount > PolicyFormat.HINT_HOP_COUNT_MASK || suffixHopCount > PolicyFormat.HINT_HOP_COUNT_MASK) throw uncompilablePath(depth);
|
|
590
|
-
const quantified = walk.kind !== PolicyFormat.HINT_KIND_NONE;
|
|
591
|
-
const frameSize = quantified ? PolicyFormat.HINT_FRAME_PREFIX_SIZE + PolicyFormat.HINT_HEADER_SIZE + suffixHopCount * PolicyFormat.HINT_HOP_SIZE : 0;
|
|
592
|
-
const out = new Uint8Array(PolicyFormat.HINT_HEADER_SIZE + mainHopCount * PolicyFormat.HINT_HOP_SIZE + frameSize + PolicyFormat.HINT_TARGET_SIZE);
|
|
593
|
-
let offset = 0;
|
|
594
|
-
out[offset] = walk.kind << PolicyFormat.HINT_KIND_SHIFT | mainHopCount;
|
|
595
|
-
offset = writeHops(out, offset + PolicyFormat.HINT_HEADER_SIZE, walk.mainHops);
|
|
596
|
-
if (quantified) {
|
|
597
|
-
writeBE32(out, offset, walk.frame.arrayDelta);
|
|
598
|
-
writeBE16(out, offset + PolicyFormat.HINT_FRAME_COUNT_OFFSET, walk.frame.count);
|
|
599
|
-
writeBE16(out, offset + PolicyFormat.HINT_FRAME_META_OFFSET, walk.frame.meta);
|
|
600
|
-
out[offset + PolicyFormat.HINT_FRAME_PREFIX_SIZE] = suffixHopCount;
|
|
601
|
-
offset = writeHops(out, offset + PolicyFormat.HINT_FRAME_PREFIX_SIZE + PolicyFormat.HINT_HEADER_SIZE, suffixHops);
|
|
602
|
-
}
|
|
603
|
-
writeBE32(out, offset, walk.delta);
|
|
604
|
-
writeBE16(out, offset + PolicyFormat.HINT_TARGET_META_OFFSET, targetMeta);
|
|
605
|
-
out[offset + PolicyFormat.HINT_TARGET_TYPECODE_OFFSET] = typeCode;
|
|
606
|
-
return out;
|
|
617
|
+
function baseFee() {
|
|
618
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BASE_FEE]));
|
|
607
619
|
}
|
|
608
|
-
function
|
|
609
|
-
|
|
610
|
-
const argIndex = steps[0];
|
|
611
|
-
if (argIndex >= paramCount(desc)) throw uncompilablePath(0);
|
|
612
|
-
const arg = headSpan(desc, DescriptorFormat.HEADER_SIZE, argIndex);
|
|
613
|
-
const walk = {
|
|
614
|
-
chain: [],
|
|
615
|
-
mainHops: [],
|
|
616
|
-
delta: arg.span,
|
|
617
|
-
frame: {
|
|
618
|
-
arrayDelta: 0,
|
|
619
|
-
count: 0,
|
|
620
|
-
meta: 0
|
|
621
|
-
},
|
|
622
|
-
kind: PolicyFormat.HINT_KIND_NONE,
|
|
623
|
-
entered: false
|
|
624
|
-
};
|
|
625
|
-
let descOffset = arg.nodeOffset;
|
|
626
|
-
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) descOffset = walkStep(desc, walk, descOffset, steps[stepIndex], stepIndex);
|
|
627
|
-
const target = inspect$1(desc, descOffset);
|
|
628
|
-
enterNode(walk, target.isDynamic && !walk.entered);
|
|
629
|
-
return encodeHint(walk, target.typeCode === TypeCode.DYNAMIC_ARRAY ? arrayMeta(desc, descOffset, target.typeCode).meta : 0, target.typeCode, steps.length);
|
|
620
|
+
function gasPrice() {
|
|
621
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.GAS_PRICE]));
|
|
630
622
|
}
|
|
631
|
-
const Descriptor = {
|
|
632
|
-
paramCount,
|
|
633
|
-
paramOffset,
|
|
634
|
-
inspect: inspect$1,
|
|
635
|
-
typeAt,
|
|
636
|
-
tupleFieldOffset,
|
|
637
|
-
tupleFieldCount,
|
|
638
|
-
staticArrayLength,
|
|
639
|
-
nodeLength,
|
|
640
|
-
arrayElementOffset,
|
|
641
|
-
walkPath,
|
|
642
|
-
compileHint
|
|
643
|
-
};
|
|
644
623
|
//#endregion
|
|
645
624
|
//#region src/operators.ts
|
|
646
625
|
function toBigInt(bytes, offset = 0) {
|
|
@@ -712,7 +691,7 @@ function isIn(value, data) {
|
|
|
712
691
|
function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
713
692
|
let base = opCode & ~Op.NOT;
|
|
714
693
|
if (isLengthOp(base)) {
|
|
715
|
-
value =
|
|
694
|
+
value = valueLength;
|
|
716
695
|
typeCode = TypeCode.UINT_MAX;
|
|
717
696
|
base = lengthToValueOp(base);
|
|
718
697
|
}
|
|
@@ -757,7 +736,7 @@ function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
|
757
736
|
result = (value & mask) === 0n;
|
|
758
737
|
break;
|
|
759
738
|
}
|
|
760
|
-
default: throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code
|
|
739
|
+
default: throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code ${formatCode(base, PolicyFormat.RULE_OPCODE_SIZE)}`);
|
|
761
740
|
}
|
|
762
741
|
return (opCode & Op.NOT) !== 0 ? !result : result;
|
|
763
742
|
}
|
|
@@ -768,8 +747,13 @@ function isValidOperatorData(opBase, dataLength) {
|
|
|
768
747
|
if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
|
|
769
748
|
return false;
|
|
770
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
|
+
}
|
|
771
755
|
function unknownTypeCode(code) {
|
|
772
|
-
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code
|
|
756
|
+
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code ${formatCode(code, DescriptorFormat.TYPECODE_SIZE)}`);
|
|
773
757
|
}
|
|
774
758
|
const ELEMENTARY = {
|
|
775
759
|
typeClass: "elementary",
|
|
@@ -805,6 +789,13 @@ function classifyTypeCode(code) {
|
|
|
805
789
|
if (code === TypeCode.TUPLE) return TUPLE;
|
|
806
790
|
unknownTypeCode(code);
|
|
807
791
|
}
|
|
792
|
+
function isElementary(code) {
|
|
793
|
+
try {
|
|
794
|
+
return classifyTypeCode(code).typeClass === "elementary";
|
|
795
|
+
} catch {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
808
799
|
function isAddressableTarget(code) {
|
|
809
800
|
let info;
|
|
810
801
|
try {
|
|
@@ -828,12 +819,243 @@ function typeCodeLabel(code) {
|
|
|
828
819
|
if (code === TypeCode.TUPLE) return "tuple";
|
|
829
820
|
return `0x${code.toString(16).padStart(2, "0")}`;
|
|
830
821
|
}
|
|
831
|
-
function lookupTypeCode(code) {
|
|
832
|
-
return {
|
|
833
|
-
label: typeCodeLabel(code),
|
|
834
|
-
...classifyTypeCode(code)
|
|
822
|
+
function lookupTypeCode(code) {
|
|
823
|
+
return {
|
|
824
|
+
label: typeCodeLabel(code),
|
|
825
|
+
...classifyTypeCode(code)
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
//#endregion
|
|
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
|
+
}
|
|
851
|
+
function nodeLength(desc, offset) {
|
|
852
|
+
return readNode(desc, offset).nodeLength;
|
|
853
|
+
}
|
|
854
|
+
function tupleFieldOffset(desc, tupleOffset, fieldIndex) {
|
|
855
|
+
let cursor = tupleOffset + DescriptorFormat.TUPLE_HEADER_SIZE;
|
|
856
|
+
for (let i = 0; i < fieldIndex; i++) cursor += nodeLength(desc, cursor);
|
|
857
|
+
return cursor;
|
|
858
|
+
}
|
|
859
|
+
function paramCount(desc) {
|
|
860
|
+
if (desc.length < DescriptorFormat.HEADER_SIZE) throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
|
|
861
|
+
return desc[1];
|
|
862
|
+
}
|
|
863
|
+
function paramOffset(desc, index) {
|
|
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})`);
|
|
866
|
+
let cursor = DescriptorFormat.HEADER_SIZE;
|
|
867
|
+
for (let i = 0; i < index; i++) cursor += nodeLength(desc, cursor);
|
|
868
|
+
return cursor;
|
|
869
|
+
}
|
|
870
|
+
function inspect$1(desc, offset) {
|
|
871
|
+
const { typeCode, staticWords } = readNode(desc, offset);
|
|
872
|
+
return {
|
|
873
|
+
typeCode,
|
|
874
|
+
isDynamic: staticWords === 0,
|
|
875
|
+
staticSize: staticWords * 32
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function typeAt(desc, steps) {
|
|
879
|
+
return walkPath(desc, steps).typeInfo;
|
|
880
|
+
}
|
|
881
|
+
function walkPath(desc, steps) {
|
|
882
|
+
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
883
|
+
let cursor = paramOffset(desc, steps[0]);
|
|
884
|
+
let quantifiedStaticLength = 0;
|
|
885
|
+
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
|
|
886
|
+
const typeCode = desc[cursor];
|
|
887
|
+
const step = steps[stepIndex];
|
|
888
|
+
if (typeCode === TypeCode.TUPLE) {
|
|
889
|
+
const fields = tupleFieldCount(desc, cursor);
|
|
890
|
+
if (step >= fields) throw new CallciumError("TUPLE_FIELD_OUT_OF_BOUNDS", `Tuple field index ${step} out of range (tuple has ${fields} fields)`);
|
|
891
|
+
cursor = tupleFieldOffset(desc, cursor, step);
|
|
892
|
+
} else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) {
|
|
893
|
+
if (typeCode === TypeCode.STATIC_ARRAY) if (isQuantifier(step)) quantifiedStaticLength = staticArrayLength(desc, cursor);
|
|
894
|
+
else {
|
|
895
|
+
const length = staticArrayLength(desc, cursor);
|
|
896
|
+
if (step >= length) throw new CallciumError("STATIC_ARRAY_INDEX_OUT_OF_BOUNDS", `Array index ${step} out of range (length=${length})`);
|
|
897
|
+
}
|
|
898
|
+
cursor = arrayElementOffset(cursor);
|
|
899
|
+
} else throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}`);
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
typeInfo: inspect$1(desc, cursor),
|
|
903
|
+
quantifiedStaticLength
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function tupleFieldCount(desc, offset) {
|
|
907
|
+
return readU16(desc, offset + 4);
|
|
908
|
+
}
|
|
909
|
+
function staticArrayLength(desc, offset) {
|
|
910
|
+
const elemOffset = arrayElementOffset(offset);
|
|
911
|
+
return readU16(desc, elemOffset + nodeLength(desc, elemOffset));
|
|
912
|
+
}
|
|
913
|
+
function arrayElementOffset(offset) {
|
|
914
|
+
return offset + DescriptorFormat.ARRAY_HEADER_SIZE;
|
|
915
|
+
}
|
|
916
|
+
function uncompilablePath(stepIndex) {
|
|
917
|
+
return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target`);
|
|
918
|
+
}
|
|
919
|
+
function headSpan(desc, offset, count) {
|
|
920
|
+
let span = 0;
|
|
921
|
+
let nodeOffset = offset;
|
|
922
|
+
for (let i = 0; i < count; i++) {
|
|
923
|
+
const node = inspect$1(desc, nodeOffset);
|
|
924
|
+
span += node.isDynamic ? 32 : node.staticSize;
|
|
925
|
+
nodeOffset += nodeLength(desc, nodeOffset);
|
|
926
|
+
}
|
|
927
|
+
return {
|
|
928
|
+
span,
|
|
929
|
+
nodeOffset
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function arrayMeta(desc, arrayOffset, code) {
|
|
933
|
+
const element = inspect$1(desc, arrayElementOffset(arrayOffset));
|
|
934
|
+
let meta = element.isDynamic ? 1 : element.staticSize >> 5;
|
|
935
|
+
if (element.isDynamic) meta |= PolicyFormat.HINT_META_ELEM_DYNAMIC;
|
|
936
|
+
if (code === TypeCode.DYNAMIC_ARRAY) meta |= PolicyFormat.HINT_META_DYNAMIC_ARRAY;
|
|
937
|
+
return {
|
|
938
|
+
meta,
|
|
939
|
+
elemIsDynamic: element.isDynamic
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
function enterNode(walk, needed) {
|
|
943
|
+
if (!needed) return;
|
|
944
|
+
walk.chain.push([
|
|
945
|
+
walk.delta,
|
|
946
|
+
PolicyFormat.HINT_NO_INDEX,
|
|
947
|
+
0
|
|
948
|
+
]);
|
|
949
|
+
walk.delta = 0;
|
|
950
|
+
}
|
|
951
|
+
function walkStep(desc, walk, descOffset, step, stepIndex) {
|
|
952
|
+
const node = inspect$1(desc, descOffset);
|
|
953
|
+
if (node.typeCode === TypeCode.TUPLE) {
|
|
954
|
+
if (step >= tupleFieldCount(desc, descOffset)) throw uncompilablePath(stepIndex);
|
|
955
|
+
enterNode(walk, node.isDynamic && !walk.entered);
|
|
956
|
+
const field = headSpan(desc, descOffset + DescriptorFormat.TUPLE_HEADER_SIZE, step);
|
|
957
|
+
walk.delta += field.span;
|
|
958
|
+
walk.entered = false;
|
|
959
|
+
return field.nodeOffset;
|
|
960
|
+
}
|
|
961
|
+
if (node.typeCode !== TypeCode.STATIC_ARRAY && node.typeCode !== TypeCode.DYNAMIC_ARRAY) throw uncompilablePath(stepIndex);
|
|
962
|
+
if (node.typeCode === TypeCode.STATIC_ARRAY && !isQuantifier(step) && step >= staticArrayLength(desc, descOffset)) throw uncompilablePath(stepIndex);
|
|
963
|
+
const { meta, elemIsDynamic } = arrayMeta(desc, descOffset, node.typeCode);
|
|
964
|
+
if (isQuantifier(step)) {
|
|
965
|
+
if (walk.kind !== PolicyFormat.HINT_KIND_NONE) throw uncompilablePath(stepIndex);
|
|
966
|
+
enterNode(walk, node.isDynamic && !walk.entered);
|
|
967
|
+
const count = node.typeCode === TypeCode.DYNAMIC_ARRAY ? 0 : staticArrayLength(desc, descOffset);
|
|
968
|
+
walk.frame = {
|
|
969
|
+
arrayDelta: walk.delta,
|
|
970
|
+
count,
|
|
971
|
+
meta
|
|
972
|
+
};
|
|
973
|
+
walk.kind = step === Quantifier.ALL ? PolicyFormat.HINT_KIND_ALL : PolicyFormat.HINT_KIND_ANY;
|
|
974
|
+
walk.mainHops = walk.chain;
|
|
975
|
+
walk.chain = [];
|
|
976
|
+
walk.delta = 0;
|
|
977
|
+
} else if (node.isDynamic) {
|
|
978
|
+
enterNode(walk, !walk.entered);
|
|
979
|
+
walk.chain.push([
|
|
980
|
+
0,
|
|
981
|
+
step,
|
|
982
|
+
meta
|
|
983
|
+
]);
|
|
984
|
+
walk.delta = 0;
|
|
985
|
+
} else walk.delta += step * (meta & PolicyFormat.HINT_META_STRIDE_MASK) * 32;
|
|
986
|
+
walk.entered = elemIsDynamic;
|
|
987
|
+
return arrayElementOffset(descOffset);
|
|
988
|
+
}
|
|
989
|
+
function writeHops(out, offset, hops) {
|
|
990
|
+
for (const [delta, index, meta] of hops) {
|
|
991
|
+
writeBE32(out, offset, delta);
|
|
992
|
+
writeBE16(out, offset + PolicyFormat.HINT_HOP_INDEX_OFFSET, index);
|
|
993
|
+
writeBE16(out, offset + PolicyFormat.HINT_HOP_META_OFFSET, meta);
|
|
994
|
+
offset += PolicyFormat.HINT_HOP_SIZE;
|
|
995
|
+
}
|
|
996
|
+
return offset;
|
|
997
|
+
}
|
|
998
|
+
function encodeHint(walk, targetMeta, typeCode, depth) {
|
|
999
|
+
let suffixHops = [];
|
|
1000
|
+
if (walk.kind === PolicyFormat.HINT_KIND_NONE) walk.mainHops = walk.chain;
|
|
1001
|
+
else suffixHops = walk.chain;
|
|
1002
|
+
const mainHopCount = walk.mainHops.length;
|
|
1003
|
+
const suffixHopCount = suffixHops.length;
|
|
1004
|
+
if (mainHopCount > PolicyFormat.HINT_HOP_COUNT_MASK || suffixHopCount > PolicyFormat.HINT_HOP_COUNT_MASK) throw uncompilablePath(depth);
|
|
1005
|
+
const quantified = walk.kind !== PolicyFormat.HINT_KIND_NONE;
|
|
1006
|
+
const frameSize = quantified ? PolicyFormat.HINT_FRAME_PREFIX_SIZE + PolicyFormat.HINT_HEADER_SIZE + suffixHopCount * PolicyFormat.HINT_HOP_SIZE : 0;
|
|
1007
|
+
const out = new Uint8Array(PolicyFormat.HINT_HEADER_SIZE + mainHopCount * PolicyFormat.HINT_HOP_SIZE + frameSize + PolicyFormat.HINT_TARGET_SIZE);
|
|
1008
|
+
let offset = 0;
|
|
1009
|
+
out[offset] = walk.kind << PolicyFormat.HINT_KIND_SHIFT | mainHopCount;
|
|
1010
|
+
offset = writeHops(out, offset + PolicyFormat.HINT_HEADER_SIZE, walk.mainHops);
|
|
1011
|
+
if (quantified) {
|
|
1012
|
+
writeBE32(out, offset, walk.frame.arrayDelta);
|
|
1013
|
+
writeBE16(out, offset + PolicyFormat.HINT_FRAME_COUNT_OFFSET, walk.frame.count);
|
|
1014
|
+
writeBE16(out, offset + PolicyFormat.HINT_FRAME_META_OFFSET, walk.frame.meta);
|
|
1015
|
+
out[offset + PolicyFormat.HINT_FRAME_PREFIX_SIZE] = suffixHopCount;
|
|
1016
|
+
offset = writeHops(out, offset + PolicyFormat.HINT_FRAME_PREFIX_SIZE + PolicyFormat.HINT_HEADER_SIZE, suffixHops);
|
|
1017
|
+
}
|
|
1018
|
+
writeBE32(out, offset, walk.delta);
|
|
1019
|
+
writeBE16(out, offset + PolicyFormat.HINT_TARGET_META_OFFSET, targetMeta);
|
|
1020
|
+
out[offset + PolicyFormat.HINT_TARGET_TYPECODE_OFFSET] = typeCode;
|
|
1021
|
+
return out;
|
|
1022
|
+
}
|
|
1023
|
+
function compileHint(desc, steps) {
|
|
1024
|
+
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
1025
|
+
const argIndex = steps[0];
|
|
1026
|
+
if (argIndex >= paramCount(desc)) throw uncompilablePath(0);
|
|
1027
|
+
const arg = headSpan(desc, DescriptorFormat.HEADER_SIZE, argIndex);
|
|
1028
|
+
const walk = {
|
|
1029
|
+
chain: [],
|
|
1030
|
+
mainHops: [],
|
|
1031
|
+
delta: arg.span,
|
|
1032
|
+
frame: {
|
|
1033
|
+
arrayDelta: 0,
|
|
1034
|
+
count: 0,
|
|
1035
|
+
meta: 0
|
|
1036
|
+
},
|
|
1037
|
+
kind: PolicyFormat.HINT_KIND_NONE,
|
|
1038
|
+
entered: false
|
|
835
1039
|
};
|
|
1040
|
+
let descOffset = arg.nodeOffset;
|
|
1041
|
+
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) descOffset = walkStep(desc, walk, descOffset, steps[stepIndex], stepIndex);
|
|
1042
|
+
const target = inspect$1(desc, descOffset);
|
|
1043
|
+
enterNode(walk, target.isDynamic && !walk.entered);
|
|
1044
|
+
return encodeHint(walk, target.typeCode === TypeCode.DYNAMIC_ARRAY ? arrayMeta(desc, descOffset, target.typeCode).meta : 0, target.typeCode, steps.length);
|
|
836
1045
|
}
|
|
1046
|
+
const Descriptor = {
|
|
1047
|
+
paramCount,
|
|
1048
|
+
paramOffset,
|
|
1049
|
+
inspect: inspect$1,
|
|
1050
|
+
typeAt,
|
|
1051
|
+
tupleFieldOffset,
|
|
1052
|
+
tupleFieldCount,
|
|
1053
|
+
staticArrayLength,
|
|
1054
|
+
nodeLength,
|
|
1055
|
+
arrayElementOffset,
|
|
1056
|
+
walkPath,
|
|
1057
|
+
compileHint
|
|
1058
|
+
};
|
|
837
1059
|
//#endregion
|
|
838
1060
|
//#region src/type-desc.ts
|
|
839
1061
|
function extractStaticWords(desc) {
|
|
@@ -864,9 +1086,9 @@ function bytes() {
|
|
|
864
1086
|
function string_() {
|
|
865
1087
|
return new Uint8Array([TypeCode.STRING]);
|
|
866
1088
|
}
|
|
867
|
-
function bytesN(
|
|
868
|
-
if (
|
|
869
|
-
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)]);
|
|
870
1092
|
}
|
|
871
1093
|
function array(elemDesc, length) {
|
|
872
1094
|
if (length === void 0) {
|
|
@@ -927,9 +1149,12 @@ function commaPositions(input, start, end) {
|
|
|
927
1149
|
for (let i = start; i < end; i++) {
|
|
928
1150
|
const char = input[i];
|
|
929
1151
|
if (char === "(" || char === "[") depth++;
|
|
930
|
-
else if (char === ")" || char === "]")
|
|
931
|
-
|
|
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);
|
|
932
1156
|
}
|
|
1157
|
+
if (depth !== 0) throw new CallciumError("MALFORMED_TYPE_STRING", `Unclosed '${input.slice(start, end)}'`);
|
|
933
1158
|
return positions;
|
|
934
1159
|
}
|
|
935
1160
|
const MAX_TYPE_STRING_NUMBER = 256 ** DescriptorFormat.ARRAY_LENGTH_SIZE - 1;
|
|
@@ -963,12 +1188,12 @@ function parseTuple(input, start, end) {
|
|
|
963
1188
|
const innerEnd = closeIndex;
|
|
964
1189
|
const commas = commaPositions(input, innerStart, innerEnd);
|
|
965
1190
|
const segments = [];
|
|
966
|
-
let
|
|
1191
|
+
let segmentStart = innerStart;
|
|
967
1192
|
for (const comma of commas) {
|
|
968
|
-
segments.push([
|
|
969
|
-
|
|
1193
|
+
segments.push([segmentStart, comma]);
|
|
1194
|
+
segmentStart = comma + 1;
|
|
970
1195
|
}
|
|
971
|
-
segments.push([
|
|
1196
|
+
segments.push([segmentStart, innerEnd]);
|
|
972
1197
|
if (segments.length === 1 && segments[0][0] === segments[0][1]) return tuple([]);
|
|
973
1198
|
return tuple(segments.map(([fieldStart, fieldEnd]) => parseType(input, fieldStart, fieldEnd)));
|
|
974
1199
|
}
|
|
@@ -980,28 +1205,25 @@ function parseBaseType(input, start, end) {
|
|
|
980
1205
|
if (segment === "bytes") return bytes();
|
|
981
1206
|
if (segment === "string") return string_();
|
|
982
1207
|
if (segment.startsWith("(")) return parseTuple(input, start, end);
|
|
983
|
-
if (segment.startsWith("uint")) return uintN(parseUint(input, start + 4, end));
|
|
984
|
-
if (segment.startsWith("int")) return intN(parseUint(input, start + 3, end));
|
|
985
|
-
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));
|
|
986
1211
|
throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
|
|
987
1212
|
}
|
|
988
1213
|
function parseType(input, start, end) {
|
|
989
1214
|
if (start >= end) throw new CallciumError("MALFORMED_TYPE_STRING", "Empty type segment");
|
|
990
|
-
const
|
|
1215
|
+
const spans = [];
|
|
991
1216
|
let baseEnd = end;
|
|
992
1217
|
while (baseEnd > start && input[baseEnd - 1] === "]") {
|
|
993
1218
|
const closeIndex = baseEnd - 1;
|
|
994
1219
|
let openIndex = closeIndex - 1;
|
|
995
1220
|
while (openIndex > start && input[openIndex] !== "[") openIndex--;
|
|
996
1221
|
if (input[openIndex] !== "[") throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched ']' at position ${closeIndex}`);
|
|
997
|
-
|
|
998
|
-
const innerEnd = closeIndex;
|
|
999
|
-
suffixes.unshift(innerStart === innerEnd ? void 0 : parseUint(input, innerStart, innerEnd));
|
|
1222
|
+
spans.unshift([openIndex + 1, closeIndex]);
|
|
1000
1223
|
baseEnd = openIndex;
|
|
1001
1224
|
}
|
|
1002
|
-
if (input[start] !== "(" && input.slice(start, baseEnd).includes("[")) throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '[' in '${input.slice(start, baseEnd)}'`);
|
|
1003
1225
|
let desc = parseBaseType(input, start, baseEnd);
|
|
1004
|
-
for (const
|
|
1226
|
+
for (const [innerStart, innerEnd] of spans) desc = array(desc, innerStart === innerEnd ? void 0 : parseUint(input, innerStart, innerEnd));
|
|
1005
1227
|
return desc;
|
|
1006
1228
|
}
|
|
1007
1229
|
function fromTypes(typesCsv) {
|
|
@@ -1025,10 +1247,11 @@ function fromTypes(typesCsv) {
|
|
|
1025
1247
|
result.set(desc, offset);
|
|
1026
1248
|
offset += desc.length;
|
|
1027
1249
|
}
|
|
1250
|
+
decodeDescriptor(result);
|
|
1028
1251
|
return result;
|
|
1029
1252
|
}
|
|
1030
1253
|
function nodeToTypeString(desc, offset) {
|
|
1031
|
-
const typeCode = desc
|
|
1254
|
+
const { typeCode } = Descriptor.inspect(desc, offset);
|
|
1032
1255
|
if (typeCode === TypeCode.TUPLE) {
|
|
1033
1256
|
const fieldCount = Descriptor.tupleFieldCount(desc, offset);
|
|
1034
1257
|
const fieldTypes = [];
|
|
@@ -1057,30 +1280,20 @@ function toTypes(desc) {
|
|
|
1057
1280
|
return types.join(",");
|
|
1058
1281
|
}
|
|
1059
1282
|
function parseNode(data, offset, depth) {
|
|
1060
|
-
|
|
1061
|
-
const code = data[offset];
|
|
1283
|
+
const { typeCode: code, staticWords, nodeLength } = readNode(data, offset);
|
|
1062
1284
|
const info = classifyTypeCode(code);
|
|
1063
|
-
const
|
|
1285
|
+
const isDynamic = staticWords === 0;
|
|
1286
|
+
const staticSize = isDynamic ? 0 : staticWords * 32;
|
|
1287
|
+
const nodeEnd = offset + nodeLength;
|
|
1064
1288
|
if (info.typeClass === "elementary") return {
|
|
1065
1289
|
typeCode: code,
|
|
1066
|
-
isDynamic
|
|
1067
|
-
staticSize
|
|
1068
|
-
next:
|
|
1290
|
+
isDynamic,
|
|
1291
|
+
staticSize,
|
|
1292
|
+
next: nodeEnd
|
|
1069
1293
|
};
|
|
1070
1294
|
if (depth > DescriptorFormat.MAX_NESTING_DEPTH) throw new CallciumError("NESTING_TOO_DEEP", `Composite nesting exceeds maximum depth ${DescriptorFormat.MAX_NESTING_DEPTH}`, offset);
|
|
1071
|
-
const metaEnd = metaOffset + DescriptorFormat.COMPOSITE_META_SIZE;
|
|
1072
|
-
if (metaEnd > data.length) throw new CallciumError("UNEXPECTED_END", "Incomplete composite metadata", offset);
|
|
1073
|
-
const meta = readU24(data, metaOffset);
|
|
1074
|
-
const staticWords = meta >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
|
|
1075
|
-
const nodeLength = meta & DescriptorFormat.META_NODE_LENGTH_MASK;
|
|
1076
|
-
const minHeader = info.typeClass === "tuple" ? DescriptorFormat.TUPLE_HEADER_SIZE : DescriptorFormat.ARRAY_HEADER_SIZE;
|
|
1077
|
-
if (nodeLength < minHeader) throw new CallciumError("NODE_LENGTH_TOO_SMALL", `Composite node length ${nodeLength} is smaller than minimum header ${minHeader}`, offset);
|
|
1078
|
-
if (offset + nodeLength > data.length) throw new CallciumError("NODE_OVERFLOW", "Composite node extends beyond descriptor", offset);
|
|
1079
|
-
const isDynamic = staticWords === 0;
|
|
1080
|
-
const staticSize = isDynamic ? 0 : staticWords * 32;
|
|
1081
|
-
const nodeEnd = offset + nodeLength;
|
|
1082
1295
|
if (info.typeClass === "tuple") {
|
|
1083
|
-
const fieldCountOffset =
|
|
1296
|
+
const fieldCountOffset = offset + DescriptorFormat.TYPECODE_SIZE + DescriptorFormat.COMPOSITE_META_SIZE;
|
|
1084
1297
|
if (fieldCountOffset + 2 > data.length) throw new CallciumError("UNEXPECTED_END", "Incomplete tuple header", offset);
|
|
1085
1298
|
const fieldCount = readU16(data, fieldCountOffset);
|
|
1086
1299
|
if (fieldCount === 0) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", "Tuple must have at least one field", offset);
|
|
@@ -1126,8 +1339,8 @@ function decodeDescriptor(data) {
|
|
|
1126
1339
|
const params = [];
|
|
1127
1340
|
let cursor = DescriptorFormat.HEADER_SIZE;
|
|
1128
1341
|
while (cursor < data.length) {
|
|
1129
|
-
if (params.length >= DescriptorFormat.MAX_PARAMS) throw new CallciumError("TOO_MANY_PARAMS", `Descriptor exceeds the maximum of ${DescriptorFormat.MAX_PARAMS} top-level params`);
|
|
1130
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`);
|
|
1131
1344
|
params.push({
|
|
1132
1345
|
index: params.length,
|
|
1133
1346
|
typeCode,
|
|
@@ -1163,6 +1376,21 @@ function field(value, start, end) {
|
|
|
1163
1376
|
}
|
|
1164
1377
|
};
|
|
1165
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
|
+
}
|
|
1166
1394
|
function resolveHintSize(data, hintStart, ruleEnd, ruleOffset) {
|
|
1167
1395
|
const overrun = () => {
|
|
1168
1396
|
throw new CallciumError("RULE_FIELD_OUT_OF_BOUNDS", "Rule is too small to hold a hint block", ruleOffset);
|
|
@@ -1219,7 +1447,7 @@ function hintTargetTypeCode(data, hintStart, hintSize) {
|
|
|
1219
1447
|
}
|
|
1220
1448
|
function decodePolicy(blob) {
|
|
1221
1449
|
const data = hexToBytes(blob);
|
|
1222
|
-
if (data.length < PolicyFormat.DESC_OFFSET
|
|
1450
|
+
if (data.length < PolicyFormat.DESC_OFFSET) throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short");
|
|
1223
1451
|
const headerByte = data[0];
|
|
1224
1452
|
const version = headerByte & PolicyFormat.VERSION_MASK;
|
|
1225
1453
|
if (version !== PolicyFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${PolicyFormat.VERSION})`);
|
|
@@ -1244,7 +1472,7 @@ function decodePolicy(blob) {
|
|
|
1244
1472
|
end: descStart + param.span.end
|
|
1245
1473
|
}
|
|
1246
1474
|
}));
|
|
1247
|
-
const descriptorRaw =
|
|
1475
|
+
const descriptorRaw = hexField(data, descStart, descEnd);
|
|
1248
1476
|
const groupCountStart = groupCountOffset;
|
|
1249
1477
|
const groupCountEnd = groupCountStart + PolicyFormat.GROUP_COUNT_SIZE;
|
|
1250
1478
|
const groupCountValue = data[groupCountOffset];
|
|
@@ -1257,18 +1485,18 @@ function decodePolicy(blob) {
|
|
|
1257
1485
|
const groupSizeValue = readU32(data, groupSizeStart);
|
|
1258
1486
|
const groupBodyStart = offset + PolicyFormat.GROUP_HEADER_SIZE;
|
|
1259
1487
|
const groupEnd = groupBodyStart + groupSizeValue;
|
|
1488
|
+
if (groupEnd > data.length) throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
|
|
1260
1489
|
const ruleCountStart = offset;
|
|
1261
1490
|
const ruleCountValue = readU16(data, ruleCountStart);
|
|
1262
1491
|
if (ruleCountValue === 0) throw new CallciumError("EMPTY_GROUP", "Group must contain at least one rule", offset);
|
|
1263
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);
|
|
1264
|
-
if (groupEnd > data.length) throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
|
|
1265
1493
|
const rules = [];
|
|
1266
1494
|
let ruleOffset = groupBodyStart;
|
|
1267
1495
|
for (let ruleIndex = 0; ruleIndex < ruleCountValue; ruleIndex++) {
|
|
1268
1496
|
if (ruleOffset + PolicyFormat.RULE_SIZE_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading rule size", ruleOffset);
|
|
1269
1497
|
const ruleSizeValue = readU16(data, ruleOffset);
|
|
1270
1498
|
if (ruleSizeValue < PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("RULE_TOO_SMALL", `Rule size ${ruleSizeValue} is below minimum ${PolicyFormat.RULE_MIN_SIZE}`, ruleOffset);
|
|
1271
|
-
if (ruleOffset + ruleSizeValue >
|
|
1499
|
+
if (ruleOffset + ruleSizeValue > data.length) throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond policy blob", ruleOffset);
|
|
1272
1500
|
const scopeOffset = ruleOffset + PolicyFormat.RULE_SCOPE_OFFSET;
|
|
1273
1501
|
const scopeValue = data[scopeOffset];
|
|
1274
1502
|
if (scopeValue !== Scope.CONTEXT && scopeValue !== Scope.CALLDATA) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${scopeValue}`, ruleOffset);
|
|
@@ -1297,7 +1525,12 @@ function decodePolicy(blob) {
|
|
|
1297
1525
|
const dataStart = dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE;
|
|
1298
1526
|
if (opBase === Op.IN) {
|
|
1299
1527
|
const wordCount = dataLengthValue / 32;
|
|
1300
|
-
|
|
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
|
+
}
|
|
1301
1534
|
}
|
|
1302
1535
|
if (opBase === Op.EQ_CTX) {
|
|
1303
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);
|
|
@@ -1307,7 +1540,7 @@ function decodePolicy(blob) {
|
|
|
1307
1540
|
if (scopeValue === Scope.CONTEXT) {
|
|
1308
1541
|
if (depthValue !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step", ruleOffset);
|
|
1309
1542
|
const contextPropertyId = readU16(data, pathStart);
|
|
1310
|
-
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);
|
|
1311
1544
|
}
|
|
1312
1545
|
const pathHex = toHex(data, pathStart, pathStart + pathLength);
|
|
1313
1546
|
rules.push({
|
|
@@ -1315,15 +1548,16 @@ function decodePolicy(blob) {
|
|
|
1315
1548
|
scope: field(scopeValue, scopeOffset, scopeOffset + 1),
|
|
1316
1549
|
pathDepth: field(depthValue, depthOffset, depthOffset + 1),
|
|
1317
1550
|
path: field(pathHex, pathStart, pathStart + pathLength),
|
|
1318
|
-
...hintSize > 0 && { hint:
|
|
1551
|
+
...hintSize > 0 && { hint: hexField(data, hintStart, hintStart + hintSize) },
|
|
1319
1552
|
opCode: field(opCodeValue, opCodeOffset, opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE),
|
|
1320
1553
|
dataLength: field(dataLengthValue, dataLengthOffset, dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE),
|
|
1321
|
-
data:
|
|
1554
|
+
data: hexField(data, dataStart, dataStart + dataLengthValue),
|
|
1322
1555
|
span: {
|
|
1323
1556
|
start: ruleOffset,
|
|
1324
1557
|
end: ruleOffset + ruleSizeValue
|
|
1325
1558
|
}
|
|
1326
1559
|
});
|
|
1560
|
+
if (ruleOffset + ruleSizeValue > groupEnd) throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
|
|
1327
1561
|
ruleOffset += ruleSizeValue;
|
|
1328
1562
|
}
|
|
1329
1563
|
if (ruleOffset !== groupEnd) throw new CallciumError("GROUP_SIZE_MISMATCH", "Rules do not exactly fill the declared group size", offset);
|
|
@@ -1345,7 +1579,12 @@ function decodePolicy(blob) {
|
|
|
1345
1579
|
selector: field(selectorHex, selectorStart, selectorEnd),
|
|
1346
1580
|
descLength: field(descLengthValue, descLengthStart, descLengthStart + PolicyFormat.DESC_LENGTH_SIZE),
|
|
1347
1581
|
descriptor: {
|
|
1348
|
-
raw
|
|
1582
|
+
get raw() {
|
|
1583
|
+
return descriptorRaw.value;
|
|
1584
|
+
},
|
|
1585
|
+
set raw(replacement) {
|
|
1586
|
+
descriptorRaw.value = replacement;
|
|
1587
|
+
},
|
|
1349
1588
|
header: field({
|
|
1350
1589
|
version: desc.version,
|
|
1351
1590
|
paramCount: desc.params.length
|
|
@@ -1368,21 +1607,28 @@ function decodePolicy(blob) {
|
|
|
1368
1607
|
data
|
|
1369
1608
|
};
|
|
1370
1609
|
}
|
|
1371
|
-
function
|
|
1372
|
-
const operators = constraint.operators.map(hexToBytes);
|
|
1373
|
-
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) {
|
|
1374
1611
|
const path = hexToBytes(constraint.path);
|
|
1375
1612
|
if (path.length === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step");
|
|
1376
1613
|
if ((path.length & 1) !== 0) throw new CallciumError("MALFORMED_PATH", "Path byte length must be even");
|
|
1377
1614
|
const depth = path.length / 2;
|
|
1378
1615
|
if (depth > PolicyFormat.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${PolicyFormat.MAX_PATH_DEPTH}`);
|
|
1379
|
-
|
|
1380
|
-
return operators.map((operator) => ({
|
|
1381
|
-
scope: constraint.scope,
|
|
1616
|
+
return {
|
|
1382
1617
|
path,
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
+
});
|
|
1386
1632
|
}
|
|
1387
1633
|
function compareBytes(a, b) {
|
|
1388
1634
|
const length = Math.min(a.length, b.length);
|
|
@@ -1400,12 +1646,16 @@ function sortRules(rules) {
|
|
|
1400
1646
|
return compareBytes(a.operator, b.operator);
|
|
1401
1647
|
});
|
|
1402
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
|
+
}
|
|
1403
1653
|
function encodeRule(rule) {
|
|
1404
1654
|
const depth = rule.path.length / 2;
|
|
1405
1655
|
if (rule.scope === Scope.CONTEXT && depth !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step");
|
|
1406
1656
|
const data = rule.operator.subarray(1);
|
|
1407
1657
|
const ruleSize = PolicyFormat.RULE_FIXED_OVERHEAD + rule.path.length + rule.hint.length + data.length;
|
|
1408
|
-
|
|
1658
|
+
requireFieldFits(ruleSize, PolicyFormat.RULE_SIZE_SIZE, "RULE_SIZE_OVERFLOW", "Rule size");
|
|
1409
1659
|
const buf = new Uint8Array(ruleSize);
|
|
1410
1660
|
writeBE16(buf, 0, ruleSize);
|
|
1411
1661
|
buf[2] = rule.scope;
|
|
@@ -1435,16 +1685,16 @@ function buildOperatorHex(rule) {
|
|
|
1435
1685
|
function encode(data) {
|
|
1436
1686
|
const descBytes = hexToBytes(data.descriptor);
|
|
1437
1687
|
if (data.groups.length === 0) throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
|
|
1438
|
-
|
|
1688
|
+
requireFieldFits(data.groups.length, PolicyFormat.GROUP_COUNT_SIZE, "GROUP_COUNT_OVERFLOW", "Group count");
|
|
1439
1689
|
const sortedGroups = data.groups.map((group) => {
|
|
1440
1690
|
const rules = group.flatMap((constraint) => flattenConstraint(constraint, descBytes));
|
|
1441
1691
|
sortRules(rules);
|
|
1442
1692
|
return rules;
|
|
1443
1693
|
});
|
|
1444
|
-
|
|
1694
|
+
requireFieldFits(descBytes.length, PolicyFormat.DESC_LENGTH_SIZE, "DESC_LENGTH_OVERFLOW", "Descriptor length");
|
|
1445
1695
|
const encodedGroups = sortedGroups.map((rules, groupIndex) => {
|
|
1446
1696
|
if (rules.length === 0) throw new CallciumError("EMPTY_GROUP", `Group ${groupIndex} is empty`);
|
|
1447
|
-
|
|
1697
|
+
requireFieldFits(rules.length, PolicyFormat.GROUP_RULECOUNT_SIZE, "RULE_COUNT_OVERFLOW", `Group ${groupIndex} rule count`);
|
|
1448
1698
|
return {
|
|
1449
1699
|
wireBytes: encodeGroupRules(rules),
|
|
1450
1700
|
ruleCount: rules.length
|
|
@@ -1454,7 +1704,8 @@ function encode(data) {
|
|
|
1454
1704
|
const hashes = new Map(encodedGroups.map((group) => [group, keccak_256(group.wireBytes)]));
|
|
1455
1705
|
encodedGroups.sort((a, b) => compareBytes(hashes.get(a), hashes.get(b)));
|
|
1456
1706
|
}
|
|
1457
|
-
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}`);
|
|
1458
1709
|
const headerByte = PolicyFormat.VERSION | (data.isSelectorless ? PolicyFormat.FLAG_NO_SELECTOR : 0);
|
|
1459
1710
|
let totalSize = PolicyFormat.HEADER_SIZE + PolicyFormat.SELECTOR_SIZE + PolicyFormat.DESC_LENGTH_SIZE + descBytes.length + PolicyFormat.GROUP_COUNT_SIZE;
|
|
1460
1711
|
for (const g of encodedGroups) totalSize += PolicyFormat.GROUP_HEADER_SIZE + g.wireBytes.length;
|
|
@@ -1482,7 +1733,6 @@ function decode(blob) {
|
|
|
1482
1733
|
const { policy } = decodePolicy(blob);
|
|
1483
1734
|
const groups = policy.groups.map((group) => {
|
|
1484
1735
|
const constraintMap = /* @__PURE__ */ new Map();
|
|
1485
|
-
const constraintOrder = [];
|
|
1486
1736
|
for (const rule of group.rules) {
|
|
1487
1737
|
const hintHex = rule.hint?.value;
|
|
1488
1738
|
const key = `${rule.scope.value}:${rule.path.value}:${hintHex ?? ""}`;
|
|
@@ -1498,10 +1748,9 @@ function decode(blob) {
|
|
|
1498
1748
|
span: rule.span
|
|
1499
1749
|
};
|
|
1500
1750
|
constraintMap.set(key, constraint);
|
|
1501
|
-
constraintOrder.push(key);
|
|
1502
1751
|
}
|
|
1503
1752
|
}
|
|
1504
|
-
return
|
|
1753
|
+
return [...constraintMap.values()];
|
|
1505
1754
|
});
|
|
1506
1755
|
return {
|
|
1507
1756
|
isSelectorless: policy.isSelectorless,
|
|
@@ -1945,7 +2194,6 @@ function negationUnderAny(groupIndex, constraintIndex, opCode) {
|
|
|
1945
2194
|
//#endregion
|
|
1946
2195
|
//#region src/policy-validator.ts
|
|
1947
2196
|
const navigabilityCodes = new Set([
|
|
1948
|
-
"EMPTY_PATH",
|
|
1949
2197
|
"PARAM_INDEX_OUT_OF_BOUNDS",
|
|
1950
2198
|
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
1951
2199
|
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
@@ -1956,8 +2204,11 @@ const UINT256_MAX = (1n << 256n) - 1n;
|
|
|
1956
2204
|
const INT256_SIGN_BIT = 1n << 255n;
|
|
1957
2205
|
function signedCompare(a, b, signed) {
|
|
1958
2206
|
if (!signed) return a - b;
|
|
1959
|
-
const
|
|
1960
|
-
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;
|
|
1961
2212
|
}
|
|
1962
2213
|
function isNumericType(typeCode) {
|
|
1963
2214
|
return typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX || typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
|
|
@@ -2015,7 +2266,7 @@ function getIncompat(opBase, typeInfo) {
|
|
|
2015
2266
|
code: "VALUE_OP_ON_DYNAMIC",
|
|
2016
2267
|
message: "Value operator used on dynamic type"
|
|
2017
2268
|
};
|
|
2018
|
-
if (
|
|
2269
|
+
if (!isElementary(typeCode)) return {
|
|
2019
2270
|
code: "VALUE_OP_ON_COMPOSITE",
|
|
2020
2271
|
message: "Value operator used on composite type"
|
|
2021
2272
|
};
|
|
@@ -2126,7 +2377,7 @@ function initContext(scope, path, steps, typeInfo) {
|
|
|
2126
2377
|
numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
|
|
2127
2378
|
bitmask: {
|
|
2128
2379
|
mustBeOne: 0n,
|
|
2129
|
-
mustBeZero: 0n
|
|
2380
|
+
mustBeZero: isBitmaskCompatible(typeInfo.typeCode) ? UINT256_MAX ^ max : 0n
|
|
2130
2381
|
},
|
|
2131
2382
|
length: emptyBoundDomain(false, 0n, LENGTH_MAX),
|
|
2132
2383
|
set: {
|
|
@@ -2139,6 +2390,7 @@ function initContext(scope, path, steps, typeInfo) {
|
|
|
2139
2390
|
function updateBound(domain, base, isNegated, value, isLength, groupIndex, constraintIndex, issues) {
|
|
2140
2391
|
if (isNegated) {
|
|
2141
2392
|
if (base === Op.EQ) {
|
|
2393
|
+
if (isOutsideDomain(domain, value)) issues.push(outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2142
2394
|
if (domain.hasEq && domain.eq === value) issues.push(eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2143
2395
|
let alreadyHole = false;
|
|
2144
2396
|
for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) {
|
|
@@ -2151,7 +2403,7 @@ function updateBound(domain, base, isNegated, value, isLength, groupIndex, const
|
|
|
2151
2403
|
}
|
|
2152
2404
|
if (base === Op.GTE && value === domain.min) issues.push(vacuousGte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2153
2405
|
else if (base === Op.LTE && value === domain.max) issues.push(vacuousLte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2154
|
-
if (
|
|
2406
|
+
if (isOutsideDomain(domain, value)) issues.push(outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2155
2407
|
else if (base === Op.GT && value === domain.max) issues.push(impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2156
2408
|
else if (base === Op.LT && value === domain.min) issues.push(impossibleLt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2157
2409
|
let changedEq = false;
|
|
@@ -2266,6 +2518,7 @@ function checkSetEmpty(ctx, groupIndex, constraintIndex, issues) {
|
|
|
2266
2518
|
else if (possibleCount < inCount) issues.push(setPartiallyExcluded(groupIndex, constraintIndex, bigintToHex(BigInt(inCount - possibleCount))));
|
|
2267
2519
|
}
|
|
2268
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)));
|
|
2269
2522
|
if (isNegated) {
|
|
2270
2523
|
for (const value of values) {
|
|
2271
2524
|
if (ctx.numeric.hasEq && ctx.numeric.eq === value) issues.push(setExcludesEquality(groupIndex, constraintIndex, bigintToHex(value)));
|
|
@@ -2281,7 +2534,6 @@ function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues)
|
|
|
2281
2534
|
}
|
|
2282
2535
|
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
2283
2536
|
} else {
|
|
2284
|
-
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)));
|
|
2285
2537
|
if (ctx.numeric.hasEq) {
|
|
2286
2538
|
let found = false;
|
|
2287
2539
|
for (const candidate of values) if (candidate === ctx.numeric.eq) {
|
|
@@ -2323,6 +2575,9 @@ function checkFusibleRange(ctx, operators, groupIndex, constraintIndex, issues)
|
|
|
2323
2575
|
const lengthPair = findLonePair(operators, Op.LENGTH_GTE, Op.LENGTH_LTE);
|
|
2324
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)));
|
|
2325
2577
|
}
|
|
2578
|
+
function hasValidPayload(opBase, dataLength) {
|
|
2579
|
+
return dataLength <= fieldMax(PolicyFormat.RULE_DATALENGTH_SIZE) && isValidOperatorData(opBase, dataLength);
|
|
2580
|
+
}
|
|
2326
2581
|
function findLonePair(operators, lowerOp, upperOp) {
|
|
2327
2582
|
let lowerCount = 0;
|
|
2328
2583
|
let upperCount = 0;
|
|
@@ -2331,7 +2586,7 @@ function findLonePair(operators, lowerOp, upperOp) {
|
|
|
2331
2586
|
for (const opHex of operators) {
|
|
2332
2587
|
const opCode = parseInt(opHex.slice(2, 4), 16);
|
|
2333
2588
|
const dataLength = (opHex.length - 4) / 2;
|
|
2334
|
-
if (
|
|
2589
|
+
if (!hasValidPayload(opCode & ~Op.NOT, dataLength)) continue;
|
|
2335
2590
|
if (opCode === lowerOp) {
|
|
2336
2591
|
lowerCount++;
|
|
2337
2592
|
low = readValue(opHex);
|
|
@@ -2349,10 +2604,10 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
|
|
|
2349
2604
|
const operators = constraint.operators;
|
|
2350
2605
|
const underAny = constraint.scope === Scope.CALLDATA && ctx.steps.includes(Quantifier.ANY);
|
|
2351
2606
|
for (const opHex of operators) {
|
|
2352
|
-
const
|
|
2607
|
+
const opBytes = toOperatorBytes(opHex);
|
|
2608
|
+
const opCode = opBytes[0];
|
|
2353
2609
|
const base = opCode & ~Op.NOT;
|
|
2354
|
-
|
|
2355
|
-
if (dataLength > 65535 || !isValidOperatorData(base, dataLength)) {
|
|
2610
|
+
if (!hasValidPayload(base, opBytes.length - 1)) {
|
|
2356
2611
|
issues.push(fromOpRule("UNKNOWN_OPERATOR", "Unknown operator code", groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
2357
2612
|
continue;
|
|
2358
2613
|
}
|
|
@@ -2365,8 +2620,9 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
|
|
|
2365
2620
|
}
|
|
2366
2621
|
if (base === Op.EQ_CTX) {
|
|
2367
2622
|
const ctxOperand = readValue(opHex);
|
|
2368
|
-
|
|
2369
|
-
|
|
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))));
|
|
2370
2626
|
} else if (isValueOp(base)) {
|
|
2371
2627
|
const typeCode = ctx.typeInfo.typeCode;
|
|
2372
2628
|
if (isLeftAligned(typeCode)) {
|
|
@@ -2420,6 +2676,7 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2420
2676
|
}
|
|
2421
2677
|
if (!ctx) {
|
|
2422
2678
|
const steps = parsePathSteps(constraint.path);
|
|
2679
|
+
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
2423
2680
|
let typeInfo;
|
|
2424
2681
|
if (constraint.scope === Scope.CALLDATA) {
|
|
2425
2682
|
if (steps.length > PolicyFormat.MAX_PATH_DEPTH) issues.push(pathDepthExceeded(groupIndex, constraintIndex, bigintToHex(BigInt(steps.length)), bigintToHex(BigInt(PolicyFormat.MAX_PATH_DEPTH))));
|
|
@@ -2441,15 +2698,10 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2441
2698
|
typeInfo = walk.typeInfo;
|
|
2442
2699
|
} else {
|
|
2443
2700
|
const ctxId = steps[0];
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
isDynamic: false,
|
|
2449
|
-
staticSize: 32
|
|
2450
|
-
};
|
|
2451
|
-
} else typeInfo = {
|
|
2452
|
-
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,
|
|
2453
2705
|
isDynamic: false,
|
|
2454
2706
|
staticSize: 32
|
|
2455
2707
|
};
|
|
@@ -2457,7 +2709,7 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2457
2709
|
ctx = initContext(constraint.scope, normalizedPath, steps, typeInfo);
|
|
2458
2710
|
contexts.push(ctx);
|
|
2459
2711
|
}
|
|
2460
|
-
if (constraint.scope === Scope.CALLDATA && constraint.hint !== void 0 && ctx.steps.length <= PolicyFormat.MAX_PATH_DEPTH) {
|
|
2712
|
+
if (constraint.scope === Scope.CALLDATA && constraint.hint !== void 0 && constraint.hint !== "0x" && ctx.steps.length <= PolicyFormat.MAX_PATH_DEPTH) {
|
|
2461
2713
|
if (bytesToHex(Descriptor.compileHint(descBytes, ctx.steps)) !== constraint.hint.toLowerCase()) issues.push(hintMismatch(groupIndex, constraintIndex));
|
|
2462
2714
|
}
|
|
2463
2715
|
validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues);
|
|
@@ -2480,33 +2732,31 @@ function validate(data) {
|
|
|
2480
2732
|
const PolicyValidator = { validate };
|
|
2481
2733
|
//#endregion
|
|
2482
2734
|
//#region src/signature.ts
|
|
2483
|
-
function isAlpha(
|
|
2484
|
-
return
|
|
2735
|
+
function isAlpha(char) {
|
|
2736
|
+
return char >= 65 && char <= 90 || char >= 97 && char <= 122;
|
|
2485
2737
|
}
|
|
2486
|
-
function isAlphanum(
|
|
2487
|
-
return isAlpha(
|
|
2738
|
+
function isAlphanum(char) {
|
|
2739
|
+
return isAlpha(char) || char >= 48 && char <= 57;
|
|
2488
2740
|
}
|
|
2489
2741
|
function parse(signature) {
|
|
2490
2742
|
if (signature.length < 3) throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: too short, got "${signature}"`);
|
|
2491
2743
|
let openParen = -1;
|
|
2492
2744
|
for (let i = 0; i < signature.length; i++) {
|
|
2493
|
-
const
|
|
2494
|
-
if (
|
|
2495
|
-
if (
|
|
2496
|
-
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;
|
|
2497
2748
|
}
|
|
2498
|
-
if (openParen
|
|
2749
|
+
if (openParen === -1) throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: must have a function name followed by parentheses, got "${signature}"`);
|
|
2499
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");
|
|
2500
2752
|
const firstChar = signature.charCodeAt(0);
|
|
2501
2753
|
if (!isAlpha(firstChar) && firstChar !== 95) throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must start with a letter or underscore");
|
|
2502
2754
|
for (let i = 1; i < openParen; i++) {
|
|
2503
|
-
const
|
|
2504
|
-
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");
|
|
2505
2757
|
}
|
|
2506
|
-
const encoded = new Uint8Array(signature.length);
|
|
2507
|
-
for (let i = 0; i < signature.length; i++) encoded[i] = signature.charCodeAt(i);
|
|
2508
2758
|
return {
|
|
2509
|
-
selector: bytesToHex(keccak_256(
|
|
2759
|
+
selector: bytesToHex(keccak_256(utf8ToBytes(signature)).subarray(0, 4)),
|
|
2510
2760
|
types: signature.slice(openParen + 1, signature.length - 1)
|
|
2511
2761
|
};
|
|
2512
2762
|
}
|
|
@@ -2515,8 +2765,7 @@ const SignatureParser = { parse };
|
|
|
2515
2765
|
//#region src/policy-builder.ts
|
|
2516
2766
|
function validateContextPath(steps) {
|
|
2517
2767
|
if (steps.length !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step");
|
|
2518
|
-
|
|
2519
|
-
if (step > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ID 0x${step.toString(16).padStart(4, "0")}`);
|
|
2768
|
+
return lookupContextProperty(steps[0]).typeCode;
|
|
2520
2769
|
}
|
|
2521
2770
|
function validateCalldataPath(steps, desc) {
|
|
2522
2771
|
const argIndex = steps[0];
|
|
@@ -2545,6 +2794,33 @@ function validateCalldataPath(steps, desc) {
|
|
|
2545
2794
|
offset = Descriptor.arrayElementOffset(offset);
|
|
2546
2795
|
} else throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type");
|
|
2547
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
|
+
};
|
|
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);
|
|
2548
2824
|
}
|
|
2549
2825
|
var PolicyBuilder = class PolicyBuilder {
|
|
2550
2826
|
draft;
|
|
@@ -2559,7 +2835,7 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2559
2835
|
selector: parsed.selector,
|
|
2560
2836
|
descriptor,
|
|
2561
2837
|
groups: [[]],
|
|
2562
|
-
|
|
2838
|
+
groupPathKeys: /* @__PURE__ */ new Set()
|
|
2563
2839
|
});
|
|
2564
2840
|
}
|
|
2565
2841
|
static createRaw(typesCsv) {
|
|
@@ -2568,32 +2844,34 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2568
2844
|
selector: "0x00000000",
|
|
2569
2845
|
descriptor: DescriptorCoder.fromTypes(typesCsv),
|
|
2570
2846
|
groups: [[]],
|
|
2571
|
-
|
|
2847
|
+
groupPathKeys: /* @__PURE__ */ new Set()
|
|
2572
2848
|
});
|
|
2573
2849
|
}
|
|
2574
2850
|
add(constraint) {
|
|
2575
|
-
const
|
|
2851
|
+
const added = {
|
|
2576
2852
|
scope: constraint.scope,
|
|
2577
2853
|
path: constraint.path,
|
|
2578
|
-
operators: [...constraint.operators]
|
|
2854
|
+
operators: [...constraint.operators],
|
|
2855
|
+
..."hint" in constraint && constraint.hint !== void 0 && { hint: constraint.hint }
|
|
2579
2856
|
};
|
|
2580
|
-
if (
|
|
2581
|
-
const steps = parsePathSteps(
|
|
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);
|
|
2582
2859
|
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
else
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
this.draft.
|
|
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);
|
|
2591
2869
|
return this;
|
|
2592
2870
|
}
|
|
2593
2871
|
or() {
|
|
2594
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");
|
|
2595
2873
|
this.draft.groups.push([]);
|
|
2596
|
-
this.draft.
|
|
2874
|
+
this.draft.groupPathKeys.clear();
|
|
2597
2875
|
return this;
|
|
2598
2876
|
}
|
|
2599
2877
|
build() {
|
|
@@ -2625,167 +2903,16 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2625
2903
|
}
|
|
2626
2904
|
};
|
|
2627
2905
|
//#endregion
|
|
2628
|
-
//#region src/constraint.ts
|
|
2629
|
-
function addressBody(value) {
|
|
2630
|
-
return toAddress(value).slice(2);
|
|
2631
|
-
}
|
|
2632
|
-
function encodeWord(value) {
|
|
2633
|
-
const word = new Uint8Array(32);
|
|
2634
|
-
if (typeof value === "boolean") {
|
|
2635
|
-
word[31] = value ? 1 : 0;
|
|
2636
|
-
return word;
|
|
2637
|
-
}
|
|
2638
|
-
if (typeof value === "string") {
|
|
2639
|
-
word.set(hexToBytes(addressBody(value)), 12);
|
|
2640
|
-
return word;
|
|
2641
|
-
}
|
|
2642
|
-
let bigValue = typeof value === "number" ? BigInt(value) : value;
|
|
2643
|
-
for (let i = 31; i >= 0; i--) {
|
|
2644
|
-
word[i] = Number(bigValue & 255n);
|
|
2645
|
-
bigValue >>= 8n;
|
|
2646
|
-
}
|
|
2647
|
-
return word;
|
|
2648
|
-
}
|
|
2649
|
-
function singleOp(opCode, value) {
|
|
2650
|
-
const buffer = new Uint8Array(33);
|
|
2651
|
-
buffer[0] = opCode;
|
|
2652
|
-
buffer.set(encodeWord(value), 1);
|
|
2653
|
-
return bytesToHex(buffer);
|
|
2654
|
-
}
|
|
2655
|
-
function rangeOp(opCode, min, max) {
|
|
2656
|
-
if (min > max) throw new CallciumError("INVALID_RANGE", `Range min (${min}) must not exceed max (${max})`);
|
|
2657
|
-
const buffer = new Uint8Array(65);
|
|
2658
|
-
buffer[0] = opCode;
|
|
2659
|
-
buffer.set(encodeWord(min), 1);
|
|
2660
|
-
buffer.set(encodeWord(max), 33);
|
|
2661
|
-
return bytesToHex(buffer);
|
|
2662
|
-
}
|
|
2663
|
-
function checkContextPropertyId(contextPropertyId) {
|
|
2664
|
-
if (!Number.isInteger(contextPropertyId) || contextPropertyId < 0 || contextPropertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ID ${contextPropertyId}`);
|
|
2665
|
-
return contextPropertyId;
|
|
2666
|
-
}
|
|
2667
|
-
function setOp(opCode, values) {
|
|
2668
|
-
const bigs = values.map((v) => {
|
|
2669
|
-
if (typeof v === "bigint") return v;
|
|
2670
|
-
if (typeof v === "number") return BigInt(v);
|
|
2671
|
-
if (typeof v === "boolean") return v ? 1n : 0n;
|
|
2672
|
-
return BigInt("0x" + addressBody(v));
|
|
2673
|
-
});
|
|
2674
|
-
const deduped = [...new Set(bigs)].toSorted((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
2675
|
-
if (deduped.length === 0) throw new CallciumError("EMPTY_SET", "Set must contain at least one value");
|
|
2676
|
-
if (deduped.length > PolicyFormat.MAX_SET_MEMBERS) throw new CallciumError("SET_TOO_LARGE", `Set must contain at most ${PolicyFormat.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
|
-
var ConstraintBuilder = class {
|
|
2683
|
-
scope;
|
|
2684
|
-
path;
|
|
2685
|
-
operators;
|
|
2686
|
-
constructor(scope, path) {
|
|
2687
|
-
this.scope = scope;
|
|
2688
|
-
this.path = path;
|
|
2689
|
-
this.operators = [];
|
|
2690
|
-
}
|
|
2691
|
-
push(opHex) {
|
|
2692
|
-
this.operators.push(opHex);
|
|
2693
|
-
return this;
|
|
2694
|
-
}
|
|
2695
|
-
eq(value) {
|
|
2696
|
-
return this.push(singleOp(Op.EQ, value));
|
|
2697
|
-
}
|
|
2698
|
-
neq(value) {
|
|
2699
|
-
return this.push(singleOp(Op.EQ | Op.NOT, value));
|
|
2700
|
-
}
|
|
2701
|
-
eqCtx(contextPropertyId) {
|
|
2702
|
-
return this.push(singleOp(Op.EQ_CTX, checkContextPropertyId(contextPropertyId)));
|
|
2703
|
-
}
|
|
2704
|
-
neqCtx(contextPropertyId) {
|
|
2705
|
-
return this.push(singleOp(Op.EQ_CTX | Op.NOT, checkContextPropertyId(contextPropertyId)));
|
|
2706
|
-
}
|
|
2707
|
-
gt(bound) {
|
|
2708
|
-
return this.push(singleOp(Op.GT, bound));
|
|
2709
|
-
}
|
|
2710
|
-
lt(bound) {
|
|
2711
|
-
return this.push(singleOp(Op.LT, bound));
|
|
2712
|
-
}
|
|
2713
|
-
gte(bound) {
|
|
2714
|
-
return this.push(singleOp(Op.GTE, bound));
|
|
2715
|
-
}
|
|
2716
|
-
lte(bound) {
|
|
2717
|
-
return this.push(singleOp(Op.LTE, bound));
|
|
2718
|
-
}
|
|
2719
|
-
between(min, max) {
|
|
2720
|
-
return this.push(rangeOp(Op.BETWEEN, BigInt(min), BigInt(max)));
|
|
2721
|
-
}
|
|
2722
|
-
isIn(values) {
|
|
2723
|
-
return this.push(setOp(Op.IN, values));
|
|
2724
|
-
}
|
|
2725
|
-
notIn(values) {
|
|
2726
|
-
return this.push(setOp(Op.IN | Op.NOT, values));
|
|
2727
|
-
}
|
|
2728
|
-
bitmaskAll(mask) {
|
|
2729
|
-
return this.push(singleOp(Op.BITMASK_ALL, mask));
|
|
2730
|
-
}
|
|
2731
|
-
bitmaskAny(mask) {
|
|
2732
|
-
return this.push(singleOp(Op.BITMASK_ANY, mask));
|
|
2733
|
-
}
|
|
2734
|
-
bitmaskNone(mask) {
|
|
2735
|
-
return this.push(singleOp(Op.BITMASK_NONE, mask));
|
|
2736
|
-
}
|
|
2737
|
-
lengthEq(n) {
|
|
2738
|
-
return this.push(singleOp(Op.LENGTH_EQ, n));
|
|
2739
|
-
}
|
|
2740
|
-
lengthGt(n) {
|
|
2741
|
-
return this.push(singleOp(Op.LENGTH_GT, n));
|
|
2742
|
-
}
|
|
2743
|
-
lengthLt(n) {
|
|
2744
|
-
return this.push(singleOp(Op.LENGTH_LT, n));
|
|
2745
|
-
}
|
|
2746
|
-
lengthGte(n) {
|
|
2747
|
-
return this.push(singleOp(Op.LENGTH_GTE, n));
|
|
2748
|
-
}
|
|
2749
|
-
lengthLte(n) {
|
|
2750
|
-
return this.push(singleOp(Op.LENGTH_LTE, n));
|
|
2751
|
-
}
|
|
2752
|
-
lengthBetween(min, max) {
|
|
2753
|
-
return this.push(rangeOp(Op.LENGTH_BETWEEN, BigInt(min), BigInt(max)));
|
|
2754
|
-
}
|
|
2755
|
-
};
|
|
2756
|
-
function arg(...steps) {
|
|
2757
|
-
return new ConstraintBuilder(Scope.CALLDATA, encodePath(steps));
|
|
2758
|
-
}
|
|
2759
|
-
function msgSender() {
|
|
2760
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_SENDER]));
|
|
2761
|
-
}
|
|
2762
|
-
function msgValue() {
|
|
2763
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_VALUE]));
|
|
2764
|
-
}
|
|
2765
|
-
function blockTimestamp() {
|
|
2766
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_TIMESTAMP]));
|
|
2767
|
-
}
|
|
2768
|
-
function blockNumber() {
|
|
2769
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_NUMBER]));
|
|
2770
|
-
}
|
|
2771
|
-
function chainId() {
|
|
2772
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.CHAIN_ID]));
|
|
2773
|
-
}
|
|
2774
|
-
function txOrigin() {
|
|
2775
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.TX_ORIGIN]));
|
|
2776
|
-
}
|
|
2777
|
-
function baseFee() {
|
|
2778
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BASE_FEE]));
|
|
2779
|
-
}
|
|
2780
|
-
function gasPrice() {
|
|
2781
|
-
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.GAS_PRICE]));
|
|
2782
|
-
}
|
|
2783
|
-
//#endregion
|
|
2784
2906
|
//#region src/calldata-reader.ts
|
|
2785
2907
|
function loadWord(callData, offset) {
|
|
2786
2908
|
if (offset < 0 || offset + 32 > callData.length) return { code: "CALLDATA_OUT_OF_BOUNDS" };
|
|
2787
2909
|
return callData.subarray(offset, offset + 32);
|
|
2788
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
|
+
}
|
|
2789
2916
|
function readPointer(callData, head) {
|
|
2790
2917
|
const word = loadWord(callData, head);
|
|
2791
2918
|
if (word instanceof Uint8Array) {
|
|
@@ -2802,8 +2929,10 @@ const abortViolationCodes = new Set([
|
|
|
2802
2929
|
"NON_CANONICAL_VALUE",
|
|
2803
2930
|
"QUANTIFIER_LIMIT_EXCEEDED"
|
|
2804
2931
|
]);
|
|
2805
|
-
function
|
|
2806
|
-
return BigInt(toAddress(
|
|
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;
|
|
2807
2936
|
}
|
|
2808
2937
|
function check(policy, callData, context) {
|
|
2809
2938
|
const { policy: decoded, data: policyBytes } = decodePolicy(policy);
|
|
@@ -2870,7 +2999,11 @@ function resolveContextOperand(operandData, context) {
|
|
|
2870
2999
|
const propertyInfo = lookupContextProperty(Number(toBigInt(operandData, 0)));
|
|
2871
3000
|
const value = context?.[propertyInfo.contextKey];
|
|
2872
3001
|
if (value === void 0) return { ctxTypeCode: propertyInfo.typeCode };
|
|
2873
|
-
return
|
|
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);
|
|
2874
3007
|
}
|
|
2875
3008
|
function readTargetBlock(hint, targetOffset) {
|
|
2876
3009
|
return {
|
|
@@ -2897,8 +3030,8 @@ function chainResolve(hint, callData, base, hopsOffset, hopCount) {
|
|
|
2897
3030
|
const meta = readU16(hint, hop + PolicyFormat.HINT_HOP_META_OFFSET);
|
|
2898
3031
|
let elems = base;
|
|
2899
3032
|
if ((meta & PolicyFormat.HINT_META_DYNAMIC_ARRAY) !== 0) {
|
|
2900
|
-
const length =
|
|
2901
|
-
if (typeof length !== "
|
|
3033
|
+
const length = readLength(callData, elems);
|
|
3034
|
+
if (typeof length !== "bigint") return length;
|
|
2902
3035
|
if (index >= length) return { code: "ARRAY_INDEX_OUT_OF_BOUNDS" };
|
|
2903
3036
|
elems += 32;
|
|
2904
3037
|
}
|
|
@@ -2911,21 +3044,19 @@ function chainResolve(hint, callData, base, hopsOffset, hopCount) {
|
|
|
2911
3044
|
}
|
|
2912
3045
|
return base;
|
|
2913
3046
|
}
|
|
2914
|
-
function evalTarget(callData, target, block, opCode, operandData,
|
|
3047
|
+
function evalTarget(callData, target, block, opCode, operandData, ctxOperand) {
|
|
2915
3048
|
const typeCode = block.typeCode;
|
|
2916
3049
|
if (isLengthValidType(typeCode)) {
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
if (typeof length !== "number") return { error: length.code };
|
|
3050
|
+
const length = readLength(callData, target);
|
|
3051
|
+
if (typeof length !== "bigint") return { error: length.code };
|
|
2920
3052
|
const stride = typeCode === TypeCode.DYNAMIC_ARRAY ? (block.targetMeta & PolicyFormat.HINT_META_STRIDE_MASK) * 32 : 1;
|
|
2921
3053
|
const room = callData.length - (target + 32);
|
|
2922
|
-
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" };
|
|
2923
3055
|
return {
|
|
2924
3056
|
passed: applyOperator(opCode, 0n, length, operandData, typeCode),
|
|
2925
|
-
value:
|
|
3057
|
+
value: length
|
|
2926
3058
|
};
|
|
2927
3059
|
}
|
|
2928
|
-
if (classifyTypeCode(typeCode).typeClass !== "elementary") throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Operator target does not carry a scalar word");
|
|
2929
3060
|
const word = loadWord(callData, target);
|
|
2930
3061
|
if (!(word instanceof Uint8Array)) return { error: word.code };
|
|
2931
3062
|
const value = toBigInt(word, 0);
|
|
@@ -2933,57 +3064,50 @@ function evalTarget(callData, target, block, opCode, operandData, context) {
|
|
|
2933
3064
|
error: "NON_CANONICAL_VALUE",
|
|
2934
3065
|
value
|
|
2935
3066
|
};
|
|
2936
|
-
if (
|
|
2937
|
-
|
|
2938
|
-
if (typeof operand !== "bigint") return {
|
|
3067
|
+
if (ctxOperand !== void 0) {
|
|
3068
|
+
if (typeof ctxOperand !== "bigint") return {
|
|
2939
3069
|
error: "MISSING_CONTEXT",
|
|
2940
|
-
ctxTypeCode:
|
|
3070
|
+
ctxTypeCode: ctxOperand.ctxTypeCode
|
|
2941
3071
|
};
|
|
2942
3072
|
return {
|
|
2943
|
-
passed: value ===
|
|
3073
|
+
passed: value === ctxOperand !== ((opCode & Op.NOT) !== 0),
|
|
2944
3074
|
value,
|
|
2945
|
-
ctxOperand
|
|
3075
|
+
ctxOperand
|
|
2946
3076
|
};
|
|
2947
3077
|
}
|
|
2948
3078
|
return {
|
|
2949
|
-
passed: applyOperator(opCode, value,
|
|
3079
|
+
passed: applyOperator(opCode, value, 32n, operandData, typeCode),
|
|
2950
3080
|
value
|
|
2951
3081
|
};
|
|
2952
3082
|
}
|
|
2953
|
-
function
|
|
3083
|
+
function ruleFields(frame) {
|
|
2954
3084
|
return {
|
|
2955
3085
|
group: frame.group,
|
|
2956
3086
|
rule: frame.rule,
|
|
2957
|
-
code,
|
|
2958
3087
|
scope: Scope.CALLDATA,
|
|
2959
3088
|
path: frame.path,
|
|
2960
3089
|
opCode: frame.opCode,
|
|
2961
3090
|
operandData: bytesToHex(frame.operandData),
|
|
2962
|
-
typeCode: frame.typeCode
|
|
3091
|
+
typeCode: frame.typeCode
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
function navigationViolation(frame, code, elementIndex) {
|
|
3095
|
+
return {
|
|
3096
|
+
code,
|
|
3097
|
+
...ruleFields(frame),
|
|
2963
3098
|
...elementIndex !== void 0 && { elementIndex }
|
|
2964
3099
|
};
|
|
2965
3100
|
}
|
|
2966
3101
|
function targetViolation(frame, result, elementIndex) {
|
|
2967
3102
|
if ("error" in result) {
|
|
2968
3103
|
if (result.error === "MISSING_CONTEXT") return {
|
|
2969
|
-
group: frame.group,
|
|
2970
|
-
rule: frame.rule,
|
|
2971
3104
|
code: result.error,
|
|
2972
|
-
|
|
2973
|
-
path: frame.path,
|
|
2974
|
-
opCode: frame.opCode,
|
|
2975
|
-
operandData: bytesToHex(frame.operandData),
|
|
3105
|
+
...ruleFields(frame),
|
|
2976
3106
|
typeCode: result.ctxTypeCode
|
|
2977
3107
|
};
|
|
2978
3108
|
if (result.error === "NON_CANONICAL_VALUE") return {
|
|
2979
|
-
group: frame.group,
|
|
2980
|
-
rule: frame.rule,
|
|
2981
3109
|
code: result.error,
|
|
2982
|
-
|
|
2983
|
-
path: frame.path,
|
|
2984
|
-
opCode: frame.opCode,
|
|
2985
|
-
operandData: bytesToHex(frame.operandData),
|
|
2986
|
-
typeCode: frame.typeCode,
|
|
3110
|
+
...ruleFields(frame),
|
|
2987
3111
|
resolvedValue: bigintToHex(result.value),
|
|
2988
3112
|
...elementIndex !== void 0 && { elementIndex }
|
|
2989
3113
|
};
|
|
@@ -2991,14 +3115,8 @@ function targetViolation(frame, result, elementIndex) {
|
|
|
2991
3115
|
}
|
|
2992
3116
|
if (result.passed) return null;
|
|
2993
3117
|
return {
|
|
2994
|
-
group: frame.group,
|
|
2995
|
-
rule: frame.rule,
|
|
2996
3118
|
code: "VALUE_MISMATCH",
|
|
2997
|
-
|
|
2998
|
-
path: frame.path,
|
|
2999
|
-
opCode: frame.opCode,
|
|
3000
|
-
operandData: bytesToHex(frame.operandData),
|
|
3001
|
-
typeCode: frame.typeCode,
|
|
3119
|
+
...ruleFields(frame),
|
|
3002
3120
|
resolvedValue: bigintToHex(result.value),
|
|
3003
3121
|
...result.ctxOperand !== void 0 && { resolvedOperand: bigintToHex(result.ctxOperand) },
|
|
3004
3122
|
...elementIndex !== void 0 && { elementIndex }
|
|
@@ -3021,7 +3139,7 @@ function evaluateCalldataRule(hint, callDataBytes, baseOffset, opCode, operandDa
|
|
|
3021
3139
|
};
|
|
3022
3140
|
const base = chainResolve(hint, callDataBytes, baseOffset, hopsOffset, hopCount);
|
|
3023
3141
|
if (typeof base !== "number") return navigationViolation(frame, base.code);
|
|
3024
|
-
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)));
|
|
3025
3143
|
}
|
|
3026
3144
|
function evaluateQuantified(hint, callDataBytes, baseOffset, kind, hopsOffset, hopCount, opCode, operandData, groupIndex, ruleIndex, pathHex, context) {
|
|
3027
3145
|
const frameOffset = hopsOffset + hopCount * PolicyFormat.HINT_HOP_SIZE;
|
|
@@ -3040,23 +3158,23 @@ function evaluateQuantified(hint, callDataBytes, baseOffset, kind, hopsOffset, h
|
|
|
3040
3158
|
const chained = chainResolve(hint, callDataBytes, baseOffset, hopsOffset, hopCount);
|
|
3041
3159
|
if (typeof chained !== "number") return navigationViolation(frame, chained.code);
|
|
3042
3160
|
let elems = chained + readU32(hint, frameOffset);
|
|
3043
|
-
let count = readU16(hint, frameOffset + PolicyFormat.HINT_FRAME_COUNT_OFFSET);
|
|
3044
|
-
if (count ===
|
|
3045
|
-
const length =
|
|
3046
|
-
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);
|
|
3047
3165
|
count = length;
|
|
3048
3166
|
elems += 32;
|
|
3049
3167
|
}
|
|
3050
|
-
if (count > PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH) return {
|
|
3168
|
+
if (count > BigInt(PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH)) return {
|
|
3051
3169
|
group: groupIndex,
|
|
3052
3170
|
rule: ruleIndex,
|
|
3053
3171
|
code: "QUANTIFIER_LIMIT_EXCEEDED",
|
|
3054
3172
|
scope: Scope.CALLDATA,
|
|
3055
3173
|
path: pathHex,
|
|
3056
|
-
resolvedValue: bigintToHex(
|
|
3174
|
+
resolvedValue: bigintToHex(count)
|
|
3057
3175
|
};
|
|
3058
3176
|
const isUniversal = kind === PolicyFormat.HINT_KIND_ALL;
|
|
3059
|
-
if (count ===
|
|
3177
|
+
if (count === 0n) {
|
|
3060
3178
|
if (isUniversal) return null;
|
|
3061
3179
|
return {
|
|
3062
3180
|
group: groupIndex,
|
|
@@ -3069,7 +3187,7 @@ function evaluateQuantified(hint, callDataBytes, baseOffset, kind, hopsOffset, h
|
|
|
3069
3187
|
const meta = readU16(hint, frameOffset + PolicyFormat.HINT_FRAME_META_OFFSET);
|
|
3070
3188
|
const elemStride = (meta & PolicyFormat.HINT_META_STRIDE_MASK) * 32;
|
|
3071
3189
|
const elemIsDynamic = (meta & PolicyFormat.HINT_META_ELEM_DYNAMIC) !== 0;
|
|
3072
|
-
|
|
3190
|
+
const reference = referenceOperand(opCode, operandData, context);
|
|
3073
3191
|
for (let elemIndex = 0; elemIndex < count; elemIndex++) {
|
|
3074
3192
|
const slot = elems + elemIndex * elemStride;
|
|
3075
3193
|
let elem = slot;
|
|
@@ -3084,65 +3202,51 @@ function evaluateQuantified(hint, callDataBytes, baseOffset, kind, hopsOffset, h
|
|
|
3084
3202
|
if (typeof chainedElem !== "number") return navigationViolation(frame, chainedElem.code, elemIndex);
|
|
3085
3203
|
base = chainedElem;
|
|
3086
3204
|
}
|
|
3087
|
-
const applied = evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData,
|
|
3205
|
+
const applied = evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData, reference);
|
|
3088
3206
|
if ("error" in applied) return targetViolation(frame, applied, elemIndex);
|
|
3089
3207
|
if (applied.passed) {
|
|
3090
3208
|
if (!isUniversal) return null;
|
|
3091
3209
|
} else if (isUniversal) return targetViolation(frame, applied, elemIndex);
|
|
3092
|
-
ctxOperand = applied.ctxOperand;
|
|
3093
3210
|
}
|
|
3094
3211
|
if (isUniversal) return null;
|
|
3095
3212
|
return {
|
|
3096
|
-
group: groupIndex,
|
|
3097
|
-
rule: ruleIndex,
|
|
3098
3213
|
code: "VALUE_MISMATCH",
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
opCode,
|
|
3102
|
-
operandData: bytesToHex(operandData),
|
|
3103
|
-
typeCode: block.typeCode,
|
|
3104
|
-
...ctxOperand !== void 0 && { resolvedOperand: bigintToHex(ctxOperand) }
|
|
3214
|
+
...ruleFields(frame),
|
|
3215
|
+
...typeof reference === "bigint" && { resolvedOperand: bigintToHex(reference) }
|
|
3105
3216
|
};
|
|
3106
3217
|
}
|
|
3107
3218
|
function evaluateContextRule(pathBytes, opCode, operandData, groupIndex, ruleIndex, pathHex, context) {
|
|
3108
|
-
const
|
|
3109
|
-
if (propertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ID 0x${propertyId.toString(16).padStart(4, "0")}`);
|
|
3110
|
-
const propertyInfo = lookupContextProperty(propertyId);
|
|
3219
|
+
const propertyInfo = lookupContextProperty(readU16(pathBytes, 0));
|
|
3111
3220
|
const contextValue = context?.[propertyInfo.contextKey];
|
|
3112
|
-
|
|
3221
|
+
const site = {
|
|
3113
3222
|
group: groupIndex,
|
|
3114
3223
|
rule: ruleIndex,
|
|
3115
|
-
code: "MISSING_CONTEXT",
|
|
3116
3224
|
scope: Scope.CONTEXT,
|
|
3117
|
-
path: pathHex
|
|
3225
|
+
path: pathHex
|
|
3226
|
+
};
|
|
3227
|
+
if (contextValue === void 0) return {
|
|
3228
|
+
code: "MISSING_CONTEXT",
|
|
3229
|
+
...site,
|
|
3118
3230
|
typeCode: propertyInfo.typeCode
|
|
3119
3231
|
};
|
|
3120
|
-
|
|
3121
|
-
if (typeof contextValue === "string") value = addressToBigInt(contextValue);
|
|
3122
|
-
else value = contextValue;
|
|
3232
|
+
const value = contextValueToWord(contextValue);
|
|
3123
3233
|
let result;
|
|
3124
3234
|
let ctxOperand;
|
|
3125
3235
|
if ((opCode & ~Op.NOT) === Op.EQ_CTX) {
|
|
3126
3236
|
const operand = resolveContextOperand(operandData, context);
|
|
3127
3237
|
if (typeof operand !== "bigint") return {
|
|
3128
|
-
group: groupIndex,
|
|
3129
|
-
rule: ruleIndex,
|
|
3130
3238
|
code: "MISSING_CONTEXT",
|
|
3131
|
-
|
|
3132
|
-
path: pathHex,
|
|
3239
|
+
...site,
|
|
3133
3240
|
opCode,
|
|
3134
3241
|
operandData: bytesToHex(operandData),
|
|
3135
3242
|
typeCode: operand.ctxTypeCode
|
|
3136
3243
|
};
|
|
3137
3244
|
result = value === operand !== ((opCode & Op.NOT) !== 0);
|
|
3138
3245
|
ctxOperand = operand;
|
|
3139
|
-
} else result = applyOperator(opCode, value,
|
|
3246
|
+
} else result = applyOperator(opCode, value, 32n, operandData, propertyInfo.typeCode);
|
|
3140
3247
|
if (!result) return {
|
|
3141
|
-
group: groupIndex,
|
|
3142
|
-
rule: ruleIndex,
|
|
3143
3248
|
code: "VALUE_MISMATCH",
|
|
3144
|
-
|
|
3145
|
-
path: pathHex,
|
|
3249
|
+
...site,
|
|
3146
3250
|
opCode,
|
|
3147
3251
|
operandData: bytesToHex(operandData),
|
|
3148
3252
|
typeCode: propertyInfo.typeCode,
|
|
@@ -3156,6 +3260,6 @@ const PolicyEnforcer = {
|
|
|
3156
3260
|
enforce
|
|
3157
3261
|
};
|
|
3158
3262
|
//#endregion
|
|
3159
|
-
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, gasPrice, hexToBytes, isLengthOp, isOpAllowed, isQuantifier, lookupContextProperty, lookupOp, lookupQuantifier, lookupScope, lookupTypeCode, msgSender, msgValue, parsePathSteps, toAddress, txOrigin };
|
|
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 };
|
|
3160
3264
|
|
|
3161
3265
|
//# sourceMappingURL=index.js.map
|