@callcium/sdk 0.11.1 → 0.12.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 +101 -88
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +104 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bytes.ts +8 -25
- package/src/calldata-reader.ts +3 -3
- package/src/constants.ts +5 -1
- package/src/constraint.ts +40 -22
- package/src/descriptor-coder.ts +14 -23
- package/src/descriptor.ts +18 -19
- package/src/errors.ts +14 -17
- package/src/index.ts +2 -3
- package/src/operators.ts +65 -35
- package/src/path.ts +1 -2
- package/src/policy-builder.ts +15 -20
- package/src/policy-coder.ts +20 -29
- package/src/policy-enforcer.ts +21 -23
- package/src/policy-validator.ts +51 -44
- package/src/signature.ts +6 -6
- package/src/type-desc.ts +6 -6
- package/src/types.ts +11 -16
package/src/policy-coder.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
2
|
+
import { concatBytes } from "@noble/hashes/utils.js";
|
|
2
3
|
|
|
3
4
|
import { bytesToHex, hexToBytes, toHex, readU16, readU32, writeBE16, writeBE32 } from "./bytes";
|
|
4
5
|
import {
|
|
@@ -15,6 +16,7 @@ import { Descriptor } from "./descriptor";
|
|
|
15
16
|
import { decodeDescriptor } from "./descriptor-coder";
|
|
16
17
|
import { CallciumError, type CallciumErrorCode } from "./errors";
|
|
17
18
|
import {
|
|
19
|
+
isContextOp,
|
|
18
20
|
isLengthOp,
|
|
19
21
|
isLengthValidType,
|
|
20
22
|
toBigInt,
|
|
@@ -45,9 +47,8 @@ function field<T>(value: T, start: number, end: number): Field<T> {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
/**
|
|
48
|
-
* Wrap the hex rendering of `data[start, end)` with its span, rendering on first read.
|
|
49
|
-
*
|
|
50
|
-
* The field reads and assigns like the plain one it replaces.
|
|
50
|
+
* Wrap the hex rendering of `data[start, end)` with its span, rendering on first read. Enforcement reads the span as
|
|
51
|
+
* bytes, so the string is built only for a caller that asks. The field reads and assigns like a plain one.
|
|
51
52
|
*/
|
|
52
53
|
function hexField(data: Uint8Array, start: number, end: number): Field<Hex> {
|
|
53
54
|
let hex: Hex | undefined;
|
|
@@ -140,8 +141,8 @@ function validateHint(data: Uint8Array, hintStart: number, hintSize: number, rul
|
|
|
140
141
|
targetTypeCode === TypeCode.DYNAMIC_ARRAY ? (targetMeta & PF.HINT_META_RESERVED_MASK) === 0 : targetMeta === 0;
|
|
141
142
|
if (!targetMetaOk) malformedHint("Target meta carries reserved or unused state", ruleOffset);
|
|
142
143
|
|
|
143
|
-
// An operator reads either a scalar word or a declared length, so a target carrying neither —
|
|
144
|
-
//
|
|
144
|
+
// An operator reads either a scalar word or a declared length, so a target carrying neither — a tuple, a static
|
|
145
|
+
// array, or an undefined code — addresses nothing.
|
|
145
146
|
if (!isAddressableTarget(targetTypeCode)) malformedHint("Target type code addresses no value", ruleOffset);
|
|
146
147
|
}
|
|
147
148
|
|
|
@@ -300,8 +301,8 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
300
301
|
throw new CallciumError("UNKNOWN_OPERATOR", "Unrecognized or malformed operator", ruleOffset);
|
|
301
302
|
}
|
|
302
303
|
|
|
303
|
-
// A length operator reads the declared length a dynamic target resolves to, and a value
|
|
304
|
-
//
|
|
304
|
+
// A length operator reads the declared length a dynamic target resolves to, and a value operator reads a scalar
|
|
305
|
+
// word, so the target type fixes which family the rule may carry.
|
|
305
306
|
if (hintSize !== 0) {
|
|
306
307
|
const targetTypeCode = hintTargetTypeCode(data, hintStart, hintSize);
|
|
307
308
|
if (isLengthValidType(targetTypeCode) !== isLengthOp(opBase)) {
|
|
@@ -328,12 +329,12 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
328
329
|
}
|
|
329
330
|
}
|
|
330
331
|
|
|
331
|
-
//
|
|
332
|
-
if (opBase
|
|
332
|
+
// A context reference operand word must be a defined context property ID.
|
|
333
|
+
if (isContextOp(opBase)) {
|
|
333
334
|
if (toBigInt(data, dataStart) > BigInt(MAX_CONTEXT_PROPERTY_ID)) {
|
|
334
335
|
throw new CallciumError(
|
|
335
336
|
"UNKNOWN_CONTEXT_PROPERTY",
|
|
336
|
-
"
|
|
337
|
+
"Context reference operand references an undefined context property",
|
|
337
338
|
ruleOffset,
|
|
338
339
|
);
|
|
339
340
|
}
|
|
@@ -468,9 +469,8 @@ function readConstraintTarget(constraint: Constraint, desc: Uint8Array): Constra
|
|
|
468
469
|
|
|
469
470
|
/** Flatten a Constraint into one Rule per operator, compiling its hint against the descriptor. */
|
|
470
471
|
function flattenConstraint(constraint: Constraint, desc: Uint8Array): Rule[] {
|
|
471
|
-
// The rule is the unit these checks run over: each rule's operator is established before the
|
|
472
|
-
//
|
|
473
|
-
// so its path is never read.
|
|
472
|
+
// The rule is the unit these checks run over: each rule's operator is established before the path and hint it shares
|
|
473
|
+
// with its siblings. A constraint carrying no operator produces no rule, so its path is never read.
|
|
474
474
|
let target: ConstraintTarget | undefined;
|
|
475
475
|
|
|
476
476
|
return constraint.operators.map((operatorHex) => {
|
|
@@ -509,8 +509,7 @@ function requireFieldFits(value: number, byteWidth: number, code: CallciumErrorC
|
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
/**
|
|
512
|
-
* Serialize a single rule to its wire format bytes.
|
|
513
|
-
* Path shape and operator framing are established by `flattenConstraint`.
|
|
512
|
+
* Serialize a single rule to its wire format bytes. `flattenConstraint` establishes path shape and operator framing.
|
|
514
513
|
*/
|
|
515
514
|
function encodeRule(rule: Rule): Uint8Array {
|
|
516
515
|
const depth = rule.path.length / 2;
|
|
@@ -537,15 +536,7 @@ function encodeRule(rule: Rule): Uint8Array {
|
|
|
537
536
|
|
|
538
537
|
/** Serialize all rules in a group to a single byte array. */
|
|
539
538
|
function encodeGroupRules(rules: Rule[]): Uint8Array {
|
|
540
|
-
|
|
541
|
-
const totalSize = parts.reduce((sum, p) => sum + p.length, 0);
|
|
542
|
-
const buf = new Uint8Array(totalSize);
|
|
543
|
-
let offset = 0;
|
|
544
|
-
for (const part of parts) {
|
|
545
|
-
buf.set(part, offset);
|
|
546
|
-
offset += part.length;
|
|
547
|
-
}
|
|
548
|
-
return buf;
|
|
539
|
+
return concatBytes(...rules.map(encodeRule));
|
|
549
540
|
}
|
|
550
541
|
|
|
551
542
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -572,8 +563,8 @@ function encode(data: PolicyData): Hex {
|
|
|
572
563
|
}
|
|
573
564
|
requireFieldFits(data.groups.length, PF.GROUP_COUNT_SIZE, "GROUP_COUNT_OVERFLOW", "Group count");
|
|
574
565
|
|
|
575
|
-
// Flatten constraints into rules and sort within each group. Hints are compiled first because
|
|
576
|
-
//
|
|
566
|
+
// Flatten constraints into rules and sort within each group. Hints are compiled first because they are part of the
|
|
567
|
+
// rule bytes the group hash covers.
|
|
577
568
|
const sortedGroups: Rule[][] = data.groups.map((group) => {
|
|
578
569
|
const rules = group.flatMap((constraint) => flattenConstraint(constraint, descBytes));
|
|
579
570
|
sortRules(rules);
|
|
@@ -590,8 +581,8 @@ function encode(data: PolicyData): Hex {
|
|
|
590
581
|
return { wireBytes: encodeGroupRules(rules), ruleCount: rules.length };
|
|
591
582
|
});
|
|
592
583
|
|
|
593
|
-
// Several groups are ordered by ascending keccak256 over their rule bytes; a lone group is
|
|
594
|
-
//
|
|
584
|
+
// Several groups are ordered by ascending keccak256 over their rule bytes; a lone group is already in canonical
|
|
585
|
+
// position, so it needs no hash.
|
|
595
586
|
if (encodedGroups.length > 1) {
|
|
596
587
|
const hashes = new Map(encodedGroups.map((group) => [group, keccak_256(group.wireBytes)]));
|
|
597
588
|
encodedGroups.sort((a, b) => compareBytes(hashes.get(a)!, hashes.get(b)!));
|
|
@@ -648,7 +639,7 @@ function encode(data: PolicyData): Hex {
|
|
|
648
639
|
/**
|
|
649
640
|
* Decode a binary policy blob into a PolicyData structure.
|
|
650
641
|
* @param blob - Binary policy as 0x-prefixed hex string.
|
|
651
|
-
* @returns The decoded policy data with constraints grouped by scope and
|
|
642
|
+
* @returns The decoded policy data with constraints grouped by scope, path, and hint.
|
|
652
643
|
* @throws {CallciumError} If the blob is structurally malformed.
|
|
653
644
|
*/
|
|
654
645
|
function decode(blob: Hex): PolicyData {
|
package/src/policy-enforcer.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { hexToBytes, bytesToHex, readU16, readU32, bigintToHex, toAddress } from "./bytes";
|
|
2
2
|
import { loadWord, readLength, readPointer } from "./calldata-reader";
|
|
3
|
-
import { PolicyFormat as PF, Scope,
|
|
3
|
+
import { PolicyFormat as PF, Scope, TypeCode, lookupContextProperty } from "./constants";
|
|
4
4
|
import { CallciumError, PolicyViolationError } from "./errors";
|
|
5
|
-
import { applyOperator, toBigInt, isLengthValidType, canonicalize } from "./operators";
|
|
5
|
+
import { applyOperator, isContextOp, toBigInt, isLengthValidType, canonicalize } from "./operators";
|
|
6
6
|
import { decodePolicy } from "./policy-coder";
|
|
7
7
|
|
|
8
8
|
import type { ReadResult } from "./calldata-reader";
|
|
@@ -32,7 +32,7 @@ const abortViolationCodes: ReadonlySet<ViolationCode> = new Set<ViolationCode>([
|
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Read a supplied context value as the 32-byte word that carries it.
|
|
35
|
-
* @throws {CallciumError} When an integer
|
|
35
|
+
* @throws {CallciumError} When an address is not 20 bytes of hex or an integer does not fit a 32-byte word.
|
|
36
36
|
*/
|
|
37
37
|
function contextValueToWord(value: bigint | Address): bigint {
|
|
38
38
|
if (typeof value === "string") return BigInt(toAddress(value));
|
|
@@ -55,8 +55,8 @@ function contextValueToWord(value: bigint | Address): bigint {
|
|
|
55
55
|
* @param callData - ABI-encoded call data as 0x-prefixed hex string.
|
|
56
56
|
* @param context - Optional execution context for context-scoped rules.
|
|
57
57
|
* @returns Pass with matched group index, or fail with one violation per evaluated failing group.
|
|
58
|
-
* @throws {CallciumError} If the policy blob is structurally malformed,
|
|
59
|
-
* reads is not an address or an integer a 32-byte word represents.
|
|
58
|
+
* @throws {CallciumError} If the policy blob is structurally malformed, the call data is not valid hex, or a context
|
|
59
|
+
* value a rule reads is not an address or an integer a 32-byte word represents.
|
|
60
60
|
*/
|
|
61
61
|
function check(policy: Hex, callData: Hex, context?: Context): EnforceResult {
|
|
62
62
|
const { policy: decoded, data: policyBytes } = decodePolicy(policy);
|
|
@@ -123,8 +123,8 @@ function check(policy: Hex, callData: Hex, context?: Context): EnforceResult {
|
|
|
123
123
|
* @param callData - ABI-encoded call data as 0x-prefixed hex string.
|
|
124
124
|
* @param context - Optional execution context for context-scoped rules.
|
|
125
125
|
* @throws {PolicyViolationError} If the policy rejects the call data.
|
|
126
|
-
* @throws {CallciumError} If the policy blob is structurally malformed,
|
|
127
|
-
* reads is not an address or an integer a 32-byte word represents.
|
|
126
|
+
* @throws {CallciumError} If the policy blob is structurally malformed, the call data is not valid hex, or a context
|
|
127
|
+
* value a rule reads is not an address or an integer a 32-byte word represents.
|
|
128
128
|
*/
|
|
129
129
|
function enforce(policy: Hex, callData: Hex, context?: Context): void {
|
|
130
130
|
const result = check(policy, callData, context);
|
|
@@ -178,12 +178,12 @@ function evaluateRule(
|
|
|
178
178
|
);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
/**
|
|
181
|
+
/** A context reference operand resolved to its word, or the missing property's type code. */
|
|
182
182
|
type ReferenceOperand = bigint | { ctxTypeCode: number };
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
|
-
* Resolve
|
|
186
|
-
*
|
|
185
|
+
* Resolve a context reference operand word to the referenced context property's value as a raw word. Returns the
|
|
186
|
+
* missing property's type code when the context does not supply it.
|
|
187
187
|
*/
|
|
188
188
|
function resolveContextOperand(operandData: Uint8Array, context?: Context): ReferenceOperand {
|
|
189
189
|
const propertyInfo = lookupContextProperty(Number(toBigInt(operandData, 0)));
|
|
@@ -192,9 +192,9 @@ function resolveContextOperand(operandData: Uint8Array, context?: Context): Refe
|
|
|
192
192
|
return contextValueToWord(value);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
/** Resolve the reference operand of
|
|
195
|
+
/** Resolve the reference operand of a context reference rule; every other operator reads a literal. */
|
|
196
196
|
function referenceOperand(opCode: number, operandData: Uint8Array, context?: Context): ReferenceOperand | undefined {
|
|
197
|
-
if ((opCode
|
|
197
|
+
if (!isContextOp(opCode)) return undefined;
|
|
198
198
|
return resolveContextOperand(operandData, context);
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -281,9 +281,8 @@ type TargetResult =
|
|
|
281
281
|
/**
|
|
282
282
|
* Load the value at a resolved target offset and apply the operator.
|
|
283
283
|
*
|
|
284
|
-
* `value` is normalised at the decision point: length operators yield the byte/element
|
|
285
|
-
*
|
|
286
|
-
* decodable. A passing target allocates no string.
|
|
284
|
+
* `value` is normalised at the decision point: length operators yield the byte/element count; scalar operators preserve
|
|
285
|
+
* the full 32-byte ABI word, keeping left-aligned `bytesN` decodable. A passing target allocates no string.
|
|
287
286
|
*/
|
|
288
287
|
function evalTarget(
|
|
289
288
|
callData: Uint8Array,
|
|
@@ -319,7 +318,7 @@ function evalTarget(
|
|
|
319
318
|
|
|
320
319
|
if (ctxOperand !== undefined) {
|
|
321
320
|
if (typeof ctxOperand !== "bigint") return { error: "MISSING_CONTEXT", ctxTypeCode: ctxOperand.ctxTypeCode };
|
|
322
|
-
const passed = (value
|
|
321
|
+
const passed = applyOperator(opCode, value, 32n, hexToBytes(bigintToHex(ctxOperand)), typeCode);
|
|
323
322
|
return { passed, value, ctxOperand };
|
|
324
323
|
}
|
|
325
324
|
|
|
@@ -539,8 +538,8 @@ function evaluateQuantified(
|
|
|
539
538
|
let elem: number = slot;
|
|
540
539
|
if (elemIsDynamic) {
|
|
541
540
|
const followed = follow(callDataBytes, elems, slot);
|
|
542
|
-
// Navigation failures abort under every quantifier: calldata the enforcer cannot read is
|
|
543
|
-
//
|
|
541
|
+
// Navigation failures abort under every quantifier: calldata the enforcer cannot read is not an element that
|
|
542
|
+
// merely fails the operator.
|
|
544
543
|
if (typeof followed !== "number") return navigationViolation(frame, followed.code, elemIndex);
|
|
545
544
|
elem = followed;
|
|
546
545
|
}
|
|
@@ -554,9 +553,8 @@ function evaluateQuantified(
|
|
|
554
553
|
|
|
555
554
|
const applied = evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData, reference);
|
|
556
555
|
if ("error" in applied) {
|
|
557
|
-
// Error results end the rule whatever the quantifier: a later element cannot rescue
|
|
558
|
-
//
|
|
559
|
-
// property is missing for every element alike (group-local).
|
|
556
|
+
// Error results end the rule whatever the quantifier: a later element cannot rescue calldata the enforcer cannot
|
|
557
|
+
// read (an abort effect), and a missing context property is missing for every element alike (group-local).
|
|
560
558
|
return targetViolation(frame, applied, elemIndex);
|
|
561
559
|
}
|
|
562
560
|
|
|
@@ -604,7 +602,7 @@ function evaluateContextRule(
|
|
|
604
602
|
|
|
605
603
|
let result: boolean;
|
|
606
604
|
let ctxOperand: bigint | undefined;
|
|
607
|
-
if ((opCode
|
|
605
|
+
if (isContextOp(opCode)) {
|
|
608
606
|
const operand = resolveContextOperand(operandData, context);
|
|
609
607
|
if (typeof operand !== "bigint") {
|
|
610
608
|
return {
|
|
@@ -615,7 +613,7 @@ function evaluateContextRule(
|
|
|
615
613
|
typeCode: operand.ctxTypeCode,
|
|
616
614
|
};
|
|
617
615
|
}
|
|
618
|
-
result = (value
|
|
616
|
+
result = applyOperator(opCode, value, 32n, hexToBytes(bigintToHex(operand)), propertyInfo.typeCode);
|
|
619
617
|
ctxOperand = operand;
|
|
620
618
|
} else {
|
|
621
619
|
result = applyOperator(opCode, value, 32n, operandData, propertyInfo.typeCode);
|
package/src/policy-validator.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { bigintToHex, bytesToHex, hexToBytes } from "./bytes";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
fieldMax,
|
|
4
|
+
findContextProperty,
|
|
5
|
+
MAX_CONTEXT_PROPERTY_ID,
|
|
6
|
+
Op,
|
|
7
|
+
operandsOf,
|
|
8
|
+
PolicyFormat,
|
|
9
|
+
Scope,
|
|
10
|
+
TypeCode,
|
|
11
|
+
} from "./constants";
|
|
3
12
|
import { Descriptor, type TypeInfo } from "./descriptor";
|
|
4
13
|
import { decodeDescriptor } from "./descriptor-coder";
|
|
5
14
|
import { CallciumError, type CallciumErrorCode } from "./errors";
|
|
@@ -7,6 +16,7 @@ import {
|
|
|
7
16
|
canonicalize,
|
|
8
17
|
isLeftAligned,
|
|
9
18
|
isSigned,
|
|
19
|
+
isContextOp,
|
|
10
20
|
isLengthOp,
|
|
11
21
|
isLengthValidType,
|
|
12
22
|
isElementary,
|
|
@@ -143,7 +153,7 @@ function getDomainLimits(typeCode: number): { min: bigint; max: bigint } {
|
|
|
143
153
|
// Operator classification
|
|
144
154
|
///////////////////////////////////////////////////////////////////////////
|
|
145
155
|
|
|
146
|
-
/** True
|
|
156
|
+
/** True if an operator code is in the value-operator range (unassigned codes included). */
|
|
147
157
|
function isValueOp(opBase: number): boolean {
|
|
148
158
|
return opBase <= Op.BITMASK_NONE;
|
|
149
159
|
}
|
|
@@ -163,7 +173,7 @@ function isBitmaskOp(opBase: number): boolean {
|
|
|
163
173
|
///////////////////////////////////////////////////////////////////////////
|
|
164
174
|
|
|
165
175
|
/** Return the incompatibility reason, or null if the operator is allowed. */
|
|
166
|
-
function
|
|
176
|
+
function checkCompatibility(opBase: number, typeInfo: TypeInfo): { code: IssueCode; message: string } | null {
|
|
167
177
|
const { typeCode, isDynamic, staticSize } = typeInfo;
|
|
168
178
|
if (isValueOp(opBase)) {
|
|
169
179
|
if (isDynamic || staticSize !== 32) {
|
|
@@ -178,15 +188,16 @@ function getIncompat(opBase: number, typeInfo: TypeInfo): { code: IssueCode; mes
|
|
|
178
188
|
if (isBitmaskOp(opBase) && !isBitmaskCompatible(typeCode)) {
|
|
179
189
|
return { code: "BITMASK_ON_INVALID", message: "Bitmask operator used on incompatible type" };
|
|
180
190
|
}
|
|
181
|
-
// IN on bool is degenerate:
|
|
191
|
+
// IN on bool is degenerate: two values reduce every set to an equality, a tautology, or dead members.
|
|
182
192
|
if (opBase === Op.IN && typeCode === TypeCode.BOOL) {
|
|
183
193
|
return { code: "IN_ON_BOOL", message: "IN operator used on boolean type" };
|
|
184
194
|
}
|
|
185
|
-
// Context properties are addresses or uint256, so only those target classes can match
|
|
195
|
+
// Context properties are addresses or uint256, so only those target classes can match, and an address is an
|
|
196
|
+
// identity rather than a magnitude, so it pairs under equality only.
|
|
186
197
|
if (
|
|
187
|
-
opBase
|
|
188
|
-
typeCode
|
|
189
|
-
!(
|
|
198
|
+
isContextOp(opBase) &&
|
|
199
|
+
!(typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) &&
|
|
200
|
+
!(opBase === Op.EQ_CTX && typeCode === TypeCode.ADDRESS)
|
|
190
201
|
) {
|
|
191
202
|
return { code: "CONTEXT_TYPE_MISMATCH", message: "Context reference operator used on incompatible type" };
|
|
192
203
|
}
|
|
@@ -202,11 +213,12 @@ function getIncompat(opBase: number, typeInfo: TypeInfo): { code: IssueCode; mes
|
|
|
202
213
|
}
|
|
203
214
|
|
|
204
215
|
/**
|
|
205
|
-
* True if operator `opCode` is allowed on a target with the given type info.
|
|
206
|
-
*
|
|
216
|
+
* True if operator `opCode` is a defined operator allowed on a target with the given type info. Tolerates the NOT flag
|
|
217
|
+
* on `opCode`.
|
|
207
218
|
*/
|
|
208
219
|
export function isOpAllowed(opCode: number, typeInfo: TypeInfo): boolean {
|
|
209
|
-
|
|
220
|
+
const opBase = opCode & ~Op.NOT;
|
|
221
|
+
return operandsOf(opBase) !== undefined && checkCompatibility(opBase, typeInfo) === null;
|
|
210
222
|
}
|
|
211
223
|
|
|
212
224
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -690,7 +702,7 @@ function updateSet(
|
|
|
690
702
|
constraintIndex: number,
|
|
691
703
|
issues: Issue[],
|
|
692
704
|
): void {
|
|
693
|
-
// Physical bounds: a member outside the type's domain never occurs, so it
|
|
705
|
+
// Physical bounds: a member outside the type's domain never occurs, so it matches and excludes nothing.
|
|
694
706
|
for (const value of values) {
|
|
695
707
|
if (isOutsideDomain(ctx.numeric, value)) {
|
|
696
708
|
issues.push(ValidationIssue.outOfPhysicalBounds(false, groupIndex, constraintIndex, bigintToHex(value)));
|
|
@@ -781,9 +793,9 @@ function checkDuplicates(issues: Issue[], operators: Hex[], groupIndex: number,
|
|
|
781
793
|
///////////////////////////////////////////////////////////////////////////
|
|
782
794
|
|
|
783
795
|
/**
|
|
784
|
-
* Report a lone unnegated gte/lte pair (value or length domain) as fusible into the
|
|
785
|
-
*
|
|
786
|
-
*
|
|
796
|
+
* Report a lone unnegated gte/lte pair (value or length domain) as fusible into the corresponding single range
|
|
797
|
+
* operator. Fires only for a satisfiable pair on a compatible type; contradictions and dominated bounds are already
|
|
798
|
+
* reported by the domain checks.
|
|
787
799
|
*/
|
|
788
800
|
function checkFusibleRange(
|
|
789
801
|
ctx: ConstraintContext,
|
|
@@ -795,7 +807,7 @@ function checkFusibleRange(
|
|
|
795
807
|
const numericPair = findLonePair(operators, Op.GTE, Op.LTE);
|
|
796
808
|
if (
|
|
797
809
|
numericPair &&
|
|
798
|
-
|
|
810
|
+
checkCompatibility(Op.GTE, ctx.typeInfo) === null &&
|
|
799
811
|
signedCompare(numericPair.low, numericPair.high, ctx.numeric.isSigned) <= 0n
|
|
800
812
|
) {
|
|
801
813
|
issues.push(
|
|
@@ -810,7 +822,7 @@ function checkFusibleRange(
|
|
|
810
822
|
}
|
|
811
823
|
|
|
812
824
|
const lengthPair = findLonePair(operators, Op.LENGTH_GTE, Op.LENGTH_LTE);
|
|
813
|
-
if (lengthPair &&
|
|
825
|
+
if (lengthPair && checkCompatibility(Op.LENGTH_GTE, ctx.typeInfo) === null && lengthPair.low <= lengthPair.high) {
|
|
814
826
|
issues.push(
|
|
815
827
|
ValidationIssue.fusibleRange(
|
|
816
828
|
true,
|
|
@@ -894,12 +906,12 @@ function validateConstraint(
|
|
|
894
906
|
issues.push(ValidationIssue.negationUnderAny(groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
895
907
|
}
|
|
896
908
|
|
|
897
|
-
const
|
|
898
|
-
if (
|
|
909
|
+
const incompatibility = checkCompatibility(base, ctx.typeInfo);
|
|
910
|
+
if (incompatibility) {
|
|
899
911
|
issues.push(
|
|
900
912
|
ValidationIssue.fromOpRule(
|
|
901
|
-
|
|
902
|
-
|
|
913
|
+
incompatibility.code,
|
|
914
|
+
incompatibility.message,
|
|
903
915
|
groupIndex,
|
|
904
916
|
constraintIndex,
|
|
905
917
|
bigintToHex(BigInt(opCode)),
|
|
@@ -908,7 +920,7 @@ function validateConstraint(
|
|
|
908
920
|
continue;
|
|
909
921
|
}
|
|
910
922
|
|
|
911
|
-
if (base
|
|
923
|
+
if (isContextOp(base)) {
|
|
912
924
|
// The operand names a context property, not a value: nothing to fold into a domain.
|
|
913
925
|
const ctxOperand = readValue(opHex);
|
|
914
926
|
const property = findContextProperty(Number(ctxOperand));
|
|
@@ -922,8 +934,8 @@ function validateConstraint(
|
|
|
922
934
|
),
|
|
923
935
|
);
|
|
924
936
|
} else {
|
|
925
|
-
// Compatibility has already narrowed the target to an address or unsigned type,
|
|
926
|
-
//
|
|
937
|
+
// Compatibility has already narrowed the target to an address or unsigned type, so the pairing reduces to
|
|
938
|
+
// whether both sides are addresses.
|
|
927
939
|
const propertyIsAddress = property.typeCode === TypeCode.ADDRESS;
|
|
928
940
|
const targetIsAddress = ctx.typeInfo.typeCode === TypeCode.ADDRESS;
|
|
929
941
|
if (propertyIsAddress !== targetIsAddress) {
|
|
@@ -938,10 +950,8 @@ function validateConstraint(
|
|
|
938
950
|
}
|
|
939
951
|
}
|
|
940
952
|
} else if (isValueOp(base)) {
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
// word. Scoped to the left-aligned types: numeric, address, and bool operands are
|
|
944
|
-
// already covered by the physical domain limits.
|
|
953
|
+
// An operand that breaks PC-1 never matches, since enforcement only admits canonical values, so the rule is
|
|
954
|
+
// unsatisfiable or vacuous; skip it. Numeric, address and bool operands are already covered by the domain limits.
|
|
945
955
|
const typeCode = ctx.typeInfo.typeCode;
|
|
946
956
|
if (isLeftAligned(typeCode)) {
|
|
947
957
|
const nonCanonical = findNonCanonicalWord(opHex, typeCode);
|
|
@@ -960,10 +970,9 @@ function validateConstraint(
|
|
|
960
970
|
|
|
961
971
|
if (base >= Op.EQ && base <= Op.BETWEEN) {
|
|
962
972
|
if (base === Op.BETWEEN) {
|
|
963
|
-
// A negated range is a disjunction (value < low OR value > high); distributing the
|
|
964
|
-
//
|
|
965
|
-
//
|
|
966
|
-
// still surface: negating an empty range excludes nothing.
|
|
973
|
+
// A negated range is a disjunction (value < low OR value > high); distributing the negation over the
|
|
974
|
+
// decomposed bounds would conjoin the complement halves into a spurious contradiction. Leave it un-analyzed,
|
|
975
|
+
// like a neq hole. Inverted bounds still surface: negating an empty range excludes nothing.
|
|
967
976
|
const { low, high } = readPair(opHex);
|
|
968
977
|
if (!isNegated) {
|
|
969
978
|
updateBound(ctx.numeric, Op.GTE, false, low, false, groupIndex, constraintIndex, issues);
|
|
@@ -1047,7 +1056,7 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1047
1056
|
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
1048
1057
|
let typeInfo: TypeInfo;
|
|
1049
1058
|
if (constraint.scope === Scope.CALLDATA) {
|
|
1050
|
-
// Compatibility
|
|
1059
|
+
// Compatibility issues against the limits the spec fixes (PWF-17, PV-7).
|
|
1051
1060
|
if (steps.length > PolicyFormat.MAX_PATH_DEPTH) {
|
|
1052
1061
|
issues.push(
|
|
1053
1062
|
ValidationIssue.pathDepthExceeded(
|
|
@@ -1089,8 +1098,8 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1089
1098
|
const ctxId = steps[0]!;
|
|
1090
1099
|
const property = findContextProperty(ctxId);
|
|
1091
1100
|
if (!property) {
|
|
1092
|
-
// An unassigned ID declares no type, so the analysis proceeds over the widest
|
|
1093
|
-
//
|
|
1101
|
+
// An unassigned ID declares no type, so the analysis proceeds over the widest unsigned domain and the warning
|
|
1102
|
+
// carries the ID.
|
|
1094
1103
|
issues.push(
|
|
1095
1104
|
ValidationIssue.unknownContextProperty(
|
|
1096
1105
|
groupIndex,
|
|
@@ -1106,10 +1115,9 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1106
1115
|
contexts.push(ctx);
|
|
1107
1116
|
}
|
|
1108
1117
|
|
|
1109
|
-
// A carried hint must equal the compilation of its own path; constraints without an encoding
|
|
1110
|
-
//
|
|
1111
|
-
// the
|
|
1112
|
-
// most once, so only the depth PWF-17 admits remains to be established.
|
|
1118
|
+
// A carried hint must equal the compilation of its own path; constraints without an encoding carry none, an empty
|
|
1119
|
+
// one carries nothing to compare, and the encoder compiles theirs from the same descriptor. Reaching here means the
|
|
1120
|
+
// path navigates the descriptor and quantifies at most once, so only the PWF-17 depth check remains.
|
|
1113
1121
|
if (
|
|
1114
1122
|
constraint.scope === Scope.CALLDATA &&
|
|
1115
1123
|
constraint.hint !== undefined &&
|
|
@@ -1136,11 +1144,10 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
|
|
|
1136
1144
|
* Validate a policy for type mismatches, contradictions, redundancies, and vacuities.
|
|
1137
1145
|
* @param data - The canonical policy data to validate.
|
|
1138
1146
|
* @returns All validation issues found, ordered by group and constraint index.
|
|
1139
|
-
* @throws {CallciumError} If the descriptor is
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
* is defined over canonical data, so these reject the input rather than report an issue on it.
|
|
1147
|
+
* @throws {CallciumError} If the descriptor is malformed; semantic validation is defined only over a well-formed one.
|
|
1148
|
+
* @throws {CallciumError} With `EMPTY_PATH` or `MALFORMED_PATH` for a path that is empty or not whole steps, and
|
|
1149
|
+
* `INVALID_HEX` or `INVALID_OPERATOR_BYTES` for an unreadable path or operator. Analysis is defined over canonical
|
|
1150
|
+
* data, so these reject the input rather than report an issue.
|
|
1144
1151
|
*/
|
|
1145
1152
|
function validate(data: PolicyData): Issue[] {
|
|
1146
1153
|
const descBytes = hexToBytes(data.descriptor);
|
package/src/signature.ts
CHANGED
|
@@ -31,14 +31,14 @@ function isAlphanum(char: number): boolean {
|
|
|
31
31
|
/**
|
|
32
32
|
* Parse an ABI function signature into its selector and types.
|
|
33
33
|
*
|
|
34
|
-
* Strict mode: no
|
|
35
|
-
*
|
|
34
|
+
* Strict mode: no space, tab, LF or CR; name must match `[A-Za-z_][A-Za-z0-9_]*`; structure must be `name(types)` with
|
|
35
|
+
* the final `)` as the last character.
|
|
36
36
|
*
|
|
37
37
|
* @param signature - A function signature string, e.g. `"transfer(address,uint256)"`.
|
|
38
38
|
* @returns The 4-byte selector and the comma-separated types string.
|
|
39
39
|
* @throws {CallciumError} with code `"MALFORMED_SIGNATURE"`, `"SIGNATURE_CONTAINS_WHITESPACE"`, or
|
|
40
|
-
* `"INVALID_FUNCTION_NAME"` if the signature is malformed. The types between the parentheses are
|
|
41
|
-
*
|
|
40
|
+
* `"INVALID_FUNCTION_NAME"` if the signature is malformed. The types between the parentheses are returned unread;
|
|
41
|
+
* whether they name types is for the type parser to say.
|
|
42
42
|
*/
|
|
43
43
|
function parse(signature: string): ParsedSignature {
|
|
44
44
|
if (signature.length < 3) {
|
|
@@ -72,8 +72,8 @@ function parse(signature: string): ParsedSignature {
|
|
|
72
72
|
throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must not be empty");
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
// Validate function name: [A-Za-z_][A-Za-z0-9_]*. Every character outside ASCII carries a code
|
|
76
|
-
//
|
|
75
|
+
// Validate function name: [A-Za-z_][A-Za-z0-9_]*. Every character outside ASCII carries a code unit above the
|
|
76
|
+
// alphanumeric ranges, so the same test rejects it.
|
|
77
77
|
const firstChar = signature.charCodeAt(0);
|
|
78
78
|
if (!isAlpha(firstChar) && firstChar !== 0x5f) {
|
|
79
79
|
throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must start with a letter or underscore");
|
package/src/type-desc.ts
CHANGED
|
@@ -9,10 +9,8 @@ import { CallciumError } from "./errors";
|
|
|
9
9
|
/**
|
|
10
10
|
* Extract the staticWords count from a descriptor node.
|
|
11
11
|
*
|
|
12
|
-
* For elementary types (typeCode < STATIC_ARRAY), returns 1 unless the type is
|
|
13
|
-
*
|
|
14
|
-
* reads the 24-bit meta field at offset 1 and shifts right by
|
|
15
|
-
* META_STATIC_WORDS_SHIFT.
|
|
12
|
+
* For elementary types (typeCode < STATIC_ARRAY), returns 1 unless the type is BYTES or STRING, which are ABI-dynamic
|
|
13
|
+
* and return 0. For composite types, returns the 24-bit meta at offset 1 right-shifted by META_STATIC_WORDS_SHIFT.
|
|
16
14
|
*/
|
|
17
15
|
function extractStaticWords(desc: Uint8Array): number {
|
|
18
16
|
const typeCode = desc[0]!;
|
|
@@ -99,7 +97,8 @@ export function bytesN(length: number): Uint8Array {
|
|
|
99
97
|
*
|
|
100
98
|
* @param elemDesc - Descriptor bytes for the element type.
|
|
101
99
|
* @param length - Fixed array length. When omitted, produces a dynamic array.
|
|
102
|
-
* @throws {CallciumError} If length is 0 or
|
|
100
|
+
* @throws {CallciumError} If length is 0 or above MAX_STATIC_ARRAY_LENGTH.
|
|
101
|
+
* @throws {CallciumError} If the node exceeds MAX_NODE_LENGTH or MAX_STATIC_WORDS.
|
|
103
102
|
*/
|
|
104
103
|
export function array(elemDesc: Uint8Array, length?: number): Uint8Array {
|
|
105
104
|
if (length === undefined) {
|
|
@@ -156,7 +155,8 @@ export function array(elemDesc: Uint8Array, length?: number): Uint8Array {
|
|
|
156
155
|
* Format: `[0x90][meta:3][fieldCount:be16][field0][field1]...`
|
|
157
156
|
*
|
|
158
157
|
* @param fieldDescs - Descriptor bytes for each field, in order.
|
|
159
|
-
* @throws {CallciumError} If fieldDescs is empty
|
|
158
|
+
* @throws {CallciumError} If fieldDescs is empty or above MAX_TUPLE_FIELDS.
|
|
159
|
+
* @throws {CallciumError} If the node exceeds MAX_NODE_LENGTH or MAX_STATIC_WORDS.
|
|
160
160
|
*/
|
|
161
161
|
export function tuple(fieldDescs: Uint8Array[]): Uint8Array {
|
|
162
162
|
if (fieldDescs.length === 0 || fieldDescs.length > DF.MAX_TUPLE_FIELDS) {
|
package/src/types.ts
CHANGED
|
@@ -16,9 +16,8 @@ export type Span = { start: number; end: number };
|
|
|
16
16
|
///////////////////////////////////////////////////////////////////////////
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Execution context for context-scoped rules.
|
|
20
|
-
*
|
|
21
|
-
* Only the properties referenced by the policy need to be supplied.
|
|
19
|
+
* Execution context for context-scoped rules. Each property maps to a well-known EVM execution environment value. Only
|
|
20
|
+
* the properties referenced by the policy need to be supplied.
|
|
22
21
|
*/
|
|
23
22
|
export type Context = {
|
|
24
23
|
msgSender?: Address;
|
|
@@ -52,8 +51,8 @@ export type SelectorMismatchViolation = {
|
|
|
52
51
|
};
|
|
53
52
|
|
|
54
53
|
/**
|
|
55
|
-
* A rule referenced a context property not supplied in the execution context — a context-scoped
|
|
56
|
-
*
|
|
54
|
+
* A rule referenced a context property not supplied in the execution context — a context-scoped subject, or a context
|
|
55
|
+
* reference operand on either scope.
|
|
57
56
|
*/
|
|
58
57
|
export type MissingContextViolation = {
|
|
59
58
|
code: "MISSING_CONTEXT";
|
|
@@ -76,10 +75,8 @@ export type MissingContextViolation = {
|
|
|
76
75
|
* - `resolvedValue` present, `elementIndex` absent — scalar leaf or context value that failed the operator.
|
|
77
76
|
* - `resolvedValue` present, `elementIndex` present — universal-quantifier per-element failure.
|
|
78
77
|
* - `resolvedValue` absent, `elementIndex` absent — existential-aggregate failure (no element satisfied).
|
|
79
|
-
* - `resolvedValue` absent, `elementIndex` present — per-element failure where the leaf could not be loaded.
|
|
80
78
|
*
|
|
81
|
-
* For length
|
|
82
|
-
* rather than a 32-byte ABI word.
|
|
79
|
+
* For length operators, `resolvedValue` is the length as a 32-byte word.
|
|
83
80
|
*/
|
|
84
81
|
export type ValueMismatchViolation = {
|
|
85
82
|
code: "VALUE_MISMATCH";
|
|
@@ -95,8 +92,8 @@ export type ValueMismatchViolation = {
|
|
|
95
92
|
typeCode: number;
|
|
96
93
|
resolvedValue?: Hex;
|
|
97
94
|
/**
|
|
98
|
-
* Resolved runtime value of a reference operand as a 32-byte word; present when the
|
|
99
|
-
*
|
|
95
|
+
* Resolved runtime value of a reference operand as a 32-byte word; present when the operator's operand names a
|
|
96
|
+
* reference resolved at enforcement, not a literal.
|
|
100
97
|
*/
|
|
101
98
|
resolvedOperand?: Hex;
|
|
102
99
|
elementIndex?: number;
|
|
@@ -121,8 +118,8 @@ type CalldataNavigationVariant<C extends NavigationViolationCode> = {
|
|
|
121
118
|
/**
|
|
122
119
|
* Calldata structure prevented the rule from being evaluated.
|
|
123
120
|
*
|
|
124
|
-
* The operator was never applied; `opCode`, `operandData`, `typeCode`, and `elementIndex`
|
|
125
|
-
*
|
|
121
|
+
* The operator was never applied; `opCode`, `operandData`, `typeCode`, and `elementIndex` are diagnostic context
|
|
122
|
+
* describing the failing site, not a constraint claim.
|
|
126
123
|
*
|
|
127
124
|
* A union of per-code variants, narrowable with `Extract<Violation, { code: "..." }>`.
|
|
128
125
|
*/
|
|
@@ -173,8 +170,7 @@ export type QuantifierEmptyArrayViolation = {
|
|
|
173
170
|
/**
|
|
174
171
|
* Structured details of a single rule failure during enforcement.
|
|
175
172
|
*
|
|
176
|
-
* Carries semantic data only; no message strings.
|
|
177
|
-
* Discriminate on `code` to narrow to the matching variant.
|
|
173
|
+
* Carries semantic data only; no message strings. Discriminate on `code` to narrow to the matching variant.
|
|
178
174
|
*/
|
|
179
175
|
export type Violation =
|
|
180
176
|
| MissingSelectorViolation
|
|
@@ -277,8 +273,7 @@ export type IssueCategory = "typeMismatch" | "contradiction" | "redundancy" | "v
|
|
|
277
273
|
/**
|
|
278
274
|
* Machine-readable code for a policy validation issue.
|
|
279
275
|
*
|
|
280
|
-
* Each code names one finding of the semantic validator
|
|
281
|
-
* name in the Solidity `IssueCode` library.
|
|
276
|
+
* Each code names one finding of the semantic validator, matching the same-named Solidity `IssueCode` constant.
|
|
282
277
|
*/
|
|
283
278
|
export type IssueCode =
|
|
284
279
|
// Type mismatch.
|