@callcium/sdk 0.9.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 +202 -145
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +763 -667
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/bytes.ts +9 -9
- package/src/calldata-reader.ts +14 -1
- package/src/constants.ts +85 -181
- package/src/constraint.ts +131 -94
- package/src/descriptor-coder.ts +53 -84
- package/src/descriptor.ts +65 -36
- package/src/errors.ts +12 -5
- package/src/index.ts +8 -17
- package/src/operators.ts +147 -11
- package/src/path.ts +80 -0
- package/src/policy-builder.ts +107 -44
- package/src/policy-coder.ts +102 -77
- package/src/policy-enforcer.ts +98 -128
- package/src/policy-validator.ts +70 -56
- package/src/signature.ts +21 -19
- package/src/type-desc.ts +15 -15
- package/src/types.ts +70 -3
- package/src/validation-issue.ts +4 -4
package/src/policy-builder.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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 {
|
|
7
|
+
import { isSigned } from "./operators";
|
|
8
|
+
import { isQuantifier, parsePathSteps } from "./path";
|
|
9
|
+
import { PolicyCoder } from "./policy-coder";
|
|
8
10
|
import { PolicyValidator } from "./policy-validator";
|
|
9
11
|
import { SignatureParser } from "./signature";
|
|
10
12
|
|
|
@@ -19,35 +21,30 @@ type PolicyDraft = {
|
|
|
19
21
|
selector: Hex;
|
|
20
22
|
descriptor: Uint8Array;
|
|
21
23
|
groups: Constraint[][];
|
|
22
|
-
|
|
24
|
+
/** Scope-qualified paths already taken in the group being built. */
|
|
25
|
+
groupPathKeys: Set<string>;
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
///////////////////////////////////////////////////////////////////////////
|
|
26
29
|
// Path validation
|
|
27
30
|
///////////////////////////////////////////////////////////////////////////
|
|
28
31
|
|
|
29
|
-
/** Validate a context-scope path. */
|
|
30
|
-
function validateContextPath(steps: number[]):
|
|
32
|
+
/** Validate a context-scope path and return the referenced property's type code. */
|
|
33
|
+
function validateContextPath(steps: number[]): number {
|
|
31
34
|
if (steps.length !== 1) {
|
|
32
|
-
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step
|
|
33
|
-
}
|
|
34
|
-
const step = steps[0]!;
|
|
35
|
-
if (step > MAX_CONTEXT_PROPERTY_ID) {
|
|
36
|
-
throw new CallciumError(
|
|
37
|
-
"UNKNOWN_CONTEXT_PROPERTY",
|
|
38
|
-
`Unknown context property ID 0x${step.toString(16).padStart(4, "0")}.`,
|
|
39
|
-
);
|
|
35
|
+
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step");
|
|
40
36
|
}
|
|
37
|
+
return lookupContextProperty(steps[0]!).typeCode;
|
|
41
38
|
}
|
|
42
39
|
|
|
43
|
-
/** Validate a calldata-scope path against the descriptor. */
|
|
44
|
-
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 {
|
|
45
42
|
const argIndex = steps[0]!;
|
|
46
43
|
const paramCount = Descriptor.paramCount(desc);
|
|
47
44
|
if (argIndex >= paramCount) {
|
|
48
45
|
throw new CallciumError(
|
|
49
46
|
"PARAM_INDEX_OUT_OF_BOUNDS",
|
|
50
|
-
`Param index ${argIndex} out of range (descriptor has ${paramCount} params)
|
|
47
|
+
`Param index ${argIndex} out of range (descriptor has ${paramCount} params)`,
|
|
51
48
|
);
|
|
52
49
|
}
|
|
53
50
|
|
|
@@ -61,10 +58,10 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
|
61
58
|
|
|
62
59
|
if (isQuantifier(step)) {
|
|
63
60
|
if (!isArray) {
|
|
64
|
-
throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node
|
|
61
|
+
throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node");
|
|
65
62
|
}
|
|
66
63
|
if (hasQuantifier) {
|
|
67
|
-
throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed
|
|
64
|
+
throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed");
|
|
68
65
|
}
|
|
69
66
|
hasQuantifier = true;
|
|
70
67
|
}
|
|
@@ -74,7 +71,7 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
|
74
71
|
if (step >= fieldCount) {
|
|
75
72
|
throw new CallciumError(
|
|
76
73
|
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
77
|
-
`Tuple field index ${step} out of range (tuple has ${fieldCount} fields)
|
|
74
|
+
`Tuple field index ${step} out of range (tuple has ${fieldCount} fields)`,
|
|
78
75
|
);
|
|
79
76
|
}
|
|
80
77
|
offset = Descriptor.tupleFieldOffset(desc, offset, step);
|
|
@@ -84,15 +81,72 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
|
84
81
|
if (step >= arrayLength) {
|
|
85
82
|
throw new CallciumError(
|
|
86
83
|
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
87
|
-
`Array index ${step} out of range (static array has ${arrayLength} elements)
|
|
84
|
+
`Array index ${step} out of range (static array has ${arrayLength} elements)`,
|
|
88
85
|
);
|
|
89
86
|
}
|
|
90
87
|
}
|
|
91
88
|
offset = Descriptor.arrayElementOffset(offset);
|
|
92
89
|
} else {
|
|
93
|
-
throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type
|
|
90
|
+
throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type");
|
|
94
91
|
}
|
|
95
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
|
+
}
|
|
96
150
|
}
|
|
97
151
|
|
|
98
152
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -119,7 +173,7 @@ export class PolicyBuilder {
|
|
|
119
173
|
selector: parsed.selector,
|
|
120
174
|
descriptor,
|
|
121
175
|
groups: [[]],
|
|
122
|
-
|
|
176
|
+
groupPathKeys: new Set(),
|
|
123
177
|
});
|
|
124
178
|
}
|
|
125
179
|
|
|
@@ -134,47 +188,54 @@ export class PolicyBuilder {
|
|
|
134
188
|
selector: "0x00000000",
|
|
135
189
|
descriptor,
|
|
136
190
|
groups: [[]],
|
|
137
|
-
|
|
191
|
+
groupPathKeys: new Set(),
|
|
138
192
|
});
|
|
139
193
|
}
|
|
140
194
|
|
|
141
195
|
/**
|
|
142
196
|
* Add a constraint to the current group.
|
|
143
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}.
|
|
144
201
|
*/
|
|
145
|
-
add(constraint: Constraint
|
|
146
|
-
|
|
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 = {
|
|
147
205
|
scope: constraint.scope,
|
|
148
206
|
path: constraint.path,
|
|
149
207
|
operators: [...constraint.operators],
|
|
208
|
+
...("hint" in constraint && constraint.hint !== undefined && { hint: constraint.hint }),
|
|
150
209
|
};
|
|
151
210
|
|
|
152
|
-
if (
|
|
153
|
-
throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator
|
|
211
|
+
if (added.operators.length === 0) {
|
|
212
|
+
throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator");
|
|
154
213
|
}
|
|
155
214
|
|
|
156
215
|
// Path shape is established before the scope decides how to navigate it.
|
|
157
|
-
const steps = parsePathSteps(
|
|
216
|
+
const steps = parsePathSteps(added.path);
|
|
158
217
|
if (steps.length === 0) {
|
|
159
|
-
throw new CallciumError("EMPTY_PATH", "Path must have at least one step
|
|
218
|
+
throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
160
219
|
}
|
|
161
220
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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);
|
|
166
226
|
} else {
|
|
167
|
-
throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${
|
|
227
|
+
throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${added.scope}`);
|
|
168
228
|
}
|
|
169
229
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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`);
|
|
174
235
|
}
|
|
175
236
|
|
|
176
|
-
|
|
177
|
-
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);
|
|
178
239
|
return this;
|
|
179
240
|
}
|
|
180
241
|
|
|
@@ -182,10 +243,10 @@ export class PolicyBuilder {
|
|
|
182
243
|
or(): this {
|
|
183
244
|
const lastGroup = this.draft.groups[this.draft.groups.length - 1]!;
|
|
184
245
|
if (lastGroup.length === 0) {
|
|
185
|
-
throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty
|
|
246
|
+
throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty");
|
|
186
247
|
}
|
|
187
248
|
this.draft.groups.push([]);
|
|
188
|
-
this.draft.
|
|
249
|
+
this.draft.groupPathKeys.clear();
|
|
189
250
|
return this;
|
|
190
251
|
}
|
|
191
252
|
|
|
@@ -196,6 +257,8 @@ export class PolicyBuilder {
|
|
|
196
257
|
* @returns The policy as a 0x-prefixed hex string.
|
|
197
258
|
* @throws {CallciumError} If any group is empty.
|
|
198
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.
|
|
199
262
|
*/
|
|
200
263
|
build(): Hex {
|
|
201
264
|
this.checkGroups();
|
|
@@ -233,7 +296,7 @@ export class PolicyBuilder {
|
|
|
233
296
|
private checkGroups(): void {
|
|
234
297
|
for (let i = 0; i < this.draft.groups.length; i++) {
|
|
235
298
|
if (this.draft.groups[i]!.length === 0) {
|
|
236
|
-
throw new CallciumError("EMPTY_GROUP", `Group ${i} is empty
|
|
299
|
+
throw new CallciumError("EMPTY_GROUP", `Group ${i} is empty`);
|
|
237
300
|
}
|
|
238
301
|
}
|
|
239
302
|
}
|
package/src/policy-coder.ts
CHANGED
|
@@ -3,19 +3,26 @@ import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
|
3
3
|
import { bytesToHex, hexToBytes, toHex, readU16, readU32, writeBE16, writeBE32 } from "./bytes";
|
|
4
4
|
import {
|
|
5
5
|
DescriptorFormat as DF,
|
|
6
|
+
fieldMax,
|
|
7
|
+
formatCode,
|
|
8
|
+
MAX_CONTEXT_PROPERTY_ID,
|
|
9
|
+
Op,
|
|
6
10
|
PolicyFormat as PF,
|
|
7
11
|
Scope,
|
|
8
|
-
Limits,
|
|
9
|
-
Op,
|
|
10
12
|
TypeCode,
|
|
11
|
-
isValidOperatorData,
|
|
12
|
-
isAddressableTarget,
|
|
13
|
-
MAX_CONTEXT_PROPERTY_ID,
|
|
14
13
|
} from "./constants";
|
|
15
14
|
import { Descriptor } from "./descriptor";
|
|
16
15
|
import { decodeDescriptor } from "./descriptor-coder";
|
|
17
|
-
import { CallciumError } from "./errors";
|
|
18
|
-
import {
|
|
16
|
+
import { CallciumError, type CallciumErrorCode } from "./errors";
|
|
17
|
+
import {
|
|
18
|
+
isLengthOp,
|
|
19
|
+
isLengthValidType,
|
|
20
|
+
toBigInt,
|
|
21
|
+
isAddressableTarget,
|
|
22
|
+
isValidOperatorData,
|
|
23
|
+
toOperatorBytes,
|
|
24
|
+
} from "./operators";
|
|
25
|
+
import { parsePathSteps } from "./path";
|
|
19
26
|
|
|
20
27
|
import type {
|
|
21
28
|
Constraint,
|
|
@@ -28,27 +35,6 @@ import type {
|
|
|
28
35
|
PolicyData,
|
|
29
36
|
} from "./types";
|
|
30
37
|
|
|
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
38
|
///////////////////////////////////////////////////////////////////////////
|
|
53
39
|
// Policy field helper
|
|
54
40
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -58,6 +44,24 @@ function field<T>(value: T, start: number, end: number): Field<T> {
|
|
|
58
44
|
return { value, span: { start, end } };
|
|
59
45
|
}
|
|
60
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Wrap the hex rendering of `data[start, end)` with its span, rendering on first read.
|
|
49
|
+
* Enforcement reads the span as bytes, so the string is built only for a caller that asks.
|
|
50
|
+
* The field reads and assigns like the plain one it replaces.
|
|
51
|
+
*/
|
|
52
|
+
function hexField(data: Uint8Array, start: number, end: number): Field<Hex> {
|
|
53
|
+
let hex: Hex | undefined;
|
|
54
|
+
return {
|
|
55
|
+
get value(): Hex {
|
|
56
|
+
return (hex ??= toHex(data, start, end));
|
|
57
|
+
},
|
|
58
|
+
set value(replacement: Hex) {
|
|
59
|
+
hex = replacement;
|
|
60
|
+
},
|
|
61
|
+
span: { start, end },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
61
65
|
///////////////////////////////////////////////////////////////////////////
|
|
62
66
|
// Hint block
|
|
63
67
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -153,7 +157,7 @@ function hintTargetTypeCode(data: Uint8Array, hintStart: number, hintSize: numbe
|
|
|
153
157
|
/** Decode a policy blob, returning the structural representation and the blob bytes it spans. */
|
|
154
158
|
export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Array } {
|
|
155
159
|
const data = hexToBytes(blob);
|
|
156
|
-
if (data.length < PF.DESC_OFFSET
|
|
160
|
+
if (data.length < PF.DESC_OFFSET) {
|
|
157
161
|
throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short");
|
|
158
162
|
}
|
|
159
163
|
|
|
@@ -199,7 +203,8 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
199
203
|
end: descStart + param.span.end,
|
|
200
204
|
},
|
|
201
205
|
}));
|
|
202
|
-
|
|
206
|
+
|
|
207
|
+
const descriptorRaw = hexField(data, descStart, descEnd);
|
|
203
208
|
|
|
204
209
|
const groupCountStart = groupCountOffset;
|
|
205
210
|
const groupCountEnd = groupCountStart + PF.GROUP_COUNT_SIZE;
|
|
@@ -221,6 +226,10 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
221
226
|
const groupBodyStart = offset + PF.GROUP_HEADER_SIZE;
|
|
222
227
|
const groupEnd = groupBodyStart + groupSizeValue;
|
|
223
228
|
|
|
229
|
+
if (groupEnd > data.length) {
|
|
230
|
+
throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
|
|
231
|
+
}
|
|
232
|
+
|
|
224
233
|
const ruleCountStart = offset;
|
|
225
234
|
const ruleCountValue = readU16(data, ruleCountStart);
|
|
226
235
|
if (ruleCountValue === 0) {
|
|
@@ -229,9 +238,6 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
229
238
|
if (groupSizeValue < ruleCountValue * PF.RULE_MIN_SIZE) {
|
|
230
239
|
throw new CallciumError("GROUP_TOO_SMALL", "Declared group size is too small for its rule count", offset);
|
|
231
240
|
}
|
|
232
|
-
if (groupEnd > data.length) {
|
|
233
|
-
throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
|
|
234
|
-
}
|
|
235
241
|
|
|
236
242
|
const rules: DecodedRule[] = [];
|
|
237
243
|
let ruleOffset = groupBodyStart;
|
|
@@ -249,8 +255,8 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
249
255
|
ruleOffset,
|
|
250
256
|
);
|
|
251
257
|
}
|
|
252
|
-
if (ruleOffset + ruleSizeValue >
|
|
253
|
-
throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond
|
|
258
|
+
if (ruleOffset + ruleSizeValue > data.length) {
|
|
259
|
+
throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond policy blob", ruleOffset);
|
|
254
260
|
}
|
|
255
261
|
|
|
256
262
|
const scopeOffset = ruleOffset + PF.RULE_SCOPE_OFFSET;
|
|
@@ -312,12 +318,13 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
312
318
|
// IN operands must be strictly ascending (unsigned); strictness also rejects duplicates.
|
|
313
319
|
if (opBase === Op.IN) {
|
|
314
320
|
const wordCount = dataLengthValue / 32;
|
|
321
|
+
let prev = toBigInt(data, dataStart);
|
|
315
322
|
for (let word = 1; word < wordCount; word++) {
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
if (compareBytes(prev, cur) >= 0) {
|
|
323
|
+
const cur = toBigInt(data, dataStart + word * 32);
|
|
324
|
+
if (cur <= prev) {
|
|
319
325
|
throw new CallciumError("UNSORTED_IN_SET", "IN operands must be strictly ascending", ruleOffset);
|
|
320
326
|
}
|
|
327
|
+
prev = cur;
|
|
321
328
|
}
|
|
322
329
|
}
|
|
323
330
|
|
|
@@ -336,10 +343,10 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
336
343
|
if (depthValue === 0) {
|
|
337
344
|
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
|
|
338
345
|
}
|
|
339
|
-
if (depthValue >
|
|
346
|
+
if (depthValue > PF.MAX_PATH_DEPTH) {
|
|
340
347
|
throw new CallciumError(
|
|
341
348
|
"PATH_TOO_DEEP",
|
|
342
|
-
`Path depth ${depthValue} exceeds maximum ${
|
|
349
|
+
`Path depth ${depthValue} exceeds maximum ${PF.MAX_PATH_DEPTH}`,
|
|
343
350
|
ruleOffset,
|
|
344
351
|
);
|
|
345
352
|
}
|
|
@@ -357,7 +364,7 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
357
364
|
if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) {
|
|
358
365
|
throw new CallciumError(
|
|
359
366
|
"UNKNOWN_CONTEXT_PROPERTY",
|
|
360
|
-
`Context rule references undefined context property
|
|
367
|
+
`Context rule references undefined context property ${formatCode(contextPropertyId, PF.PATH_STEP_SIZE)}`,
|
|
361
368
|
ruleOffset,
|
|
362
369
|
);
|
|
363
370
|
}
|
|
@@ -370,14 +377,17 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
370
377
|
pathDepth: field(depthValue, depthOffset, depthOffset + 1),
|
|
371
378
|
path: field(pathHex, pathStart, pathStart + pathLength),
|
|
372
379
|
...(hintSize > 0 && {
|
|
373
|
-
hint:
|
|
380
|
+
hint: hexField(data, hintStart, hintStart + hintSize),
|
|
374
381
|
}),
|
|
375
382
|
opCode: field(opCodeValue, opCodeOffset, opCodeOffset + PF.RULE_OPCODE_SIZE),
|
|
376
383
|
dataLength: field(dataLengthValue, dataLengthOffset, dataLengthOffset + PF.RULE_DATALENGTH_SIZE),
|
|
377
|
-
data:
|
|
384
|
+
data: hexField(data, dataStart, dataStart + dataLengthValue),
|
|
378
385
|
span: { start: ruleOffset, end: ruleOffset + ruleSizeValue },
|
|
379
386
|
});
|
|
380
387
|
|
|
388
|
+
if (ruleOffset + ruleSizeValue > groupEnd) {
|
|
389
|
+
throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
|
|
390
|
+
}
|
|
381
391
|
ruleOffset += ruleSizeValue;
|
|
382
392
|
}
|
|
383
393
|
|
|
@@ -404,7 +414,12 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
404
414
|
selector: field(selectorHex, selectorStart, selectorEnd),
|
|
405
415
|
descLength: field(descLengthValue, descLengthStart, descLengthStart + PF.DESC_LENGTH_SIZE),
|
|
406
416
|
descriptor: {
|
|
407
|
-
raw:
|
|
417
|
+
get raw(): Hex {
|
|
418
|
+
return descriptorRaw.value;
|
|
419
|
+
},
|
|
420
|
+
set raw(replacement: Hex) {
|
|
421
|
+
descriptorRaw.value = replacement;
|
|
422
|
+
},
|
|
408
423
|
header: field({ version: desc.version, paramCount: desc.params.length }, descStart, descStart + DF.HEADER_SIZE),
|
|
409
424
|
params,
|
|
410
425
|
span: { start: descStart, end: descEnd },
|
|
@@ -425,34 +440,44 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
425
440
|
|
|
426
441
|
type Rule = { scope: number; path: Uint8Array; operator: Uint8Array; hint: Uint8Array };
|
|
427
442
|
|
|
428
|
-
/**
|
|
429
|
-
|
|
430
|
-
const operators = constraint.operators.map(hexToBytes);
|
|
431
|
-
for (const operator of operators) {
|
|
432
|
-
if (operator.length < 1) {
|
|
433
|
-
throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode).");
|
|
434
|
-
}
|
|
435
|
-
}
|
|
443
|
+
/** The path bytes and compiled hint every rule a constraint produces carries. */
|
|
444
|
+
type ConstraintTarget = { path: Uint8Array; hint: Uint8Array };
|
|
436
445
|
|
|
446
|
+
/** Read a constraint's path and compile the hint its rules carry. */
|
|
447
|
+
function readConstraintTarget(constraint: Constraint, desc: Uint8Array): ConstraintTarget {
|
|
437
448
|
const path = hexToBytes(constraint.path);
|
|
438
449
|
|
|
439
450
|
// Path shape checks precede compilation: the compiler assumes a framed, depth-bounded path.
|
|
440
451
|
if (path.length === 0) {
|
|
441
|
-
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step
|
|
452
|
+
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step");
|
|
442
453
|
}
|
|
443
454
|
if ((path.length & 1) !== 0) {
|
|
444
|
-
throw new CallciumError("MALFORMED_PATH", "Path byte length must be even
|
|
455
|
+
throw new CallciumError("MALFORMED_PATH", "Path byte length must be even");
|
|
445
456
|
}
|
|
446
457
|
const depth = path.length / 2;
|
|
447
|
-
if (depth >
|
|
448
|
-
throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${
|
|
458
|
+
if (depth > PF.MAX_PATH_DEPTH) {
|
|
459
|
+
throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${PF.MAX_PATH_DEPTH}`);
|
|
449
460
|
}
|
|
450
461
|
|
|
451
462
|
const hint =
|
|
452
463
|
constraint.scope === Scope.CALLDATA
|
|
453
464
|
? Descriptor.compileHint(desc, parsePathSteps(constraint.path))
|
|
454
465
|
: new Uint8Array(0);
|
|
455
|
-
return
|
|
466
|
+
return { path, hint };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Flatten a Constraint into one Rule per operator, compiling its hint against the descriptor. */
|
|
470
|
+
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
|
+
// path and hint it shares with its siblings. A constraint carrying no operator produces no rule,
|
|
473
|
+
// so its path is never read.
|
|
474
|
+
let target: ConstraintTarget | undefined;
|
|
475
|
+
|
|
476
|
+
return constraint.operators.map((operatorHex) => {
|
|
477
|
+
const operator = toOperatorBytes(operatorHex);
|
|
478
|
+
target ??= readConstraintTarget(constraint, desc);
|
|
479
|
+
return { scope: constraint.scope, operator, ...target };
|
|
480
|
+
});
|
|
456
481
|
}
|
|
457
482
|
|
|
458
483
|
/** Compare two byte arrays lexicographically. */
|
|
@@ -477,6 +502,12 @@ function sortRules(rules: Rule[]): void {
|
|
|
477
502
|
});
|
|
478
503
|
}
|
|
479
504
|
|
|
505
|
+
/** Require a value to fit the big-endian field of `byteWidth` bytes. */
|
|
506
|
+
function requireFieldFits(value: number, byteWidth: number, code: CallciumErrorCode, subject: string): void {
|
|
507
|
+
const max = fieldMax(byteWidth);
|
|
508
|
+
if (value > max) throw new CallciumError(code, `${subject} ${value} exceeds maximum ${max}`);
|
|
509
|
+
}
|
|
510
|
+
|
|
480
511
|
/**
|
|
481
512
|
* Serialize a single rule to its wire format bytes.
|
|
482
513
|
* Path shape and operator framing are established by `flattenConstraint`.
|
|
@@ -484,13 +515,11 @@ function sortRules(rules: Rule[]): void {
|
|
|
484
515
|
function encodeRule(rule: Rule): Uint8Array {
|
|
485
516
|
const depth = rule.path.length / 2;
|
|
486
517
|
if (rule.scope === Scope.CONTEXT && depth !== 1) {
|
|
487
|
-
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step
|
|
518
|
+
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step");
|
|
488
519
|
}
|
|
489
520
|
const data = rule.operator.subarray(1);
|
|
490
521
|
const ruleSize = PF.RULE_FIXED_OVERHEAD + rule.path.length + rule.hint.length + data.length;
|
|
491
|
-
|
|
492
|
-
throw new CallciumError("RULE_SIZE_OVERFLOW", `Rule size ${ruleSize} exceeds maximum 65535`);
|
|
493
|
-
}
|
|
522
|
+
requireFieldFits(ruleSize, PF.RULE_SIZE_SIZE, "RULE_SIZE_OVERFLOW", "Rule size");
|
|
494
523
|
|
|
495
524
|
const buf = new Uint8Array(ruleSize);
|
|
496
525
|
writeBE16(buf, 0, ruleSize);
|
|
@@ -541,9 +570,7 @@ function encode(data: PolicyData): Hex {
|
|
|
541
570
|
if (data.groups.length === 0) {
|
|
542
571
|
throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
|
|
543
572
|
}
|
|
544
|
-
|
|
545
|
-
throw new CallciumError("GROUP_COUNT_OVERFLOW", `Group count ${data.groups.length} exceeds maximum 255`);
|
|
546
|
-
}
|
|
573
|
+
requireFieldFits(data.groups.length, PF.GROUP_COUNT_SIZE, "GROUP_COUNT_OVERFLOW", "Group count");
|
|
547
574
|
|
|
548
575
|
// Flatten constraints into rules and sort within each group. Hints are compiled first because
|
|
549
576
|
// they are part of the rule bytes the group hash covers.
|
|
@@ -553,20 +580,13 @@ function encode(data: PolicyData): Hex {
|
|
|
553
580
|
return rules;
|
|
554
581
|
});
|
|
555
582
|
|
|
556
|
-
|
|
557
|
-
throw new CallciumError("DESC_LENGTH_OVERFLOW", `Descriptor length ${descBytes.length} exceeds maximum 65535`);
|
|
558
|
-
}
|
|
583
|
+
requireFieldFits(descBytes.length, PF.DESC_LENGTH_SIZE, "DESC_LENGTH_OVERFLOW", "Descriptor length");
|
|
559
584
|
|
|
560
585
|
const encodedGroups = sortedGroups.map((rules, groupIndex) => {
|
|
561
586
|
if (rules.length === 0) {
|
|
562
587
|
throw new CallciumError("EMPTY_GROUP", `Group ${groupIndex} is empty`);
|
|
563
588
|
}
|
|
564
|
-
|
|
565
|
-
throw new CallciumError(
|
|
566
|
-
"RULE_COUNT_OVERFLOW",
|
|
567
|
-
`Group ${groupIndex} rule count ${rules.length} exceeds maximum 65535`,
|
|
568
|
-
);
|
|
569
|
-
}
|
|
589
|
+
requireFieldFits(rules.length, PF.GROUP_RULECOUNT_SIZE, "RULE_COUNT_OVERFLOW", `Group ${groupIndex} rule count`);
|
|
570
590
|
return { wireBytes: encodeGroupRules(rules), ruleCount: rules.length };
|
|
571
591
|
});
|
|
572
592
|
|
|
@@ -577,7 +597,13 @@ function encode(data: PolicyData): Hex {
|
|
|
577
597
|
encodedGroups.sort((a, b) => compareBytes(hashes.get(a)!, hashes.get(b)!));
|
|
578
598
|
}
|
|
579
599
|
|
|
580
|
-
const selectorBytes = data.isSelectorless ? new Uint8Array(
|
|
600
|
+
const selectorBytes = data.isSelectorless ? new Uint8Array(PF.SELECTOR_SIZE) : hexToBytes(data.selector);
|
|
601
|
+
if (selectorBytes.length !== PF.SELECTOR_SIZE) {
|
|
602
|
+
throw new CallciumError(
|
|
603
|
+
"MALFORMED_SELECTOR",
|
|
604
|
+
`Selector must be ${PF.SELECTOR_SIZE} bytes, got ${selectorBytes.length}`,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
581
607
|
|
|
582
608
|
const headerByte = PF.VERSION | (data.isSelectorless ? PF.FLAG_NO_SELECTOR : 0);
|
|
583
609
|
|
|
@@ -629,8 +655,8 @@ function decode(blob: Hex): PolicyData {
|
|
|
629
655
|
const { policy } = decodePolicy(blob);
|
|
630
656
|
|
|
631
657
|
const groups: Constraint[][] = policy.groups.map((group) => {
|
|
658
|
+
// A Map keeps its insertion order, which is the order the rules named their constraints in.
|
|
632
659
|
const constraintMap = new Map<string, Constraint>();
|
|
633
|
-
const constraintOrder: string[] = [];
|
|
634
660
|
|
|
635
661
|
for (const rule of group.rules) {
|
|
636
662
|
const hintHex = rule.hint?.value;
|
|
@@ -649,11 +675,10 @@ function decode(blob: Hex): PolicyData {
|
|
|
649
675
|
span: rule.span,
|
|
650
676
|
};
|
|
651
677
|
constraintMap.set(key, constraint);
|
|
652
|
-
constraintOrder.push(key);
|
|
653
678
|
}
|
|
654
679
|
}
|
|
655
680
|
|
|
656
|
-
return
|
|
681
|
+
return [...constraintMap.values()];
|
|
657
682
|
});
|
|
658
683
|
|
|
659
684
|
return {
|