@callcium/sdk 0.8.0 → 0.10.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 +74 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +411 -330
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bytes.ts +9 -9
- package/src/constants.ts +37 -175
- package/src/constraint.ts +24 -31
- package/src/descriptor-coder.ts +18 -20
- package/src/descriptor.ts +12 -11
- package/src/errors.ts +0 -3
- package/src/index.ts +8 -17
- package/src/operators.ts +114 -9
- package/src/path.ts +71 -0
- package/src/policy-builder.ts +17 -16
- package/src/policy-coder.ts +22 -41
- package/src/policy-enforcer.ts +90 -35
- package/src/policy-validator.ts +58 -25
- package/src/type-desc.ts +10 -10
- package/src/types.ts +79 -5
- package/src/validation-issue.ts +18 -4
package/src/policy-enforcer.ts
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
import { hexToBytes, bytesToHex, readU16, readU32, bigintToHex } from "./bytes";
|
|
1
|
+
import { hexToBytes, bytesToHex, readU16, readU32, bigintToHex, toAddress } from "./bytes";
|
|
2
2
|
import { loadWord, readPointer } from "./calldata-reader";
|
|
3
|
-
import {
|
|
4
|
-
PolicyFormat as PF,
|
|
5
|
-
Scope,
|
|
6
|
-
MAX_CONTEXT_PROPERTY_ID,
|
|
7
|
-
Limits,
|
|
8
|
-
TypeCode,
|
|
9
|
-
classifyTypeCode,
|
|
10
|
-
lookupContextProperty,
|
|
11
|
-
} from "./constants";
|
|
3
|
+
import { PolicyFormat as PF, Scope, MAX_CONTEXT_PROPERTY_ID, Op, TypeCode, lookupContextProperty } from "./constants";
|
|
12
4
|
import { CallciumError, PolicyViolationError } from "./errors";
|
|
13
|
-
import { applyOperator, toBigInt, isLengthOp, isLengthValidType, canonicalize } from "./operators";
|
|
5
|
+
import { applyOperator, toBigInt, isLengthOp, isLengthValidType, canonicalize, classifyTypeCode } from "./operators";
|
|
14
6
|
import { decodePolicy } from "./policy-coder";
|
|
15
7
|
|
|
16
8
|
import type { ReadResult } from "./calldata-reader";
|
|
@@ -29,20 +21,17 @@ import type {
|
|
|
29
21
|
// Helpers
|
|
30
22
|
///////////////////////////////////////////////////////////////////////////
|
|
31
23
|
|
|
32
|
-
/** Violations that abort evaluation instead of failing only their group
|
|
33
|
-
const
|
|
24
|
+
/** Violations that abort evaluation instead of failing only their group. */
|
|
25
|
+
const abortViolationCodes: ReadonlySet<ViolationCode> = new Set<ViolationCode>([
|
|
34
26
|
"CALLDATA_OUT_OF_BOUNDS",
|
|
35
27
|
"ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
36
28
|
"NON_CANONICAL_VALUE",
|
|
37
29
|
"QUANTIFIER_LIMIT_EXCEEDED",
|
|
38
30
|
]);
|
|
39
31
|
|
|
40
|
-
/**
|
|
32
|
+
/** Validate a hex address and read it as a 256-bit bigint. */
|
|
41
33
|
function addressToBigInt(hex: string): bigint {
|
|
42
|
-
|
|
43
|
-
const padded = new Uint8Array(32);
|
|
44
|
-
padded.set(bytes, 32 - bytes.length);
|
|
45
|
-
return toBigInt(padded, 0);
|
|
34
|
+
return BigInt(toAddress(hex));
|
|
46
35
|
}
|
|
47
36
|
|
|
48
37
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -99,8 +88,8 @@ function check(policy: Hex, callData: Hex, context?: Context): EnforceResult {
|
|
|
99
88
|
const violation = evaluateRule(rule, policyBytes, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
|
|
100
89
|
if (violation !== null) {
|
|
101
90
|
allViolations.push(violation);
|
|
102
|
-
if (
|
|
103
|
-
// Abort violations reject the policy outright; later groups are not consulted
|
|
91
|
+
if (abortViolationCodes.has(violation.code)) {
|
|
92
|
+
// Abort violations reject the policy outright; later groups are not consulted.
|
|
104
93
|
return { ok: false, violations: allViolations };
|
|
105
94
|
}
|
|
106
95
|
groupFailed = true;
|
|
@@ -172,9 +161,21 @@ function evaluateRule(
|
|
|
172
161
|
groupIndex,
|
|
173
162
|
ruleIndex,
|
|
174
163
|
rule.path.value,
|
|
164
|
+
context,
|
|
175
165
|
);
|
|
176
166
|
}
|
|
177
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Resolve an EQ_CTX operand word to the referenced context property's value as a raw word.
|
|
170
|
+
* Returns the missing property's type code when the context does not supply it.
|
|
171
|
+
*/
|
|
172
|
+
function resolveContextOperand(operandData: Uint8Array, context?: Context): bigint | { ctxTypeCode: number } {
|
|
173
|
+
const propertyInfo = lookupContextProperty(Number(toBigInt(operandData, 0)));
|
|
174
|
+
const value = context?.[propertyInfo.contextKey];
|
|
175
|
+
if (value === undefined) return { ctxTypeCode: propertyInfo.typeCode };
|
|
176
|
+
return typeof value === "string" ? addressToBigInt(value) : value;
|
|
177
|
+
}
|
|
178
|
+
|
|
178
179
|
///////////////////////////////////////////////////////////////////////////
|
|
179
180
|
// Hint chain resolution
|
|
180
181
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -247,9 +248,10 @@ function chainResolve(
|
|
|
247
248
|
///////////////////////////////////////////////////////////////////////////
|
|
248
249
|
|
|
249
250
|
type TargetResult =
|
|
250
|
-
| { passed: boolean; value: bigint }
|
|
251
|
+
| { passed: boolean; value: bigint; ctxOperand?: bigint }
|
|
251
252
|
| { error: NavigationViolationCode }
|
|
252
|
-
| { error: "NON_CANONICAL_VALUE"; value: bigint }
|
|
253
|
+
| { error: "NON_CANONICAL_VALUE"; value: bigint }
|
|
254
|
+
| { error: "MISSING_CONTEXT"; ctxTypeCode: number };
|
|
253
255
|
|
|
254
256
|
/**
|
|
255
257
|
* Load the value at a resolved target offset and apply the operator.
|
|
@@ -264,13 +266,14 @@ function evalTarget(
|
|
|
264
266
|
block: TargetBlock,
|
|
265
267
|
opCode: number,
|
|
266
268
|
operandData: Uint8Array,
|
|
269
|
+
context?: Context,
|
|
267
270
|
): TargetResult {
|
|
268
271
|
const typeCode = block.typeCode;
|
|
269
272
|
|
|
270
273
|
// A dynamic target's chain ends at its payload, so the word there is the declared length.
|
|
271
274
|
if (isLengthValidType(typeCode)) {
|
|
272
275
|
if (!isLengthOp(opCode)) {
|
|
273
|
-
throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Value operator on a target without a scalar word
|
|
276
|
+
throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Value operator on a target without a scalar word");
|
|
274
277
|
}
|
|
275
278
|
const length = readPointer(callData, target);
|
|
276
279
|
if (typeof length !== "number") return { error: length.code };
|
|
@@ -285,7 +288,7 @@ function evalTarget(
|
|
|
285
288
|
}
|
|
286
289
|
|
|
287
290
|
if (classifyTypeCode(typeCode).typeClass !== "elementary") {
|
|
288
|
-
throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Operator target does not carry a scalar word
|
|
291
|
+
throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Operator target does not carry a scalar word");
|
|
289
292
|
}
|
|
290
293
|
const word = loadWord(callData, target);
|
|
291
294
|
if (!(word instanceof Uint8Array)) return { error: word.code };
|
|
@@ -295,6 +298,13 @@ function evalTarget(
|
|
|
295
298
|
return { error: "NON_CANONICAL_VALUE", value };
|
|
296
299
|
}
|
|
297
300
|
|
|
301
|
+
if ((opCode & ~Op.NOT) === Op.EQ_CTX) {
|
|
302
|
+
const operand = resolveContextOperand(operandData, context);
|
|
303
|
+
if (typeof operand !== "bigint") return { error: "MISSING_CONTEXT", ctxTypeCode: operand.ctxTypeCode };
|
|
304
|
+
const passed = (value === operand) !== ((opCode & Op.NOT) !== 0);
|
|
305
|
+
return { passed, value, ctxOperand: operand };
|
|
306
|
+
}
|
|
307
|
+
|
|
298
308
|
const passed = applyOperator(opCode, value, 32, operandData, typeCode);
|
|
299
309
|
return { passed, value };
|
|
300
310
|
}
|
|
@@ -331,6 +341,18 @@ function navigationViolation(frame: RuleFrame, code: NavigationViolationCode, el
|
|
|
331
341
|
/** Convert a failed or erroring target result into its violation. Passing results map to null. */
|
|
332
342
|
function targetViolation(frame: RuleFrame, result: TargetResult, elementIndex?: number): Violation | null {
|
|
333
343
|
if ("error" in result) {
|
|
344
|
+
if (result.error === "MISSING_CONTEXT") {
|
|
345
|
+
return {
|
|
346
|
+
group: frame.group,
|
|
347
|
+
rule: frame.rule,
|
|
348
|
+
code: result.error,
|
|
349
|
+
scope: Scope.CALLDATA,
|
|
350
|
+
path: frame.path,
|
|
351
|
+
opCode: frame.opCode,
|
|
352
|
+
operandData: bytesToHex(frame.operandData),
|
|
353
|
+
typeCode: result.ctxTypeCode,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
334
356
|
if (result.error === "NON_CANONICAL_VALUE") {
|
|
335
357
|
return {
|
|
336
358
|
group: frame.group,
|
|
@@ -359,6 +381,7 @@ function targetViolation(frame: RuleFrame, result: TargetResult, elementIndex?:
|
|
|
359
381
|
operandData: bytesToHex(frame.operandData),
|
|
360
382
|
typeCode: frame.typeCode,
|
|
361
383
|
resolvedValue: bigintToHex(result.value),
|
|
384
|
+
...(result.ctxOperand !== undefined && { resolvedOperand: bigintToHex(result.ctxOperand) }),
|
|
362
385
|
...(elementIndex !== undefined && { elementIndex }),
|
|
363
386
|
};
|
|
364
387
|
}
|
|
@@ -373,6 +396,7 @@ function evaluateCalldataRule(
|
|
|
373
396
|
groupIndex: number,
|
|
374
397
|
ruleIndex: number,
|
|
375
398
|
pathHex: Hex,
|
|
399
|
+
context?: Context,
|
|
376
400
|
): Violation | null {
|
|
377
401
|
const header = hint[0]!;
|
|
378
402
|
const hopCount = header & PF.HINT_HOP_COUNT_MASK;
|
|
@@ -392,6 +416,7 @@ function evaluateCalldataRule(
|
|
|
392
416
|
groupIndex,
|
|
393
417
|
ruleIndex,
|
|
394
418
|
pathHex,
|
|
419
|
+
context,
|
|
395
420
|
);
|
|
396
421
|
}
|
|
397
422
|
|
|
@@ -408,7 +433,10 @@ function evaluateCalldataRule(
|
|
|
408
433
|
const base = chainResolve(hint, callDataBytes, baseOffset, hopsOffset, hopCount);
|
|
409
434
|
if (typeof base !== "number") return navigationViolation(frame, base.code);
|
|
410
435
|
|
|
411
|
-
return targetViolation(
|
|
436
|
+
return targetViolation(
|
|
437
|
+
frame,
|
|
438
|
+
evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData, context),
|
|
439
|
+
);
|
|
412
440
|
}
|
|
413
441
|
|
|
414
442
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -428,6 +456,7 @@ function evaluateQuantified(
|
|
|
428
456
|
groupIndex: number,
|
|
429
457
|
ruleIndex: number,
|
|
430
458
|
pathHex: Hex,
|
|
459
|
+
context?: Context,
|
|
431
460
|
): Violation | null {
|
|
432
461
|
const frameOffset = hopsOffset + hopCount * PF.HINT_HOP_SIZE;
|
|
433
462
|
const suffixHeaderOffset = frameOffset + PF.HINT_FRAME_PREFIX_SIZE;
|
|
@@ -459,7 +488,7 @@ function evaluateQuantified(
|
|
|
459
488
|
elems += 32;
|
|
460
489
|
}
|
|
461
490
|
|
|
462
|
-
if (count >
|
|
491
|
+
if (count > PF.MAX_QUANTIFIED_ARRAY_LENGTH) {
|
|
463
492
|
return {
|
|
464
493
|
group: groupIndex,
|
|
465
494
|
rule: ruleIndex,
|
|
@@ -486,13 +515,15 @@ function evaluateQuantified(
|
|
|
486
515
|
const elemStride = (meta & PF.HINT_META_STRIDE_MASK) * 32;
|
|
487
516
|
const elemIsDynamic = (meta & PF.HINT_META_ELEM_DYNAMIC) !== 0;
|
|
488
517
|
|
|
518
|
+
// A reference operand resolves identically for every element; any element's result carries it.
|
|
519
|
+
let ctxOperand: bigint | undefined;
|
|
489
520
|
for (let elemIndex = 0; elemIndex < count; elemIndex++) {
|
|
490
521
|
const slot = elems + elemIndex * elemStride;
|
|
491
522
|
let elem: number = slot;
|
|
492
523
|
if (elemIsDynamic) {
|
|
493
524
|
const followed = follow(callDataBytes, elems, slot);
|
|
494
525
|
// Navigation failures abort under every quantifier: calldata the enforcer cannot read is
|
|
495
|
-
// not an element that merely fails the operator
|
|
526
|
+
// not an element that merely fails the operator.
|
|
496
527
|
if (typeof followed !== "number") return navigationViolation(frame, followed.code, elemIndex);
|
|
497
528
|
elem = followed;
|
|
498
529
|
}
|
|
@@ -504,10 +535,11 @@ function evaluateQuantified(
|
|
|
504
535
|
base = chainedElem;
|
|
505
536
|
}
|
|
506
537
|
|
|
507
|
-
const applied = evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData);
|
|
538
|
+
const applied = evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData, context);
|
|
508
539
|
if ("error" in applied) {
|
|
509
|
-
//
|
|
510
|
-
//
|
|
540
|
+
// Error results end the rule whatever the quantifier: a later element cannot rescue
|
|
541
|
+
// calldata the enforcer cannot read (an abort effect), and a missing context
|
|
542
|
+
// property is missing for every element alike (group-local).
|
|
511
543
|
return targetViolation(frame, applied, elemIndex);
|
|
512
544
|
}
|
|
513
545
|
|
|
@@ -517,6 +549,7 @@ function evaluateQuantified(
|
|
|
517
549
|
} else if (isUniversal) {
|
|
518
550
|
return targetViolation(frame, applied, elemIndex);
|
|
519
551
|
}
|
|
552
|
+
ctxOperand = applied.ctxOperand;
|
|
520
553
|
}
|
|
521
554
|
|
|
522
555
|
if (isUniversal) return null;
|
|
@@ -530,6 +563,7 @@ function evaluateQuantified(
|
|
|
530
563
|
opCode,
|
|
531
564
|
operandData: bytesToHex(operandData),
|
|
532
565
|
typeCode: block.typeCode,
|
|
566
|
+
...(ctxOperand !== undefined && { resolvedOperand: bigintToHex(ctxOperand) }),
|
|
533
567
|
};
|
|
534
568
|
}
|
|
535
569
|
|
|
@@ -556,8 +590,8 @@ function evaluateContextRule(
|
|
|
556
590
|
);
|
|
557
591
|
}
|
|
558
592
|
|
|
559
|
-
const
|
|
560
|
-
const contextValue = context?.[
|
|
593
|
+
const propertyInfo = lookupContextProperty(propertyId);
|
|
594
|
+
const contextValue = context?.[propertyInfo.contextKey];
|
|
561
595
|
|
|
562
596
|
if (contextValue === undefined) {
|
|
563
597
|
return {
|
|
@@ -566,7 +600,7 @@ function evaluateContextRule(
|
|
|
566
600
|
code: "MISSING_CONTEXT",
|
|
567
601
|
scope: Scope.CONTEXT,
|
|
568
602
|
path: pathHex,
|
|
569
|
-
typeCode:
|
|
603
|
+
typeCode: propertyInfo.typeCode,
|
|
570
604
|
};
|
|
571
605
|
}
|
|
572
606
|
|
|
@@ -577,7 +611,27 @@ function evaluateContextRule(
|
|
|
577
611
|
value = contextValue;
|
|
578
612
|
}
|
|
579
613
|
|
|
580
|
-
|
|
614
|
+
let result: boolean;
|
|
615
|
+
let ctxOperand: bigint | undefined;
|
|
616
|
+
if ((opCode & ~Op.NOT) === Op.EQ_CTX) {
|
|
617
|
+
const operand = resolveContextOperand(operandData, context);
|
|
618
|
+
if (typeof operand !== "bigint") {
|
|
619
|
+
return {
|
|
620
|
+
group: groupIndex,
|
|
621
|
+
rule: ruleIndex,
|
|
622
|
+
code: "MISSING_CONTEXT",
|
|
623
|
+
scope: Scope.CONTEXT,
|
|
624
|
+
path: pathHex,
|
|
625
|
+
opCode,
|
|
626
|
+
operandData: bytesToHex(operandData),
|
|
627
|
+
typeCode: operand.ctxTypeCode,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
result = (value === operand) !== ((opCode & Op.NOT) !== 0);
|
|
631
|
+
ctxOperand = operand;
|
|
632
|
+
} else {
|
|
633
|
+
result = applyOperator(opCode, value, 32, operandData, propertyInfo.typeCode);
|
|
634
|
+
}
|
|
581
635
|
|
|
582
636
|
if (!result) {
|
|
583
637
|
return {
|
|
@@ -588,8 +642,9 @@ function evaluateContextRule(
|
|
|
588
642
|
path: pathHex,
|
|
589
643
|
opCode,
|
|
590
644
|
operandData: bytesToHex(operandData),
|
|
591
|
-
typeCode:
|
|
645
|
+
typeCode: propertyInfo.typeCode,
|
|
592
646
|
resolvedValue: bigintToHex(value),
|
|
647
|
+
...(ctxOperand !== undefined && { resolvedOperand: bigintToHex(ctxOperand) }),
|
|
593
648
|
};
|
|
594
649
|
}
|
|
595
650
|
|
package/src/policy-validator.ts
CHANGED
|
@@ -1,31 +1,28 @@
|
|
|
1
1
|
import { bigintToHex, bytesToHex, hexToBytes } from "./bytes";
|
|
2
|
-
import {
|
|
3
|
-
classifyTypeCode,
|
|
4
|
-
isQuantifier,
|
|
5
|
-
isValidOperatorData,
|
|
6
|
-
Limits,
|
|
7
|
-
lookupContextProperty,
|
|
8
|
-
MAX_CONTEXT_PROPERTY_ID,
|
|
9
|
-
Op,
|
|
10
|
-
Quantifier,
|
|
11
|
-
Scope,
|
|
12
|
-
TypeCode,
|
|
13
|
-
} from "./constants";
|
|
2
|
+
import { PolicyFormat, lookupContextProperty, MAX_CONTEXT_PROPERTY_ID, Op, Scope, TypeCode } from "./constants";
|
|
14
3
|
import { Descriptor, type TypeInfo } from "./descriptor";
|
|
15
4
|
import { decodeDescriptor } from "./descriptor-coder";
|
|
16
5
|
import { CallciumError, type CallciumErrorCode } from "./errors";
|
|
17
|
-
import {
|
|
18
|
-
|
|
6
|
+
import {
|
|
7
|
+
canonicalize,
|
|
8
|
+
isLeftAligned,
|
|
9
|
+
isSigned,
|
|
10
|
+
isLengthOp,
|
|
11
|
+
isLengthValidType,
|
|
12
|
+
classifyTypeCode,
|
|
13
|
+
isValidOperatorData,
|
|
14
|
+
} from "./operators";
|
|
15
|
+
import { Quantifier, isQuantifier, parsePathSteps } from "./path";
|
|
19
16
|
import * as ValidationIssue from "./validation-issue";
|
|
20
17
|
|
|
21
|
-
import type { Constraint, Hex, Issue, PolicyData } from "./types";
|
|
18
|
+
import type { Constraint, Hex, Issue, IssueCode, PolicyData } from "./types";
|
|
22
19
|
|
|
23
20
|
///////////////////////////////////////////////////////////////////////////
|
|
24
21
|
// Internal types
|
|
25
22
|
///////////////////////////////////////////////////////////////////////////
|
|
26
23
|
|
|
27
24
|
/** Codes a path walk raises when the path does not navigate the descriptor (PV-1). */
|
|
28
|
-
const
|
|
25
|
+
const navigabilityCodes: ReadonlySet<CallciumErrorCode> = new Set<CallciumErrorCode>([
|
|
29
26
|
"EMPTY_PATH",
|
|
30
27
|
"PARAM_INDEX_OUT_OF_BOUNDS",
|
|
31
28
|
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
@@ -139,7 +136,7 @@ function getDomainLimits(typeCode: number): { min: bigint; max: bigint } {
|
|
|
139
136
|
// Operator classification
|
|
140
137
|
///////////////////////////////////////////////////////////////////////////
|
|
141
138
|
|
|
142
|
-
/** True for EQ, GT, LT, GTE, LTE, BETWEEN, IN, BITMASK_*. */
|
|
139
|
+
/** True for EQ, GT, LT, GTE, LTE, BETWEEN, IN, EQ_CTX, BITMASK_*. */
|
|
143
140
|
function isValueOp(opBase: number): boolean {
|
|
144
141
|
return opBase <= Op.BITMASK_NONE;
|
|
145
142
|
}
|
|
@@ -159,7 +156,7 @@ function isBitmaskOp(opBase: number): boolean {
|
|
|
159
156
|
///////////////////////////////////////////////////////////////////////////
|
|
160
157
|
|
|
161
158
|
/** Return the incompatibility reason, or null if the operator is allowed. */
|
|
162
|
-
function getIncompat(opBase: number, typeInfo: TypeInfo): { code:
|
|
159
|
+
function getIncompat(opBase: number, typeInfo: TypeInfo): { code: IssueCode; message: string } | null {
|
|
163
160
|
const { typeCode, isDynamic, staticSize } = typeInfo;
|
|
164
161
|
if (isValueOp(opBase)) {
|
|
165
162
|
if (isDynamic || staticSize !== 32) {
|
|
@@ -178,6 +175,14 @@ function getIncompat(opBase: number, typeInfo: TypeInfo): { code: string; messag
|
|
|
178
175
|
if (opBase === Op.IN && typeCode === TypeCode.BOOL) {
|
|
179
176
|
return { code: "IN_ON_BOOL", message: "IN operator used on boolean type" };
|
|
180
177
|
}
|
|
178
|
+
// Context properties are addresses or uint256, so only those target classes can match.
|
|
179
|
+
if (
|
|
180
|
+
opBase === Op.EQ_CTX &&
|
|
181
|
+
typeCode !== TypeCode.ADDRESS &&
|
|
182
|
+
!(typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX)
|
|
183
|
+
) {
|
|
184
|
+
return { code: "CONTEXT_TYPE_MISMATCH", message: "Context reference operator used on incompatible type" };
|
|
185
|
+
}
|
|
181
186
|
return null;
|
|
182
187
|
}
|
|
183
188
|
if (isLengthOp(opBase)) {
|
|
@@ -890,7 +895,35 @@ function validateConstraint(
|
|
|
890
895
|
continue;
|
|
891
896
|
}
|
|
892
897
|
|
|
893
|
-
if (
|
|
898
|
+
if (base === Op.EQ_CTX) {
|
|
899
|
+
// The operand names a context property, not a value: nothing to fold into a domain.
|
|
900
|
+
const ctxOperand = readValue(opHex);
|
|
901
|
+
if (ctxOperand > BigInt(MAX_CONTEXT_PROPERTY_ID)) {
|
|
902
|
+
issues.push(
|
|
903
|
+
ValidationIssue.unknownContextProperty(
|
|
904
|
+
groupIndex,
|
|
905
|
+
constraintIndex,
|
|
906
|
+
bigintToHex(ctxOperand),
|
|
907
|
+
bigintToHex(BigInt(MAX_CONTEXT_PROPERTY_ID)),
|
|
908
|
+
),
|
|
909
|
+
);
|
|
910
|
+
} else {
|
|
911
|
+
// Compatibility has already narrowed the target to an address or unsigned type,
|
|
912
|
+
// so the pairing reduces to whether both sides are addresses.
|
|
913
|
+
const propertyIsAddress = lookupContextProperty(Number(ctxOperand)).typeCode === TypeCode.ADDRESS;
|
|
914
|
+
const targetIsAddress = ctx.typeInfo.typeCode === TypeCode.ADDRESS;
|
|
915
|
+
if (propertyIsAddress !== targetIsAddress) {
|
|
916
|
+
issues.push(
|
|
917
|
+
ValidationIssue.contextTypeMismatch(
|
|
918
|
+
groupIndex,
|
|
919
|
+
constraintIndex,
|
|
920
|
+
bigintToHex(ctxOperand),
|
|
921
|
+
bigintToHex(BigInt(ctx.typeInfo.typeCode)),
|
|
922
|
+
),
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
} else if (isValueOp(base)) {
|
|
894
927
|
// A non-canonical operand can never equal a canonicalized runtime value (PC-1, spec
|
|
895
928
|
// section 4.5), so the rule is unsatisfiable or vacuous; skip analyzing the garbage
|
|
896
929
|
// word. Scoped to the left-aligned types: numeric, address, and bool operands are
|
|
@@ -1000,13 +1033,13 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1000
1033
|
let typeInfo: TypeInfo;
|
|
1001
1034
|
if (constraint.scope === Scope.CALLDATA) {
|
|
1002
1035
|
// Compatibility warnings against the limits the spec fixes (PWF-17, PV-7).
|
|
1003
|
-
if (steps.length >
|
|
1036
|
+
if (steps.length > PolicyFormat.MAX_PATH_DEPTH) {
|
|
1004
1037
|
issues.push(
|
|
1005
1038
|
ValidationIssue.pathDepthExceeded(
|
|
1006
1039
|
groupIndex,
|
|
1007
1040
|
constraintIndex,
|
|
1008
1041
|
bigintToHex(BigInt(steps.length)),
|
|
1009
|
-
bigintToHex(BigInt(
|
|
1042
|
+
bigintToHex(BigInt(PolicyFormat.MAX_PATH_DEPTH)),
|
|
1010
1043
|
),
|
|
1011
1044
|
);
|
|
1012
1045
|
}
|
|
@@ -1015,7 +1048,7 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1015
1048
|
walk = Descriptor.walkPath(descBytes, steps);
|
|
1016
1049
|
} catch (error) {
|
|
1017
1050
|
// Navigability faults become issues; anything else propagates.
|
|
1018
|
-
if (error instanceof CallciumError &&
|
|
1051
|
+
if (error instanceof CallciumError && navigabilityCodes.has(error.code)) {
|
|
1019
1052
|
issues.push(ValidationIssue.unnavigablePath(groupIndex, constraintIndex));
|
|
1020
1053
|
continue;
|
|
1021
1054
|
}
|
|
@@ -1026,13 +1059,13 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1026
1059
|
issues.push(ValidationIssue.nestedQuantifier(groupIndex, constraintIndex));
|
|
1027
1060
|
continue;
|
|
1028
1061
|
}
|
|
1029
|
-
if (walk.quantifiedStaticLength >
|
|
1062
|
+
if (walk.quantifiedStaticLength > PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH) {
|
|
1030
1063
|
issues.push(
|
|
1031
1064
|
ValidationIssue.quantifierOverStaticLimit(
|
|
1032
1065
|
groupIndex,
|
|
1033
1066
|
constraintIndex,
|
|
1034
1067
|
bigintToHex(BigInt(walk.quantifiedStaticLength)),
|
|
1035
|
-
bigintToHex(BigInt(
|
|
1068
|
+
bigintToHex(BigInt(PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH)),
|
|
1036
1069
|
),
|
|
1037
1070
|
);
|
|
1038
1071
|
}
|
|
@@ -1067,7 +1100,7 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1067
1100
|
if (
|
|
1068
1101
|
constraint.scope === Scope.CALLDATA &&
|
|
1069
1102
|
constraint.hint !== undefined &&
|
|
1070
|
-
ctx.steps.length <=
|
|
1103
|
+
ctx.steps.length <= PolicyFormat.MAX_PATH_DEPTH
|
|
1071
1104
|
) {
|
|
1072
1105
|
const compiled = bytesToHex(Descriptor.compileHint(descBytes, ctx.steps));
|
|
1073
1106
|
if (compiled !== constraint.hint.toLowerCase()) {
|
package/src/type-desc.ts
CHANGED
|
@@ -48,7 +48,7 @@ export function function_(): Uint8Array {
|
|
|
48
48
|
*/
|
|
49
49
|
export function uintN(bits: number): Uint8Array {
|
|
50
50
|
if (bits < 8 || bits > 256 || bits % 8 !== 0) {
|
|
51
|
-
throw new CallciumError("UNKNOWN_TYPE", `Invalid uintN bits: ${bits}. Must be 8–256 in steps of 8
|
|
51
|
+
throw new CallciumError("UNKNOWN_TYPE", `Invalid uintN bits: ${bits}. Must be 8–256 in steps of 8`);
|
|
52
52
|
}
|
|
53
53
|
return new Uint8Array([TypeCode.UINT_MIN + (bits / 8 - 1)]);
|
|
54
54
|
}
|
|
@@ -60,7 +60,7 @@ export function uintN(bits: number): Uint8Array {
|
|
|
60
60
|
*/
|
|
61
61
|
export function intN(bits: number): Uint8Array {
|
|
62
62
|
if (bits < 8 || bits > 256 || bits % 8 !== 0) {
|
|
63
|
-
throw new CallciumError("UNKNOWN_TYPE", `Invalid intN bits: ${bits}. Must be 8–256 in steps of 8
|
|
63
|
+
throw new CallciumError("UNKNOWN_TYPE", `Invalid intN bits: ${bits}. Must be 8–256 in steps of 8`);
|
|
64
64
|
}
|
|
65
65
|
return new Uint8Array([TypeCode.INT_MIN + (bits / 8 - 1)]);
|
|
66
66
|
}
|
|
@@ -82,7 +82,7 @@ export function string_(): Uint8Array {
|
|
|
82
82
|
*/
|
|
83
83
|
export function bytesN(n: number): Uint8Array {
|
|
84
84
|
if (n < 1 || n > 32) {
|
|
85
|
-
throw new CallciumError("UNKNOWN_TYPE", `Invalid bytesN size: ${n}. Must be 1–32
|
|
85
|
+
throw new CallciumError("UNKNOWN_TYPE", `Invalid bytesN size: ${n}. Must be 1–32`);
|
|
86
86
|
}
|
|
87
87
|
return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (n - 1)]);
|
|
88
88
|
}
|
|
@@ -108,7 +108,7 @@ export function array(elemDesc: Uint8Array, length?: number): Uint8Array {
|
|
|
108
108
|
if (nodeLength > DF.MAX_NODE_LENGTH) {
|
|
109
109
|
throw new CallciumError(
|
|
110
110
|
"NODE_LENGTH_TOO_LARGE",
|
|
111
|
-
`Dynamic array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}
|
|
111
|
+
`Dynamic array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}`,
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
const meta24 = (0 << DF.META_STATIC_WORDS_SHIFT) | nodeLength;
|
|
@@ -123,14 +123,14 @@ export function array(elemDesc: Uint8Array, length?: number): Uint8Array {
|
|
|
123
123
|
if (length <= 0 || length > DF.MAX_STATIC_ARRAY_LENGTH) {
|
|
124
124
|
throw new CallciumError(
|
|
125
125
|
"INVALID_ARRAY_LENGTH",
|
|
126
|
-
`Static array length ${length} out of range. Must be 1–${DF.MAX_STATIC_ARRAY_LENGTH}
|
|
126
|
+
`Static array length ${length} out of range. Must be 1–${DF.MAX_STATIC_ARRAY_LENGTH}`,
|
|
127
127
|
);
|
|
128
128
|
}
|
|
129
129
|
const nodeLength = DF.ARRAY_HEADER_SIZE + elemDesc.length + DF.ARRAY_LENGTH_SIZE;
|
|
130
130
|
if (nodeLength > DF.MAX_NODE_LENGTH) {
|
|
131
131
|
throw new CallciumError(
|
|
132
132
|
"NODE_LENGTH_TOO_LARGE",
|
|
133
|
-
`Static array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}
|
|
133
|
+
`Static array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}`,
|
|
134
134
|
);
|
|
135
135
|
}
|
|
136
136
|
const elemStaticWords = extractStaticWords(elemDesc);
|
|
@@ -138,7 +138,7 @@ export function array(elemDesc: Uint8Array, length?: number): Uint8Array {
|
|
|
138
138
|
if (staticWords > DF.MAX_STATIC_WORDS) {
|
|
139
139
|
throw new CallciumError(
|
|
140
140
|
"STATIC_WORDS_TOO_LARGE",
|
|
141
|
-
`Static array static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}
|
|
141
|
+
`Static array static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}`,
|
|
142
142
|
);
|
|
143
143
|
}
|
|
144
144
|
const meta24 = (staticWords << DF.META_STATIC_WORDS_SHIFT) | nodeLength;
|
|
@@ -162,7 +162,7 @@ export function tuple(fieldDescs: Uint8Array[]): Uint8Array {
|
|
|
162
162
|
if (fieldDescs.length === 0 || fieldDescs.length > DF.MAX_TUPLE_FIELDS) {
|
|
163
163
|
throw new CallciumError(
|
|
164
164
|
"INVALID_TUPLE_FIELD_COUNT",
|
|
165
|
-
`Tuple field count ${fieldDescs.length} out of range. Must be 1–${DF.MAX_TUPLE_FIELDS}
|
|
165
|
+
`Tuple field count ${fieldDescs.length} out of range. Must be 1–${DF.MAX_TUPLE_FIELDS}`,
|
|
166
166
|
);
|
|
167
167
|
}
|
|
168
168
|
const totalFieldBytes = fieldDescs.reduce((sum, f) => sum + f.length, 0);
|
|
@@ -170,7 +170,7 @@ export function tuple(fieldDescs: Uint8Array[]): Uint8Array {
|
|
|
170
170
|
if (nodeLength > DF.MAX_NODE_LENGTH) {
|
|
171
171
|
throw new CallciumError(
|
|
172
172
|
"NODE_LENGTH_TOO_LARGE",
|
|
173
|
-
`Tuple node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}
|
|
173
|
+
`Tuple node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}`,
|
|
174
174
|
);
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -187,7 +187,7 @@ export function tuple(fieldDescs: Uint8Array[]): Uint8Array {
|
|
|
187
187
|
if (staticWords > DF.MAX_STATIC_WORDS) {
|
|
188
188
|
throw new CallciumError(
|
|
189
189
|
"STATIC_WORDS_TOO_LARGE",
|
|
190
|
-
`Tuple static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}
|
|
190
|
+
`Tuple static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}`,
|
|
191
191
|
);
|
|
192
192
|
}
|
|
193
193
|
const meta24 = (staticWords << DF.META_STATIC_WORDS_SHIFT) | nodeLength;
|