@callcium/sdk 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +150 -103
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +683 -579
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/calldata-reader.ts +14 -1
- package/src/constants.ts +54 -11
- package/src/constraint.ts +131 -69
- package/src/descriptor-coder.ts +37 -66
- package/src/descriptor.ts +54 -26
- package/src/errors.ts +12 -2
- package/src/index.ts +1 -0
- package/src/operators.ts +36 -5
- package/src/path.ts +15 -6
- package/src/policy-builder.ts +94 -32
- package/src/policy-coder.ts +100 -45
- package/src/policy-enforcer.ts +85 -103
- package/src/policy-validator.ts +52 -35
- package/src/signature.ts +21 -19
- package/src/type-desc.ts +6 -6
package/src/descriptor.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readU16, readU24, writeBE16, writeBE32 } from "./bytes";
|
|
2
2
|
import { DescriptorFormat as DF, PolicyFormat as PF, TypeCode } from "./constants";
|
|
3
3
|
import { CallciumError } from "./errors";
|
|
4
|
+
import { classifyTypeCode } from "./operators";
|
|
4
5
|
import { Quantifier, isQuantifier } from "./path";
|
|
5
6
|
|
|
6
7
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -41,15 +42,51 @@ type HintWalk = {
|
|
|
41
42
|
// Internal helpers
|
|
42
43
|
///////////////////////////////////////////////////////////////////////////
|
|
43
44
|
|
|
45
|
+
/** A descriptor node's type code, static word count, and total byte length. */
|
|
46
|
+
type Node = { typeCode: number; staticWords: number; nodeLength: number };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read the node at `offset`.
|
|
50
|
+
* Elementary nodes occupy exactly one byte; composite nodes encode their static
|
|
51
|
+
* word count and length in the 24-bit meta field that follows the type code.
|
|
52
|
+
* @throws {CallciumError} When the node starts past the descriptor, carries an undefined type code,
|
|
53
|
+
* or declares a length below its own header or beyond the descriptor.
|
|
54
|
+
*/
|
|
55
|
+
export function readNode(desc: Uint8Array, offset: number): Node {
|
|
56
|
+
if (offset >= desc.length) {
|
|
57
|
+
throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
|
|
58
|
+
}
|
|
59
|
+
const typeCode = desc[offset]!;
|
|
60
|
+
const info = classifyTypeCode(typeCode);
|
|
61
|
+
if (info.typeClass === "elementary") {
|
|
62
|
+
return { typeCode, staticWords: info.isDynamic ? 0 : 1, nodeLength: DF.TYPECODE_SIZE };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (offset + DF.TYPECODE_SIZE + DF.COMPOSITE_META_SIZE > desc.length) {
|
|
66
|
+
throw new CallciumError("UNEXPECTED_END", "Incomplete composite metadata", offset);
|
|
67
|
+
}
|
|
68
|
+
const meta = readU24(desc, offset + DF.TYPECODE_SIZE);
|
|
69
|
+
const length = meta & DF.META_NODE_LENGTH_MASK;
|
|
70
|
+
const minHeader = info.typeClass === "tuple" ? DF.TUPLE_HEADER_SIZE : DF.ARRAY_HEADER_SIZE;
|
|
71
|
+
if (length < minHeader) {
|
|
72
|
+
throw new CallciumError(
|
|
73
|
+
"NODE_LENGTH_TOO_SMALL",
|
|
74
|
+
`Composite node length ${length} is smaller than minimum header ${minHeader}`,
|
|
75
|
+
offset,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (offset + length > desc.length) {
|
|
79
|
+
throw new CallciumError("NODE_OVERFLOW", "Composite node extends beyond descriptor", offset);
|
|
80
|
+
}
|
|
81
|
+
return { typeCode, staticWords: meta >> DF.META_STATIC_WORDS_SHIFT, nodeLength: length };
|
|
82
|
+
}
|
|
83
|
+
|
|
44
84
|
/**
|
|
45
85
|
* Return the byte length of the node at `offset`.
|
|
46
|
-
*
|
|
47
|
-
* length in the lower 12 bits of the 24-bit meta field.
|
|
86
|
+
* @throws {CallciumError} When the descriptor holds no readable node at `offset`.
|
|
48
87
|
*/
|
|
49
88
|
function nodeLength(desc: Uint8Array, offset: number): number {
|
|
50
|
-
|
|
51
|
-
if (typeCode < TypeCode.STATIC_ARRAY) return DF.TYPECODE_SIZE;
|
|
52
|
-
return readU24(desc, offset + 1) & DF.META_NODE_LENGTH_MASK;
|
|
89
|
+
return readNode(desc, offset).nodeLength;
|
|
53
90
|
}
|
|
54
91
|
|
|
55
92
|
/**
|
|
@@ -68,14 +105,20 @@ function tupleFieldOffset(desc: Uint8Array, tupleOffset: number, fieldIndex: num
|
|
|
68
105
|
// Public interface
|
|
69
106
|
///////////////////////////////////////////////////////////////////////////
|
|
70
107
|
|
|
71
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Return the declared top-level parameter count.
|
|
110
|
+
* @throws {CallciumError} When the descriptor is too short to hold a header.
|
|
111
|
+
*/
|
|
72
112
|
function paramCount(desc: Uint8Array): number {
|
|
113
|
+
if (desc.length < DF.HEADER_SIZE) {
|
|
114
|
+
throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
|
|
115
|
+
}
|
|
73
116
|
return desc[1]!;
|
|
74
117
|
}
|
|
75
118
|
|
|
76
119
|
/** Return byte offset of the N-th top-level param (0-indexed). */
|
|
77
120
|
function paramOffset(desc: Uint8Array, index: number): number {
|
|
78
|
-
const count = desc
|
|
121
|
+
const count = paramCount(desc);
|
|
79
122
|
if (index >= count) {
|
|
80
123
|
throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count})`);
|
|
81
124
|
}
|
|
@@ -91,17 +134,11 @@ function paramOffset(desc: Uint8Array, index: number): number {
|
|
|
91
134
|
* @param desc - Raw descriptor bytes.
|
|
92
135
|
* @param offset - Byte position of the node.
|
|
93
136
|
* @returns typeCode, isDynamic, and staticSize in bytes.
|
|
137
|
+
* @throws {CallciumError} When the descriptor holds no readable node at `offset`.
|
|
94
138
|
*/
|
|
95
139
|
function inspect(desc: Uint8Array, offset: number): TypeInfo {
|
|
96
|
-
const typeCode = desc
|
|
97
|
-
|
|
98
|
-
if (typeCode < TypeCode.STATIC_ARRAY) {
|
|
99
|
-
staticWords = typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING ? 0 : 1;
|
|
100
|
-
} else {
|
|
101
|
-
staticWords = readU24(desc, offset + 1) >> DF.META_STATIC_WORDS_SHIFT;
|
|
102
|
-
}
|
|
103
|
-
const isDynamic = staticWords === 0;
|
|
104
|
-
return { typeCode, isDynamic, staticSize: staticWords * 32 };
|
|
140
|
+
const { typeCode, staticWords } = readNode(desc, offset);
|
|
141
|
+
return { typeCode, isDynamic: staticWords === 0, staticSize: staticWords * 32 };
|
|
105
142
|
}
|
|
106
143
|
|
|
107
144
|
/**
|
|
@@ -134,16 +171,7 @@ function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quan
|
|
|
134
171
|
throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
135
172
|
}
|
|
136
173
|
|
|
137
|
-
|
|
138
|
-
const count = desc[1]!;
|
|
139
|
-
if (paramIndex >= count) {
|
|
140
|
-
throw new CallciumError(
|
|
141
|
-
"PARAM_INDEX_OUT_OF_BOUNDS",
|
|
142
|
-
`Param index ${paramIndex} out of range (paramCount=${count})`,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
let cursor = paramOffset(desc, paramIndex);
|
|
174
|
+
let cursor = paramOffset(desc, steps[0]!);
|
|
147
175
|
let quantifiedStaticLength = 0;
|
|
148
176
|
|
|
149
177
|
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
|
package/src/errors.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Machine-readable error code for structural decoding and validation failures.
|
|
3
3
|
*
|
|
4
|
-
* Each code names one invariant
|
|
5
|
-
*
|
|
4
|
+
* Each code names one invariant. Where Solidity detects the same invariant, the code is its error
|
|
5
|
+
* name transliterated from `PascalCase` to `SCREAMING_SNAKE_CASE`. The codes under "Inputs and
|
|
6
|
+
* validation" have no counterpart: they name input a Solidity signature cannot express.
|
|
7
|
+
* `OUT_OF_PHYSICAL_BOUNDS` names an invariant Solidity carries twice, as the error
|
|
8
|
+
* `PolicyBuilder.OutOfPhysicalBounds` and as an issue code, so the name is shared with `IssueCode`.
|
|
6
9
|
*/
|
|
7
10
|
export type CallciumErrorCode =
|
|
8
11
|
// Policy and descriptor framing.
|
|
@@ -47,6 +50,7 @@ export type CallciumErrorCode =
|
|
|
47
50
|
| "EMPTY_SET"
|
|
48
51
|
| "SET_TOO_LARGE"
|
|
49
52
|
| "INVALID_RANGE"
|
|
53
|
+
| "OUT_OF_PHYSICAL_BOUNDS"
|
|
50
54
|
// Descriptor nodes.
|
|
51
55
|
| "UNKNOWN_TYPE_CODE"
|
|
52
56
|
| "NODE_LENGTH_TOO_SMALL"
|
|
@@ -72,6 +76,10 @@ export type CallciumErrorCode =
|
|
|
72
76
|
// Inputs and validation.
|
|
73
77
|
| "UNKNOWN_QUANTIFIER"
|
|
74
78
|
| "INVALID_HEX"
|
|
79
|
+
| "MALFORMED_SELECTOR"
|
|
80
|
+
| "PATH_STEP_OVERFLOW"
|
|
81
|
+
| "OPERAND_OVERFLOW"
|
|
82
|
+
| "CONTEXT_VALUE_OVERFLOW"
|
|
75
83
|
| "VALIDATION_ERROR";
|
|
76
84
|
|
|
77
85
|
/**
|
|
@@ -88,6 +96,8 @@ export class CallciumError extends Error {
|
|
|
88
96
|
super(`${prefix}${message}`);
|
|
89
97
|
this.name = "CallciumError";
|
|
90
98
|
this.code = code;
|
|
99
|
+
// The property is declared optional, so it stays absent rather than present and undefined.
|
|
100
|
+
if (offset !== undefined) this.offset = offset;
|
|
91
101
|
}
|
|
92
102
|
}
|
|
93
103
|
|
package/src/index.ts
CHANGED
package/src/operators.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hexToBytes } from "./bytes";
|
|
2
|
+
import { DescriptorFormat as DF, formatCode, Op, PolicyFormat as PF, TypeCode, operandsOf } from "./constants";
|
|
2
3
|
import { CallciumError } from "./errors";
|
|
3
4
|
|
|
5
|
+
import type { Hex } from "./types";
|
|
6
|
+
|
|
4
7
|
/** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
|
|
5
8
|
export function toBigInt(bytes: Uint8Array, offset = 0): bigint {
|
|
6
9
|
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
|
|
@@ -128,7 +131,7 @@ function isIn(value: bigint, data: Uint8Array): boolean {
|
|
|
128
131
|
export function applyOperator(
|
|
129
132
|
opCode: number,
|
|
130
133
|
value: bigint,
|
|
131
|
-
valueLength:
|
|
134
|
+
valueLength: bigint,
|
|
132
135
|
operandData: Uint8Array,
|
|
133
136
|
typeCode: number,
|
|
134
137
|
): boolean {
|
|
@@ -138,7 +141,7 @@ export function applyOperator(
|
|
|
138
141
|
// value and the opcode maps onto its EQ/GT/LT/GTE/LTE/BETWEEN twin. Counts are non-negative,
|
|
139
142
|
// so the comparison is always unsigned — force an unsigned type code regardless of the target.
|
|
140
143
|
if (isLengthOp(base)) {
|
|
141
|
-
value =
|
|
144
|
+
value = valueLength;
|
|
142
145
|
typeCode = TypeCode.UINT_MAX;
|
|
143
146
|
base = lengthToValueOp(base);
|
|
144
147
|
}
|
|
@@ -196,7 +199,7 @@ export function applyOperator(
|
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
default:
|
|
199
|
-
throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code
|
|
202
|
+
throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code ${formatCode(base, PF.RULE_OPCODE_SIZE)}`);
|
|
200
203
|
}
|
|
201
204
|
|
|
202
205
|
const negate = (opCode & Op.NOT) !== 0;
|
|
@@ -217,6 +220,20 @@ export function isValidOperatorData(opBase: number, dataLength: number): boolean
|
|
|
217
220
|
return false;
|
|
218
221
|
}
|
|
219
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Read an operator's encoding as bytes: one opcode byte followed by its data payload.
|
|
225
|
+
* @param operator - 0x-prefixed hex string holding a single operator.
|
|
226
|
+
* @returns The operator's bytes, opcode first.
|
|
227
|
+
* @throws {CallciumError} If the hex is malformed or carries no opcode byte.
|
|
228
|
+
*/
|
|
229
|
+
export function toOperatorBytes(operator: Hex): Uint8Array {
|
|
230
|
+
const bytes = hexToBytes(operator);
|
|
231
|
+
if (bytes.length < 1) {
|
|
232
|
+
throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode)");
|
|
233
|
+
}
|
|
234
|
+
return bytes;
|
|
235
|
+
}
|
|
236
|
+
|
|
220
237
|
///////////////////////////////////////////////////////////////////////////
|
|
221
238
|
// Type code classification
|
|
222
239
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -232,7 +249,7 @@ export type TypeCodeInfo = TypeClassInfo & { label: string };
|
|
|
232
249
|
|
|
233
250
|
/** Throw an UNKNOWN_TYPE_CODE error. */
|
|
234
251
|
function unknownTypeCode(code: number): never {
|
|
235
|
-
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code
|
|
252
|
+
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code ${formatCode(code, DF.TYPECODE_SIZE)}`);
|
|
236
253
|
}
|
|
237
254
|
|
|
238
255
|
// Pre-allocated constant objects for fixed type codes (avoids per-call allocation).
|
|
@@ -264,6 +281,20 @@ export function classifyTypeCode(code: number): TypeClassInfo {
|
|
|
264
281
|
unknownTypeCode(code);
|
|
265
282
|
}
|
|
266
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Determine whether a type code names an elementary type.
|
|
286
|
+
* @param code - A single-byte descriptor type code.
|
|
287
|
+
* @returns True for an elementary code; false for a composite or unassigned one.
|
|
288
|
+
*/
|
|
289
|
+
export function isElementary(code: number): boolean {
|
|
290
|
+
try {
|
|
291
|
+
// An undefined code is reported by a throw, and names no elementary type either way.
|
|
292
|
+
return classifyTypeCode(code).typeClass === "elementary";
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
267
298
|
/**
|
|
268
299
|
* Determine whether a type code names a value an operator can read: a scalar word or a declared
|
|
269
300
|
* length. Tuples, static arrays, and undefined codes address nothing.
|
package/src/path.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { bytesToHex, hexToBytes, readU16, writeBE16 } from "./bytes";
|
|
2
|
-
import { buildCodeMap, PolicyFormat as PF } from "./constants";
|
|
2
|
+
import { buildCodeMap, formatCode, PolicyFormat as PF } from "./constants";
|
|
3
3
|
import { CallciumError } from "./errors";
|
|
4
4
|
|
|
5
5
|
import type { Hex } from "./types";
|
|
@@ -20,7 +20,7 @@ export const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
|
|
|
20
20
|
export type QuantifierInfo = { label: string };
|
|
21
21
|
|
|
22
22
|
const quantifierByCode: ReadonlyMap<number, QuantifierInfo> = new Map<number, QuantifierInfo>(
|
|
23
|
-
QUANTIFIER_TABLE.map((
|
|
23
|
+
QUANTIFIER_TABLE.map((entry) => [entry.code, { label: entry.label }]),
|
|
24
24
|
);
|
|
25
25
|
|
|
26
26
|
/** Check whether a path step is a quantifier (ALL or ANY). */
|
|
@@ -36,7 +36,8 @@ export function isQuantifier(step: number): boolean {
|
|
|
36
36
|
*/
|
|
37
37
|
export function lookupQuantifier(code: number): QuantifierInfo {
|
|
38
38
|
const info = quantifierByCode.get(code);
|
|
39
|
-
if (!info)
|
|
39
|
+
if (!info)
|
|
40
|
+
throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step ${formatCode(code, PF.PATH_STEP_SIZE)}`);
|
|
40
41
|
return info;
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -44,11 +45,19 @@ export function lookupQuantifier(code: number): QuantifierInfo {
|
|
|
44
45
|
// Path steps
|
|
45
46
|
///////////////////////////////////////////////////////////////////////////
|
|
46
47
|
|
|
47
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Encode a sequence of uint16 path steps as a big-endian hex string.
|
|
50
|
+
* @throws {CallciumError} When a step is not an integer the field holds. A wrapped step would
|
|
51
|
+
* address a different argument.
|
|
52
|
+
*/
|
|
48
53
|
export function encodePath(steps: readonly number[]): Hex {
|
|
49
|
-
const buffer = new Uint8Array(steps.length *
|
|
54
|
+
const buffer = new Uint8Array(steps.length * PF.PATH_STEP_SIZE);
|
|
50
55
|
for (let i = 0; i < steps.length; i++) {
|
|
51
|
-
|
|
56
|
+
const step = steps[i]!;
|
|
57
|
+
if (!Number.isInteger(step) || step < 0 || step > PF.PATH_STEP_MAX) {
|
|
58
|
+
throw new CallciumError("PATH_STEP_OVERFLOW", `Path step ${step} is outside the path step field`);
|
|
59
|
+
}
|
|
60
|
+
writeBE16(buffer, i * PF.PATH_STEP_SIZE, step);
|
|
52
61
|
}
|
|
53
62
|
return bytesToHex(buffer);
|
|
54
63
|
}
|
package/src/policy-builder.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { bytesToHex } from "./bytes";
|
|
2
|
-
import { Scope, TypeCode,
|
|
3
|
-
import {
|
|
2
|
+
import { Scope, TypeCode, lookupContextProperty } from "./constants";
|
|
3
|
+
import { readOperandExtremes } from "./constraint";
|
|
4
4
|
import { Descriptor } from "./descriptor";
|
|
5
5
|
import { DescriptorCoder } from "./descriptor-coder";
|
|
6
6
|
import { CallciumError, ValidationError } from "./errors";
|
|
7
|
+
import { isSigned } from "./operators";
|
|
7
8
|
import { isQuantifier, parsePathSteps } from "./path";
|
|
8
9
|
import { PolicyCoder } from "./policy-coder";
|
|
9
10
|
import { PolicyValidator } from "./policy-validator";
|
|
@@ -20,29 +21,24 @@ type PolicyDraft = {
|
|
|
20
21
|
selector: Hex;
|
|
21
22
|
descriptor: Uint8Array;
|
|
22
23
|
groups: Constraint[][];
|
|
23
|
-
|
|
24
|
+
/** Scope-qualified paths already taken in the group being built. */
|
|
25
|
+
groupPathKeys: Set<string>;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
///////////////////////////////////////////////////////////////////////////
|
|
27
29
|
// Path validation
|
|
28
30
|
///////////////////////////////////////////////////////////////////////////
|
|
29
31
|
|
|
30
|
-
/** Validate a context-scope path. */
|
|
31
|
-
function validateContextPath(steps: number[]):
|
|
32
|
+
/** Validate a context-scope path and return the referenced property's type code. */
|
|
33
|
+
function validateContextPath(steps: number[]): number {
|
|
32
34
|
if (steps.length !== 1) {
|
|
33
35
|
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step");
|
|
34
36
|
}
|
|
35
|
-
|
|
36
|
-
if (step > MAX_CONTEXT_PROPERTY_ID) {
|
|
37
|
-
throw new CallciumError(
|
|
38
|
-
"UNKNOWN_CONTEXT_PROPERTY",
|
|
39
|
-
`Unknown context property ID 0x${step.toString(16).padStart(4, "0")}`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
37
|
+
return lookupContextProperty(steps[0]!).typeCode;
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
/** Validate a calldata-scope path against the descriptor. */
|
|
45
|
-
function validateCalldataPath(steps: number[], desc: Uint8Array):
|
|
40
|
+
/** Validate a calldata-scope path against the descriptor and return the target's type code. */
|
|
41
|
+
function validateCalldataPath(steps: number[], desc: Uint8Array): number {
|
|
46
42
|
const argIndex = steps[0]!;
|
|
47
43
|
const paramCount = Descriptor.paramCount(desc);
|
|
48
44
|
if (argIndex >= paramCount) {
|
|
@@ -94,6 +90,63 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
|
94
90
|
throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type");
|
|
95
91
|
}
|
|
96
92
|
}
|
|
93
|
+
|
|
94
|
+
return Descriptor.inspect(desc, offset).typeCode;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
///////////////////////////////////////////////////////////////////////////
|
|
98
|
+
// Operand domain
|
|
99
|
+
///////////////////////////////////////////////////////////////////////////
|
|
100
|
+
|
|
101
|
+
// Values a 32-byte word represents, which is the distance between an operand and its alias.
|
|
102
|
+
const WORD_VALUES = 1n << 256n;
|
|
103
|
+
|
|
104
|
+
/** Integers an integer target admits, or null for any other type. */
|
|
105
|
+
function targetBounds(typeCode: number): { min: bigint; max: bigint } | null {
|
|
106
|
+
if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
|
|
107
|
+
const bits = BigInt(typeCode - TypeCode.UINT_MIN + 1) * 8n;
|
|
108
|
+
return { min: 0n, max: (1n << bits) - 1n };
|
|
109
|
+
}
|
|
110
|
+
if (typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX) {
|
|
111
|
+
const bits = BigInt(typeCode - TypeCode.INT_MIN + 1) * 8n;
|
|
112
|
+
const half = 1n << (bits - 1n);
|
|
113
|
+
return { min: -half, max: half - 1n };
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Reject an operand outside the target's range whose word carries a value inside it. */
|
|
119
|
+
function reject(written: bigint, folded: bigint): never {
|
|
120
|
+
throw new CallciumError(
|
|
121
|
+
"OUT_OF_PHYSICAL_BOUNDS",
|
|
122
|
+
`Operand ${written} is outside the physical range of the type and encodes as ${folded}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Reject an operand that two's complement folds onto a value the target admits.
|
|
128
|
+
* Such an operand and the value it folds onto encode to one word, so the operands as written are
|
|
129
|
+
* the only place the two stay distinct. An operand spans no more than one word, which leaves two
|
|
130
|
+
* ways across: a negative's alias clears every target narrower than the whole word, and a positive
|
|
131
|
+
* reaches a signed range only at that type's own minimum word.
|
|
132
|
+
*/
|
|
133
|
+
function checkOperandDomain(constraint: Constraint, typeCode: number): void {
|
|
134
|
+
const bounds = targetBounds(typeCode);
|
|
135
|
+
const extremes = readOperandExtremes(constraint);
|
|
136
|
+
if (bounds === null || extremes === undefined) return;
|
|
137
|
+
|
|
138
|
+
// A negative operand occupies the word its unsigned alias does, which only the target holding
|
|
139
|
+
// the whole word can hold too.
|
|
140
|
+
if (extremes.leastNegative < 0n && typeCode === TypeCode.UINT_MAX) {
|
|
141
|
+
reject(extremes.leastNegative, extremes.leastNegative + WORD_VALUES);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!isSigned(typeCode)) return;
|
|
145
|
+
// An operand at or above the signed minimum's own word denotes that negative, and every such
|
|
146
|
+
// operand already exceeds the signed maximum.
|
|
147
|
+
if (extremes.greatest >= WORD_VALUES + bounds.min) {
|
|
148
|
+
reject(extremes.greatest, extremes.greatest - WORD_VALUES);
|
|
149
|
+
}
|
|
97
150
|
}
|
|
98
151
|
|
|
99
152
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -120,7 +173,7 @@ export class PolicyBuilder {
|
|
|
120
173
|
selector: parsed.selector,
|
|
121
174
|
descriptor,
|
|
122
175
|
groups: [[]],
|
|
123
|
-
|
|
176
|
+
groupPathKeys: new Set(),
|
|
124
177
|
});
|
|
125
178
|
}
|
|
126
179
|
|
|
@@ -135,47 +188,54 @@ export class PolicyBuilder {
|
|
|
135
188
|
selector: "0x00000000",
|
|
136
189
|
descriptor,
|
|
137
190
|
groups: [[]],
|
|
138
|
-
|
|
191
|
+
groupPathKeys: new Set(),
|
|
139
192
|
});
|
|
140
193
|
}
|
|
141
194
|
|
|
142
195
|
/**
|
|
143
196
|
* Add a constraint to the current group.
|
|
144
197
|
* @param constraint - A `Constraint` object or a `ConstraintBuilder` instance.
|
|
198
|
+
* @throws {CallciumError} With code `OUT_OF_PHYSICAL_BOUNDS` when a numeric operand lies outside
|
|
199
|
+
* the target type's range and encodes as a value inside it. Operands are read as written, so a
|
|
200
|
+
* `Constraint` carrying encoded operator bytes is left to {@link validate}.
|
|
145
201
|
*/
|
|
146
|
-
add(constraint: Constraint
|
|
147
|
-
|
|
202
|
+
add(constraint: Constraint): this {
|
|
203
|
+
// A builder compiles no hint; one arrives only on a constraint that came already encoded.
|
|
204
|
+
const added: Constraint = {
|
|
148
205
|
scope: constraint.scope,
|
|
149
206
|
path: constraint.path,
|
|
150
207
|
operators: [...constraint.operators],
|
|
208
|
+
...("hint" in constraint && constraint.hint !== undefined && { hint: constraint.hint }),
|
|
151
209
|
};
|
|
152
210
|
|
|
153
|
-
if (
|
|
211
|
+
if (added.operators.length === 0) {
|
|
154
212
|
throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator");
|
|
155
213
|
}
|
|
156
214
|
|
|
157
215
|
// Path shape is established before the scope decides how to navigate it.
|
|
158
|
-
const steps = parsePathSteps(
|
|
216
|
+
const steps = parsePathSteps(added.path);
|
|
159
217
|
if (steps.length === 0) {
|
|
160
218
|
throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
161
219
|
}
|
|
162
220
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
221
|
+
let targetTypeCode: number;
|
|
222
|
+
if (added.scope === Scope.CONTEXT) {
|
|
223
|
+
targetTypeCode = validateContextPath(steps);
|
|
224
|
+
} else if (added.scope === Scope.CALLDATA) {
|
|
225
|
+
targetTypeCode = validateCalldataPath(steps, this.draft.descriptor);
|
|
167
226
|
} else {
|
|
168
|
-
throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${
|
|
227
|
+
throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${added.scope}`);
|
|
169
228
|
}
|
|
170
229
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
230
|
+
checkOperandDomain(constraint, targetTypeCode);
|
|
231
|
+
|
|
232
|
+
const key = `${added.scope}:${added.path.toLowerCase()}`;
|
|
233
|
+
if (this.draft.groupPathKeys.has(key)) {
|
|
234
|
+
throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${added.path} in the same group`);
|
|
175
235
|
}
|
|
176
236
|
|
|
177
|
-
|
|
178
|
-
this.draft.groups[this.draft.groups.length - 1]!.push(
|
|
237
|
+
this.draft.groupPathKeys.add(key);
|
|
238
|
+
this.draft.groups[this.draft.groups.length - 1]!.push(added);
|
|
179
239
|
return this;
|
|
180
240
|
}
|
|
181
241
|
|
|
@@ -186,7 +246,7 @@ export class PolicyBuilder {
|
|
|
186
246
|
throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty");
|
|
187
247
|
}
|
|
188
248
|
this.draft.groups.push([]);
|
|
189
|
-
this.draft.
|
|
249
|
+
this.draft.groupPathKeys.clear();
|
|
190
250
|
return this;
|
|
191
251
|
}
|
|
192
252
|
|
|
@@ -197,6 +257,8 @@ export class PolicyBuilder {
|
|
|
197
257
|
* @returns The policy as a 0x-prefixed hex string.
|
|
198
258
|
* @throws {CallciumError} If any group is empty.
|
|
199
259
|
* @throws {ValidationError} If validation finds any issue.
|
|
260
|
+
* @throws {CallciumError} With code `RULE_SIZE_OVERFLOW` when a rule's encoded bytes exceed the
|
|
261
|
+
* width of the rule size field.
|
|
200
262
|
*/
|
|
201
263
|
build(): Hex {
|
|
202
264
|
this.checkGroups();
|