@callcium/sdk 0.9.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 +54 -44
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +315 -323
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bytes.ts +9 -9
- package/src/constants.ts +36 -175
- package/src/constraint.ts +7 -32
- package/src/descriptor-coder.ts +18 -20
- package/src/descriptor.ts +12 -11
- package/src/errors.ts +0 -3
- package/src/index.ts +7 -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 +11 -41
- package/src/policy-enforcer.ts +22 -34
- package/src/policy-validator.ts +20 -23
- package/src/type-desc.ts +10 -10
- package/src/types.ts +70 -3
- package/src/validation-issue.ts +4 -4
package/src/operators.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Op, TypeCode } from "./constants";
|
|
1
|
+
import { Op, TypeCode, operandsOf } from "./constants";
|
|
2
2
|
import { CallciumError } from "./errors";
|
|
3
3
|
|
|
4
4
|
/** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
|
|
@@ -28,7 +28,7 @@ export function isLeftAligned(typeCode: number): boolean {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Canonicalize a raw 256-bit calldata word to its ABI value for the declared type
|
|
31
|
+
* Canonicalize a raw 256-bit calldata word to its ABI value for the declared type.
|
|
32
32
|
* A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
|
|
33
33
|
* unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
|
|
34
34
|
* comparison use the canonical ABI value rather than the raw bytes.
|
|
@@ -103,14 +103,14 @@ function compareTyped(a: bigint, b: bigint, typeCode: number): number {
|
|
|
103
103
|
/** Binary search over a sorted array of 32-byte words. Comparison is always unsigned. */
|
|
104
104
|
function isIn(value: bigint, data: Uint8Array): boolean {
|
|
105
105
|
const count = data.length / 32;
|
|
106
|
-
let
|
|
107
|
-
let
|
|
108
|
-
while (
|
|
109
|
-
const mid = (
|
|
106
|
+
let low = 0;
|
|
107
|
+
let high = count - 1;
|
|
108
|
+
while (low <= high) {
|
|
109
|
+
const mid = (low + high) >>> 1;
|
|
110
110
|
const elem = toBigInt(data, mid * 32);
|
|
111
111
|
if (value === elem) return true;
|
|
112
|
-
if (value < elem)
|
|
113
|
-
else
|
|
112
|
+
if (value < elem) high = mid - 1;
|
|
113
|
+
else low = mid + 1;
|
|
114
114
|
}
|
|
115
115
|
return false;
|
|
116
116
|
}
|
|
@@ -196,9 +196,114 @@ export function applyOperator(
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
default:
|
|
199
|
-
throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}
|
|
199
|
+
throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}`);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
const negate = (opCode & Op.NOT) !== 0;
|
|
203
203
|
return negate ? !result : result;
|
|
204
204
|
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check whether an operator's data payload has the correct length.
|
|
208
|
+
* @param opBase - Base operator code with the NOT flag stripped.
|
|
209
|
+
* @param dataLength - Byte length of the operator's data payload.
|
|
210
|
+
* @returns True if the data length is valid for the given operator.
|
|
211
|
+
*/
|
|
212
|
+
export function isValidOperatorData(opBase: number, dataLength: number): boolean {
|
|
213
|
+
const operands = operandsOf(opBase);
|
|
214
|
+
if (operands === "single") return dataLength === 32;
|
|
215
|
+
if (operands === "range") return dataLength === 64;
|
|
216
|
+
if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
///////////////////////////////////////////////////////////////////////////
|
|
221
|
+
// Type code classification
|
|
222
|
+
///////////////////////////////////////////////////////////////////////////
|
|
223
|
+
|
|
224
|
+
/** Structural category for a descriptor type code. */
|
|
225
|
+
export type TypeClass = "elementary" | "tuple" | "staticArray" | "dynamicArray";
|
|
226
|
+
|
|
227
|
+
/** Structural classification without label. */
|
|
228
|
+
export type TypeClassInfo = { typeClass: TypeClass; isDynamic: boolean };
|
|
229
|
+
|
|
230
|
+
/** Display metadata for a descriptor type code. */
|
|
231
|
+
export type TypeCodeInfo = TypeClassInfo & { label: string };
|
|
232
|
+
|
|
233
|
+
/** Throw an UNKNOWN_TYPE_CODE error. */
|
|
234
|
+
function unknownTypeCode(code: number): never {
|
|
235
|
+
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code 0x${code.toString(16).padStart(2, "0")}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Pre-allocated constant objects for fixed type codes (avoids per-call allocation).
|
|
239
|
+
const ELEMENTARY = { typeClass: "elementary", isDynamic: false } as const;
|
|
240
|
+
const ELEMENTARY_DYNAMIC = { typeClass: "elementary", isDynamic: true } as const;
|
|
241
|
+
const STATIC_ARR = { typeClass: "staticArray", isDynamic: false } as const;
|
|
242
|
+
const DYNAMIC_ARR = { typeClass: "dynamicArray", isDynamic: true } as const;
|
|
243
|
+
const TUPLE = { typeClass: "tuple", isDynamic: false } as const;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Classify a type code into its structural category and dynamism.
|
|
247
|
+
* Lightweight variant of `lookupTypeCode` that skips label computation.
|
|
248
|
+
* @param code - A single-byte descriptor type code.
|
|
249
|
+
* @returns Type class and whether the type is ABI-dynamic.
|
|
250
|
+
* @throws {CallciumError} If the code is not a recognised type code.
|
|
251
|
+
*/
|
|
252
|
+
export function classifyTypeCode(code: number): TypeClassInfo {
|
|
253
|
+
if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return ELEMENTARY;
|
|
254
|
+
if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return ELEMENTARY;
|
|
255
|
+
if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
|
|
256
|
+
if (code >= 0x44 && code <= 0x4f) unknownTypeCode(code);
|
|
257
|
+
if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
|
|
258
|
+
if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYNAMIC;
|
|
259
|
+
if (code >= 0x72 && code <= 0x7f) unknownTypeCode(code);
|
|
260
|
+
if (code === TypeCode.STATIC_ARRAY) return STATIC_ARR;
|
|
261
|
+
if (code === TypeCode.DYNAMIC_ARRAY) return DYNAMIC_ARR;
|
|
262
|
+
if (code >= 0x82 && code <= 0x8f) unknownTypeCode(code);
|
|
263
|
+
if (code === TypeCode.TUPLE) return TUPLE;
|
|
264
|
+
unknownTypeCode(code);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Determine whether a type code names a value an operator can read: a scalar word or a declared
|
|
269
|
+
* length. Tuples, static arrays, and undefined codes address nothing.
|
|
270
|
+
* @param code - A single-byte descriptor type code.
|
|
271
|
+
* @returns True when an operator can be applied to a target of this type.
|
|
272
|
+
*/
|
|
273
|
+
export function isAddressableTarget(code: number): boolean {
|
|
274
|
+
let info: TypeClassInfo;
|
|
275
|
+
try {
|
|
276
|
+
// An undefined code is reported by a throw, and addresses nothing either way.
|
|
277
|
+
info = classifyTypeCode(code);
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
return info.typeClass === "elementary" || info.typeClass === "dynamicArray";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Compute the ABI type label for a type code. */
|
|
285
|
+
function typeCodeLabel(code: number): string {
|
|
286
|
+
if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return `uint${(code - TypeCode.UINT_MIN + 1) * 8}`;
|
|
287
|
+
if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return `int${(code - TypeCode.INT_MIN + 1) * 8}`;
|
|
288
|
+
if (code === TypeCode.ADDRESS) return "address";
|
|
289
|
+
if (code === TypeCode.BOOL) return "bool";
|
|
290
|
+
if (code === TypeCode.FUNCTION) return "function";
|
|
291
|
+
if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX)
|
|
292
|
+
return `bytes${code - TypeCode.FIXED_BYTES_MIN + 1}`;
|
|
293
|
+
if (code === TypeCode.BYTES) return "bytes";
|
|
294
|
+
if (code === TypeCode.STRING) return "string";
|
|
295
|
+
if (code === TypeCode.STATIC_ARRAY) return "T[k]";
|
|
296
|
+
if (code === TypeCode.DYNAMIC_ARRAY) return "T[]";
|
|
297
|
+
if (code === TypeCode.TUPLE) return "tuple";
|
|
298
|
+
return `0x${code.toString(16).padStart(2, "0")}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Map a raw type code byte to its ABI type label, structural category, and dynamism.
|
|
303
|
+
* @param code - A single-byte descriptor type code.
|
|
304
|
+
* @returns Label, type class, and whether the type is ABI-dynamic.
|
|
305
|
+
* @throws {CallciumError} If the code is not a recognised type code.
|
|
306
|
+
*/
|
|
307
|
+
export function lookupTypeCode(code: number): TypeCodeInfo {
|
|
308
|
+
return { label: typeCodeLabel(code), ...classifyTypeCode(code) };
|
|
309
|
+
}
|
package/src/path.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { bytesToHex, hexToBytes, readU16, writeBE16 } from "./bytes";
|
|
2
|
+
import { buildCodeMap, PolicyFormat as PF } from "./constants";
|
|
3
|
+
import { CallciumError } from "./errors";
|
|
4
|
+
|
|
5
|
+
import type { Hex } from "./types";
|
|
6
|
+
|
|
7
|
+
///////////////////////////////////////////////////////////////////////////
|
|
8
|
+
// Quantifier steps
|
|
9
|
+
///////////////////////////////////////////////////////////////////////////
|
|
10
|
+
|
|
11
|
+
const QUANTIFIER_TABLE = [
|
|
12
|
+
{ key: "ALL", code: 0xffff, label: "all" },
|
|
13
|
+
{ key: "ANY", code: 0xfffe, label: "any" },
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
/** Reserved path step values that trigger quantified evaluation over array elements. */
|
|
17
|
+
export const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
|
|
18
|
+
|
|
19
|
+
/** Display metadata for a quantifier step. */
|
|
20
|
+
export type QuantifierInfo = { label: string };
|
|
21
|
+
|
|
22
|
+
const quantifierByCode: ReadonlyMap<number, QuantifierInfo> = new Map<number, QuantifierInfo>(
|
|
23
|
+
QUANTIFIER_TABLE.map((e) => [e.code, { label: e.label }]),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/** Check whether a path step is a quantifier (ALL or ANY). */
|
|
27
|
+
export function isQuantifier(step: number): boolean {
|
|
28
|
+
return step >= Quantifier.ANY;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Map a quantifier path step to its display label.
|
|
33
|
+
* @param code - Quantifier step value.
|
|
34
|
+
* @returns Display metadata for the quantifier.
|
|
35
|
+
* @throws {CallciumError} If the code is not a recognised quantifier.
|
|
36
|
+
*/
|
|
37
|
+
export function lookupQuantifier(code: number): QuantifierInfo {
|
|
38
|
+
const info = quantifierByCode.get(code);
|
|
39
|
+
if (!info) throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
|
|
40
|
+
return info;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
///////////////////////////////////////////////////////////////////////////
|
|
44
|
+
// Path steps
|
|
45
|
+
///////////////////////////////////////////////////////////////////////////
|
|
46
|
+
|
|
47
|
+
/** Encode a sequence of uint16 path steps as a big-endian hex string. */
|
|
48
|
+
export function encodePath(steps: readonly number[]): Hex {
|
|
49
|
+
const buffer = new Uint8Array(steps.length * 2);
|
|
50
|
+
for (let i = 0; i < steps.length; i++) {
|
|
51
|
+
writeBE16(buffer, i * 2, steps[i]!);
|
|
52
|
+
}
|
|
53
|
+
return bytesToHex(buffer);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse a BE16-encoded hex path into an array of step values.
|
|
58
|
+
* @param path - 0x-prefixed hex string containing BE16-encoded path steps.
|
|
59
|
+
* @returns Array of numeric step values.
|
|
60
|
+
*/
|
|
61
|
+
export function parsePathSteps(path: Hex): number[] {
|
|
62
|
+
const bytes = hexToBytes(path);
|
|
63
|
+
if (bytes.length % PF.PATH_STEP_SIZE !== 0) {
|
|
64
|
+
throw new CallciumError("MALFORMED_PATH", `Path byte length ${bytes.length} is not a whole number of steps`);
|
|
65
|
+
}
|
|
66
|
+
const steps: number[] = [];
|
|
67
|
+
for (let offset = 0; offset < bytes.length; offset += PF.PATH_STEP_SIZE) {
|
|
68
|
+
steps.push(readU16(bytes, offset));
|
|
69
|
+
}
|
|
70
|
+
return steps;
|
|
71
|
+
}
|
package/src/policy-builder.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { bytesToHex } from "./bytes";
|
|
2
|
-
import { Scope, TypeCode,
|
|
2
|
+
import { Scope, TypeCode, MAX_CONTEXT_PROPERTY_ID } from "./constants";
|
|
3
3
|
import { ConstraintBuilder } from "./constraint";
|
|
4
4
|
import { Descriptor } from "./descriptor";
|
|
5
5
|
import { DescriptorCoder } from "./descriptor-coder";
|
|
6
6
|
import { CallciumError, ValidationError } from "./errors";
|
|
7
|
-
import {
|
|
7
|
+
import { isQuantifier, parsePathSteps } from "./path";
|
|
8
|
+
import { PolicyCoder } from "./policy-coder";
|
|
8
9
|
import { PolicyValidator } from "./policy-validator";
|
|
9
10
|
import { SignatureParser } from "./signature";
|
|
10
11
|
|
|
@@ -29,13 +30,13 @@ type PolicyDraft = {
|
|
|
29
30
|
/** Validate a context-scope path. */
|
|
30
31
|
function validateContextPath(steps: number[]): void {
|
|
31
32
|
if (steps.length !== 1) {
|
|
32
|
-
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step
|
|
33
|
+
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step");
|
|
33
34
|
}
|
|
34
35
|
const step = steps[0]!;
|
|
35
36
|
if (step > MAX_CONTEXT_PROPERTY_ID) {
|
|
36
37
|
throw new CallciumError(
|
|
37
38
|
"UNKNOWN_CONTEXT_PROPERTY",
|
|
38
|
-
`Unknown context property ID 0x${step.toString(16).padStart(4, "0")}
|
|
39
|
+
`Unknown context property ID 0x${step.toString(16).padStart(4, "0")}`,
|
|
39
40
|
);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -47,7 +48,7 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
|
47
48
|
if (argIndex >= paramCount) {
|
|
48
49
|
throw new CallciumError(
|
|
49
50
|
"PARAM_INDEX_OUT_OF_BOUNDS",
|
|
50
|
-
`Param index ${argIndex} out of range (descriptor has ${paramCount} params)
|
|
51
|
+
`Param index ${argIndex} out of range (descriptor has ${paramCount} params)`,
|
|
51
52
|
);
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -61,10 +62,10 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
|
61
62
|
|
|
62
63
|
if (isQuantifier(step)) {
|
|
63
64
|
if (!isArray) {
|
|
64
|
-
throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node
|
|
65
|
+
throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node");
|
|
65
66
|
}
|
|
66
67
|
if (hasQuantifier) {
|
|
67
|
-
throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed
|
|
68
|
+
throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed");
|
|
68
69
|
}
|
|
69
70
|
hasQuantifier = true;
|
|
70
71
|
}
|
|
@@ -74,7 +75,7 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
|
74
75
|
if (step >= fieldCount) {
|
|
75
76
|
throw new CallciumError(
|
|
76
77
|
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
77
|
-
`Tuple field index ${step} out of range (tuple has ${fieldCount} fields)
|
|
78
|
+
`Tuple field index ${step} out of range (tuple has ${fieldCount} fields)`,
|
|
78
79
|
);
|
|
79
80
|
}
|
|
80
81
|
offset = Descriptor.tupleFieldOffset(desc, offset, step);
|
|
@@ -84,13 +85,13 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
|
84
85
|
if (step >= arrayLength) {
|
|
85
86
|
throw new CallciumError(
|
|
86
87
|
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
87
|
-
`Array index ${step} out of range (static array has ${arrayLength} elements)
|
|
88
|
+
`Array index ${step} out of range (static array has ${arrayLength} elements)`,
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
offset = Descriptor.arrayElementOffset(offset);
|
|
92
93
|
} else {
|
|
93
|
-
throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type
|
|
94
|
+
throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type");
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
}
|
|
@@ -150,13 +151,13 @@ export class PolicyBuilder {
|
|
|
150
151
|
};
|
|
151
152
|
|
|
152
153
|
if (c.operators.length === 0) {
|
|
153
|
-
throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator
|
|
154
|
+
throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator");
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
// Path shape is established before the scope decides how to navigate it.
|
|
157
158
|
const steps = parsePathSteps(c.path);
|
|
158
159
|
if (steps.length === 0) {
|
|
159
|
-
throw new CallciumError("EMPTY_PATH", "Path must have at least one step
|
|
160
|
+
throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
if (c.scope === Scope.CONTEXT) {
|
|
@@ -164,13 +165,13 @@ export class PolicyBuilder {
|
|
|
164
165
|
} else if (c.scope === Scope.CALLDATA) {
|
|
165
166
|
validateCalldataPath(steps, this.draft.descriptor);
|
|
166
167
|
} else {
|
|
167
|
-
throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${c.scope}
|
|
168
|
+
throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${c.scope}`);
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
const key = `${c.scope}:${c.path.toLowerCase()}`;
|
|
171
172
|
const currentHashes = this.draft.pathHashes[this.draft.pathHashes.length - 1]!;
|
|
172
173
|
if (currentHashes.has(key)) {
|
|
173
|
-
throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${c.path} in the same group
|
|
174
|
+
throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${c.path} in the same group`);
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
currentHashes.add(key);
|
|
@@ -182,7 +183,7 @@ export class PolicyBuilder {
|
|
|
182
183
|
or(): this {
|
|
183
184
|
const lastGroup = this.draft.groups[this.draft.groups.length - 1]!;
|
|
184
185
|
if (lastGroup.length === 0) {
|
|
185
|
-
throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty
|
|
186
|
+
throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty");
|
|
186
187
|
}
|
|
187
188
|
this.draft.groups.push([]);
|
|
188
189
|
this.draft.pathHashes.push(new Set());
|
|
@@ -233,7 +234,7 @@ export class PolicyBuilder {
|
|
|
233
234
|
private checkGroups(): void {
|
|
234
235
|
for (let i = 0; i < this.draft.groups.length; i++) {
|
|
235
236
|
if (this.draft.groups[i]!.length === 0) {
|
|
236
|
-
throw new CallciumError("EMPTY_GROUP", `Group ${i} is empty
|
|
237
|
+
throw new CallciumError("EMPTY_GROUP", `Group ${i} is empty`);
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
}
|
package/src/policy-coder.ts
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
2
2
|
|
|
3
3
|
import { bytesToHex, hexToBytes, toHex, readU16, readU32, writeBE16, writeBE32 } from "./bytes";
|
|
4
|
-
import {
|
|
5
|
-
DescriptorFormat as DF,
|
|
6
|
-
PolicyFormat as PF,
|
|
7
|
-
Scope,
|
|
8
|
-
Limits,
|
|
9
|
-
Op,
|
|
10
|
-
TypeCode,
|
|
11
|
-
isValidOperatorData,
|
|
12
|
-
isAddressableTarget,
|
|
13
|
-
MAX_CONTEXT_PROPERTY_ID,
|
|
14
|
-
} from "./constants";
|
|
4
|
+
import { DescriptorFormat as DF, PolicyFormat as PF, Scope, Op, TypeCode, MAX_CONTEXT_PROPERTY_ID } from "./constants";
|
|
15
5
|
import { Descriptor } from "./descriptor";
|
|
16
6
|
import { decodeDescriptor } from "./descriptor-coder";
|
|
17
7
|
import { CallciumError } from "./errors";
|
|
18
|
-
import { isLengthOp, isLengthValidType, toBigInt } from "./operators";
|
|
8
|
+
import { isLengthOp, isLengthValidType, toBigInt, isAddressableTarget, isValidOperatorData } from "./operators";
|
|
9
|
+
import { parsePathSteps } from "./path";
|
|
19
10
|
|
|
20
11
|
import type {
|
|
21
12
|
Constraint,
|
|
@@ -28,27 +19,6 @@ import type {
|
|
|
28
19
|
PolicyData,
|
|
29
20
|
} from "./types";
|
|
30
21
|
|
|
31
|
-
///////////////////////////////////////////////////////////////////////////
|
|
32
|
-
// Path helper
|
|
33
|
-
///////////////////////////////////////////////////////////////////////////
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Parse a BE16-encoded hex path into an array of step values.
|
|
37
|
-
* @param path - 0x-prefixed hex string containing BE16-encoded path steps.
|
|
38
|
-
* @returns Array of numeric step values.
|
|
39
|
-
*/
|
|
40
|
-
export function parsePathSteps(path: Hex): number[] {
|
|
41
|
-
const bytes = hexToBytes(path);
|
|
42
|
-
if (bytes.length % PF.PATH_STEP_SIZE !== 0) {
|
|
43
|
-
throw new CallciumError("MALFORMED_PATH", `Path byte length ${bytes.length} is not a whole number of steps`);
|
|
44
|
-
}
|
|
45
|
-
const steps: number[] = [];
|
|
46
|
-
for (let offset = 0; offset < bytes.length; offset += PF.PATH_STEP_SIZE) {
|
|
47
|
-
steps.push(readU16(bytes, offset));
|
|
48
|
-
}
|
|
49
|
-
return steps;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
22
|
///////////////////////////////////////////////////////////////////////////
|
|
53
23
|
// Policy field helper
|
|
54
24
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -336,10 +306,10 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
336
306
|
if (depthValue === 0) {
|
|
337
307
|
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
|
|
338
308
|
}
|
|
339
|
-
if (depthValue >
|
|
309
|
+
if (depthValue > PF.MAX_PATH_DEPTH) {
|
|
340
310
|
throw new CallciumError(
|
|
341
311
|
"PATH_TOO_DEEP",
|
|
342
|
-
`Path depth ${depthValue} exceeds maximum ${
|
|
312
|
+
`Path depth ${depthValue} exceeds maximum ${PF.MAX_PATH_DEPTH}`,
|
|
343
313
|
ruleOffset,
|
|
344
314
|
);
|
|
345
315
|
}
|
|
@@ -430,7 +400,7 @@ function flattenConstraint(constraint: Constraint, desc: Uint8Array): Rule[] {
|
|
|
430
400
|
const operators = constraint.operators.map(hexToBytes);
|
|
431
401
|
for (const operator of operators) {
|
|
432
402
|
if (operator.length < 1) {
|
|
433
|
-
throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode)
|
|
403
|
+
throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode)");
|
|
434
404
|
}
|
|
435
405
|
}
|
|
436
406
|
|
|
@@ -438,14 +408,14 @@ function flattenConstraint(constraint: Constraint, desc: Uint8Array): Rule[] {
|
|
|
438
408
|
|
|
439
409
|
// Path shape checks precede compilation: the compiler assumes a framed, depth-bounded path.
|
|
440
410
|
if (path.length === 0) {
|
|
441
|
-
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step
|
|
411
|
+
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step");
|
|
442
412
|
}
|
|
443
413
|
if ((path.length & 1) !== 0) {
|
|
444
|
-
throw new CallciumError("MALFORMED_PATH", "Path byte length must be even
|
|
414
|
+
throw new CallciumError("MALFORMED_PATH", "Path byte length must be even");
|
|
445
415
|
}
|
|
446
416
|
const depth = path.length / 2;
|
|
447
|
-
if (depth >
|
|
448
|
-
throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${
|
|
417
|
+
if (depth > PF.MAX_PATH_DEPTH) {
|
|
418
|
+
throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${PF.MAX_PATH_DEPTH}`);
|
|
449
419
|
}
|
|
450
420
|
|
|
451
421
|
const hint =
|
|
@@ -484,7 +454,7 @@ function sortRules(rules: Rule[]): void {
|
|
|
484
454
|
function encodeRule(rule: Rule): Uint8Array {
|
|
485
455
|
const depth = rule.path.length / 2;
|
|
486
456
|
if (rule.scope === Scope.CONTEXT && depth !== 1) {
|
|
487
|
-
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step
|
|
457
|
+
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step");
|
|
488
458
|
}
|
|
489
459
|
const data = rule.operator.subarray(1);
|
|
490
460
|
const ruleSize = PF.RULE_FIXED_OVERHEAD + rule.path.length + rule.hint.length + data.length;
|
package/src/policy-enforcer.ts
CHANGED
|
@@ -1,17 +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
|
-
Op,
|
|
9
|
-
TypeCode,
|
|
10
|
-
classifyTypeCode,
|
|
11
|
-
lookupContextProperty,
|
|
12
|
-
} from "./constants";
|
|
3
|
+
import { PolicyFormat as PF, Scope, MAX_CONTEXT_PROPERTY_ID, Op, TypeCode, lookupContextProperty } from "./constants";
|
|
13
4
|
import { CallciumError, PolicyViolationError } from "./errors";
|
|
14
|
-
import { applyOperator, toBigInt, isLengthOp, isLengthValidType, canonicalize } from "./operators";
|
|
5
|
+
import { applyOperator, toBigInt, isLengthOp, isLengthValidType, canonicalize, classifyTypeCode } from "./operators";
|
|
15
6
|
import { decodePolicy } from "./policy-coder";
|
|
16
7
|
|
|
17
8
|
import type { ReadResult } from "./calldata-reader";
|
|
@@ -30,20 +21,17 @@ import type {
|
|
|
30
21
|
// Helpers
|
|
31
22
|
///////////////////////////////////////////////////////////////////////////
|
|
32
23
|
|
|
33
|
-
/** Violations that abort evaluation instead of failing only their group
|
|
34
|
-
const
|
|
24
|
+
/** Violations that abort evaluation instead of failing only their group. */
|
|
25
|
+
const abortViolationCodes: ReadonlySet<ViolationCode> = new Set<ViolationCode>([
|
|
35
26
|
"CALLDATA_OUT_OF_BOUNDS",
|
|
36
27
|
"ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
37
28
|
"NON_CANONICAL_VALUE",
|
|
38
29
|
"QUANTIFIER_LIMIT_EXCEEDED",
|
|
39
30
|
]);
|
|
40
31
|
|
|
41
|
-
/**
|
|
32
|
+
/** Validate a hex address and read it as a 256-bit bigint. */
|
|
42
33
|
function addressToBigInt(hex: string): bigint {
|
|
43
|
-
|
|
44
|
-
const padded = new Uint8Array(32);
|
|
45
|
-
padded.set(bytes, 32 - bytes.length);
|
|
46
|
-
return toBigInt(padded, 0);
|
|
34
|
+
return BigInt(toAddress(hex));
|
|
47
35
|
}
|
|
48
36
|
|
|
49
37
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -100,8 +88,8 @@ function check(policy: Hex, callData: Hex, context?: Context): EnforceResult {
|
|
|
100
88
|
const violation = evaluateRule(rule, policyBytes, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
|
|
101
89
|
if (violation !== null) {
|
|
102
90
|
allViolations.push(violation);
|
|
103
|
-
if (
|
|
104
|
-
// 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.
|
|
105
93
|
return { ok: false, violations: allViolations };
|
|
106
94
|
}
|
|
107
95
|
groupFailed = true;
|
|
@@ -182,9 +170,9 @@ function evaluateRule(
|
|
|
182
170
|
* Returns the missing property's type code when the context does not supply it.
|
|
183
171
|
*/
|
|
184
172
|
function resolveContextOperand(operandData: Uint8Array, context?: Context): bigint | { ctxTypeCode: number } {
|
|
185
|
-
const
|
|
186
|
-
const value = context?.[
|
|
187
|
-
if (value === undefined) return { ctxTypeCode:
|
|
173
|
+
const propertyInfo = lookupContextProperty(Number(toBigInt(operandData, 0)));
|
|
174
|
+
const value = context?.[propertyInfo.contextKey];
|
|
175
|
+
if (value === undefined) return { ctxTypeCode: propertyInfo.typeCode };
|
|
188
176
|
return typeof value === "string" ? addressToBigInt(value) : value;
|
|
189
177
|
}
|
|
190
178
|
|
|
@@ -285,7 +273,7 @@ function evalTarget(
|
|
|
285
273
|
// A dynamic target's chain ends at its payload, so the word there is the declared length.
|
|
286
274
|
if (isLengthValidType(typeCode)) {
|
|
287
275
|
if (!isLengthOp(opCode)) {
|
|
288
|
-
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");
|
|
289
277
|
}
|
|
290
278
|
const length = readPointer(callData, target);
|
|
291
279
|
if (typeof length !== "number") return { error: length.code };
|
|
@@ -300,7 +288,7 @@ function evalTarget(
|
|
|
300
288
|
}
|
|
301
289
|
|
|
302
290
|
if (classifyTypeCode(typeCode).typeClass !== "elementary") {
|
|
303
|
-
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");
|
|
304
292
|
}
|
|
305
293
|
const word = loadWord(callData, target);
|
|
306
294
|
if (!(word instanceof Uint8Array)) return { error: word.code };
|
|
@@ -500,7 +488,7 @@ function evaluateQuantified(
|
|
|
500
488
|
elems += 32;
|
|
501
489
|
}
|
|
502
490
|
|
|
503
|
-
if (count >
|
|
491
|
+
if (count > PF.MAX_QUANTIFIED_ARRAY_LENGTH) {
|
|
504
492
|
return {
|
|
505
493
|
group: groupIndex,
|
|
506
494
|
rule: ruleIndex,
|
|
@@ -535,7 +523,7 @@ function evaluateQuantified(
|
|
|
535
523
|
if (elemIsDynamic) {
|
|
536
524
|
const followed = follow(callDataBytes, elems, slot);
|
|
537
525
|
// Navigation failures abort under every quantifier: calldata the enforcer cannot read is
|
|
538
|
-
// not an element that merely fails the operator
|
|
526
|
+
// not an element that merely fails the operator.
|
|
539
527
|
if (typeof followed !== "number") return navigationViolation(frame, followed.code, elemIndex);
|
|
540
528
|
elem = followed;
|
|
541
529
|
}
|
|
@@ -550,7 +538,7 @@ function evaluateQuantified(
|
|
|
550
538
|
const applied = evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData, context);
|
|
551
539
|
if ("error" in applied) {
|
|
552
540
|
// Error results end the rule whatever the quantifier: a later element cannot rescue
|
|
553
|
-
// calldata the enforcer cannot read (abort
|
|
541
|
+
// calldata the enforcer cannot read (an abort effect), and a missing context
|
|
554
542
|
// property is missing for every element alike (group-local).
|
|
555
543
|
return targetViolation(frame, applied, elemIndex);
|
|
556
544
|
}
|
|
@@ -602,8 +590,8 @@ function evaluateContextRule(
|
|
|
602
590
|
);
|
|
603
591
|
}
|
|
604
592
|
|
|
605
|
-
const
|
|
606
|
-
const contextValue = context?.[
|
|
593
|
+
const propertyInfo = lookupContextProperty(propertyId);
|
|
594
|
+
const contextValue = context?.[propertyInfo.contextKey];
|
|
607
595
|
|
|
608
596
|
if (contextValue === undefined) {
|
|
609
597
|
return {
|
|
@@ -612,7 +600,7 @@ function evaluateContextRule(
|
|
|
612
600
|
code: "MISSING_CONTEXT",
|
|
613
601
|
scope: Scope.CONTEXT,
|
|
614
602
|
path: pathHex,
|
|
615
|
-
typeCode:
|
|
603
|
+
typeCode: propertyInfo.typeCode,
|
|
616
604
|
};
|
|
617
605
|
}
|
|
618
606
|
|
|
@@ -642,7 +630,7 @@ function evaluateContextRule(
|
|
|
642
630
|
result = (value === operand) !== ((opCode & Op.NOT) !== 0);
|
|
643
631
|
ctxOperand = operand;
|
|
644
632
|
} else {
|
|
645
|
-
result = applyOperator(opCode, value, 32, operandData,
|
|
633
|
+
result = applyOperator(opCode, value, 32, operandData, propertyInfo.typeCode);
|
|
646
634
|
}
|
|
647
635
|
|
|
648
636
|
if (!result) {
|
|
@@ -654,7 +642,7 @@ function evaluateContextRule(
|
|
|
654
642
|
path: pathHex,
|
|
655
643
|
opCode,
|
|
656
644
|
operandData: bytesToHex(operandData),
|
|
657
|
-
typeCode:
|
|
645
|
+
typeCode: propertyInfo.typeCode,
|
|
658
646
|
resolvedValue: bigintToHex(value),
|
|
659
647
|
...(ctxOperand !== undefined && { resolvedOperand: bigintToHex(ctxOperand) }),
|
|
660
648
|
};
|