@callcium/sdk 0.11.1 → 0.13.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/README.md +2 -2
- package/dist/index.d.ts +683 -622
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2297 -2051
- package/dist/index.js.map +1 -1
- package/package.json +13 -10
- package/src/bytes.ts +9 -25
- package/src/calldata-reader.ts +3 -3
- package/src/constants.ts +156 -82
- package/src/constraint.ts +61 -33
- package/src/descriptor-coder.ts +139 -125
- package/src/descriptor.ts +82 -292
- package/src/errors.ts +38 -30
- package/src/hint.ts +237 -0
- package/src/index.ts +30 -12
- package/src/operators.ts +173 -242
- package/src/path.ts +7 -32
- package/src/policy-builder.ts +205 -190
- package/src/policy-coder.ts +1160 -536
- package/src/policy-enforcer.ts +499 -488
- package/src/policy-validator.ts +563 -1045
- package/src/signature.ts +7 -11
- package/src/type-desc.ts +12 -12
- package/src/types.ts +232 -136
- package/src/validation-issue.ts +78 -465
package/src/descriptor.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { readU16, readU24
|
|
2
|
-
import { DescriptorFormat as DF,
|
|
1
|
+
import { readU16, readU24 } from "./bytes";
|
|
2
|
+
import { DescriptorFormat as DF, TypeCode } from "./constants";
|
|
3
3
|
import { CallciumError } from "./errors";
|
|
4
|
-
import {
|
|
5
|
-
import { Quantifier, isQuantifier } from "./path";
|
|
4
|
+
import { lookupTypeCode } from "./operators";
|
|
6
5
|
|
|
7
6
|
///////////////////////////////////////////////////////////////////////////
|
|
8
7
|
// Public types
|
|
@@ -15,29 +14,6 @@ export type TypeInfo = {
|
|
|
15
14
|
staticSize: number;
|
|
16
15
|
};
|
|
17
16
|
|
|
18
|
-
///////////////////////////////////////////////////////////////////////////
|
|
19
|
-
// Internal types
|
|
20
|
-
///////////////////////////////////////////////////////////////////////////
|
|
21
|
-
|
|
22
|
-
/** A hop entry of a compiled chain: [delta, index, meta]. */
|
|
23
|
-
type Hop = [number, number, number];
|
|
24
|
-
|
|
25
|
-
/** State threaded through the compilation of a path into a hint block. */
|
|
26
|
-
type HintWalk = {
|
|
27
|
-
/** Hops of the chain currently being compiled. */
|
|
28
|
-
chain: Hop[];
|
|
29
|
-
/** Hops of the main chain, captured when a quantifier closes it. */
|
|
30
|
-
mainHops: Hop[];
|
|
31
|
-
/** Byte offset of the next node relative to the chain's current base. */
|
|
32
|
-
delta: number;
|
|
33
|
-
/** Frame prefix fields, captured when a quantifier closes the main chain. */
|
|
34
|
-
frame: { arrayDelta: number; count: number; meta: number };
|
|
35
|
-
/** Header kind the path resolves to. */
|
|
36
|
-
kind: number;
|
|
37
|
-
/** Set while the chain already ends inside the current node, which then takes no entry hop. */
|
|
38
|
-
entered: boolean;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
17
|
///////////////////////////////////////////////////////////////////////////
|
|
42
18
|
// Internal helpers
|
|
43
19
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -46,18 +22,17 @@ type HintWalk = {
|
|
|
46
22
|
type Node = { typeCode: number; staticWords: number; nodeLength: number };
|
|
47
23
|
|
|
48
24
|
/**
|
|
49
|
-
* Read the node at `offset`.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* or declares a length below its own header or beyond the descriptor.
|
|
25
|
+
* Read the node at `offset`. Elementary nodes occupy exactly one byte. Composite nodes encode their static word count
|
|
26
|
+
* and length in the 24-bit meta field that follows the type code.
|
|
27
|
+
* @throws {CallciumError} When the node starts past the descriptor, carries an undefined type code, or declares a
|
|
28
|
+
* length below its own header or beyond the descriptor.
|
|
54
29
|
*/
|
|
55
30
|
export function readNode(desc: Uint8Array, offset: number): Node {
|
|
56
31
|
if (offset >= desc.length) {
|
|
57
32
|
throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
|
|
58
33
|
}
|
|
59
34
|
const typeCode = desc[offset]!;
|
|
60
|
-
const info =
|
|
35
|
+
const info = lookupTypeCode(typeCode);
|
|
61
36
|
if (info.typeClass === "elementary") {
|
|
62
37
|
return { typeCode, staticWords: info.isDynamic ? 0 : 1, nodeLength: DF.TYPECODE_SIZE };
|
|
63
38
|
}
|
|
@@ -83,50 +58,25 @@ export function readNode(desc: Uint8Array, offset: number): Node {
|
|
|
83
58
|
|
|
84
59
|
/**
|
|
85
60
|
* Return the byte length of the node at `offset`.
|
|
86
|
-
* @throws {CallciumError} When the descriptor
|
|
61
|
+
* @throws {CallciumError} When the descriptor contains no readable node at `offset`.
|
|
87
62
|
*/
|
|
88
63
|
function nodeLength(desc: Uint8Array, offset: number): number {
|
|
89
64
|
return readNode(desc, offset).nodeLength;
|
|
90
65
|
}
|
|
91
66
|
|
|
92
|
-
/**
|
|
93
|
-
* Return the byte offset of the `fieldIndex`-th field inside a tuple node.
|
|
94
|
-
* Starts at the first field and skips `fieldIndex` nodes using `nodeLength`.
|
|
95
|
-
*/
|
|
96
|
-
function tupleFieldOffset(desc: Uint8Array, tupleOffset: number, fieldIndex: number): number {
|
|
97
|
-
let cursor = tupleOffset + DF.TUPLE_HEADER_SIZE;
|
|
98
|
-
for (let i = 0; i < fieldIndex; i++) {
|
|
99
|
-
cursor += nodeLength(desc, cursor);
|
|
100
|
-
}
|
|
101
|
-
return cursor;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
67
|
///////////////////////////////////////////////////////////////////////////
|
|
105
68
|
// Public interface
|
|
106
69
|
///////////////////////////////////////////////////////////////////////////
|
|
107
70
|
|
|
108
71
|
/**
|
|
109
72
|
* Return the declared top-level parameter count.
|
|
110
|
-
* @throws {CallciumError} When the descriptor is too short
|
|
73
|
+
* @throws {CallciumError} When the descriptor is too short for a header.
|
|
111
74
|
*/
|
|
112
75
|
function paramCount(desc: Uint8Array): number {
|
|
113
76
|
if (desc.length < DF.HEADER_SIZE) {
|
|
114
77
|
throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
|
|
115
78
|
}
|
|
116
|
-
return desc[
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Return byte offset of the N-th top-level param (0-indexed). */
|
|
120
|
-
function paramOffset(desc: Uint8Array, index: number): number {
|
|
121
|
-
const count = paramCount(desc);
|
|
122
|
-
if (index >= count) {
|
|
123
|
-
throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count})`);
|
|
124
|
-
}
|
|
125
|
-
let cursor = DF.HEADER_SIZE;
|
|
126
|
-
for (let i = 0; i < index; i++) {
|
|
127
|
-
cursor += nodeLength(desc, cursor);
|
|
128
|
-
}
|
|
129
|
-
return cursor;
|
|
79
|
+
return desc[DF.HEADER_PARAMCOUNT_OFFSET]!;
|
|
130
80
|
}
|
|
131
81
|
|
|
132
82
|
/**
|
|
@@ -134,91 +84,20 @@ function paramOffset(desc: Uint8Array, index: number): number {
|
|
|
134
84
|
* @param desc - Raw descriptor bytes.
|
|
135
85
|
* @param offset - Byte position of the node.
|
|
136
86
|
* @returns typeCode, isDynamic, and staticSize in bytes.
|
|
137
|
-
* @throws {CallciumError} When the descriptor
|
|
87
|
+
* @throws {CallciumError} When the descriptor contains no readable node at `offset`.
|
|
138
88
|
*/
|
|
139
89
|
function inspect(desc: Uint8Array, offset: number): TypeInfo {
|
|
140
90
|
const { typeCode, staticWords } = readNode(desc, offset);
|
|
141
91
|
return { typeCode, isDynamic: staticWords === 0, staticSize: staticWords * 32 };
|
|
142
92
|
}
|
|
143
93
|
|
|
144
|
-
/**
|
|
145
|
-
* Resolve the type at a calldata path (array of step indices).
|
|
146
|
-
*
|
|
147
|
-
* The first step selects a top-level param. Each subsequent step descends
|
|
148
|
-
* into the current node: for tuples it selects a field, for arrays it
|
|
149
|
-
* advances to the element descriptor.
|
|
150
|
-
*
|
|
151
|
-
* @param desc - Raw descriptor bytes.
|
|
152
|
-
* @param steps - Path steps, length >= 1.
|
|
153
|
-
* @returns TypeInfo for the node at the resolved path.
|
|
154
|
-
* @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent into elementary type.
|
|
155
|
-
*/
|
|
156
|
-
function typeAt(desc: Uint8Array, steps: number[]): TypeInfo {
|
|
157
|
-
return walkPath(desc, steps).typeInfo;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Resolve the type at the path and the declared length of the static array a
|
|
162
|
-
* quantifier step descends into (zero when the path holds no quantifier step
|
|
163
|
-
* over a static array).
|
|
164
|
-
* @param desc - Raw descriptor bytes.
|
|
165
|
-
* @param steps - Path steps, length >= 1.
|
|
166
|
-
* @returns TypeInfo at the resolved path and the quantified static array length.
|
|
167
|
-
* @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent into elementary type.
|
|
168
|
-
*/
|
|
169
|
-
function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quantifiedStaticLength: number } {
|
|
170
|
-
if (steps.length === 0) {
|
|
171
|
-
throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
let cursor = paramOffset(desc, steps[0]!);
|
|
175
|
-
let quantifiedStaticLength = 0;
|
|
176
|
-
|
|
177
|
-
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
|
|
178
|
-
const typeCode = desc[cursor]!;
|
|
179
|
-
const step = steps[stepIndex]!;
|
|
180
|
-
|
|
181
|
-
if (typeCode === TypeCode.TUPLE) {
|
|
182
|
-
const fields = tupleFieldCount(desc, cursor);
|
|
183
|
-
if (step >= fields) {
|
|
184
|
-
throw new CallciumError(
|
|
185
|
-
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
186
|
-
`Tuple field index ${step} out of range (tuple has ${fields} fields)`,
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
cursor = tupleFieldOffset(desc, cursor, step);
|
|
190
|
-
} else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) {
|
|
191
|
-
// Quantifier sentinels descend into the element type; only concrete
|
|
192
|
-
// indices are bounds-checked against the declared array length.
|
|
193
|
-
if (typeCode === TypeCode.STATIC_ARRAY) {
|
|
194
|
-
if (isQuantifier(step)) {
|
|
195
|
-
quantifiedStaticLength = staticArrayLength(desc, cursor);
|
|
196
|
-
} else {
|
|
197
|
-
const length = staticArrayLength(desc, cursor);
|
|
198
|
-
if (step >= length) {
|
|
199
|
-
throw new CallciumError(
|
|
200
|
-
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
201
|
-
`Array index ${step} out of range (length=${length})`,
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
cursor = arrayElementOffset(cursor);
|
|
207
|
-
} else {
|
|
208
|
-
throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return { typeInfo: inspect(desc, cursor), quantifiedStaticLength };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
94
|
/**
|
|
216
95
|
* Return tuple field count at a tuple node offset.
|
|
217
96
|
* @param desc - Raw descriptor bytes.
|
|
218
97
|
* @param offset - Byte position of the tuple node.
|
|
219
98
|
*/
|
|
220
99
|
function tupleFieldCount(desc: Uint8Array, offset: number): number {
|
|
221
|
-
return readU16(desc, offset +
|
|
100
|
+
return readU16(desc, offset + DF.TYPECODE_SIZE + DF.COMPOSITE_META_SIZE);
|
|
222
101
|
}
|
|
223
102
|
|
|
224
103
|
/**
|
|
@@ -241,188 +120,99 @@ function arrayElementOffset(offset: number): number {
|
|
|
241
120
|
return offset + DF.ARRAY_HEADER_SIZE;
|
|
242
121
|
}
|
|
243
122
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** Return the combined head slot span of the `count` nodes at `offset`, and the offset of the node past them. */
|
|
250
|
-
function headSpan(desc: Uint8Array, offset: number, count: number): { span: number; nodeOffset: number } {
|
|
251
|
-
let span = 0;
|
|
252
|
-
let nodeOffset = offset;
|
|
253
|
-
for (let i = 0; i < count; i++) {
|
|
254
|
-
const node = inspect(desc, nodeOffset);
|
|
255
|
-
// An indirected node occupies a single offset word.
|
|
256
|
-
span += node.isDynamic ? 32 : node.staticSize;
|
|
257
|
-
nodeOffset += nodeLength(desc, nodeOffset);
|
|
258
|
-
}
|
|
259
|
-
return { span, nodeOffset };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Return the meta word describing the array node at `arrayOffset`, and whether its elements are dynamic. */
|
|
263
|
-
function arrayMeta(desc: Uint8Array, arrayOffset: number, code: number): { meta: number; elemIsDynamic: boolean } {
|
|
264
|
-
const element = inspect(desc, arrayElementOffset(arrayOffset));
|
|
265
|
-
// A dynamic element occupies one offset word within the element region.
|
|
266
|
-
let meta = element.isDynamic ? 1 : element.staticSize >> 5;
|
|
267
|
-
if (element.isDynamic) meta |= PF.HINT_META_ELEM_DYNAMIC;
|
|
268
|
-
if (code === TypeCode.DYNAMIC_ARRAY) meta |= PF.HINT_META_DYNAMIC_ARRAY;
|
|
269
|
-
return { meta, elemIsDynamic: element.isDynamic };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** Append the hop entering an indirected node's payload and rebase the offset accumulator. */
|
|
273
|
-
function enterNode(walk: HintWalk, needed: boolean): void {
|
|
274
|
-
if (!needed) return;
|
|
275
|
-
walk.chain.push([walk.delta, PF.HINT_NO_INDEX, 0]);
|
|
276
|
-
walk.delta = 0;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** Apply one path step to `walk` and return the descriptor offset the step reaches. */
|
|
280
|
-
function walkStep(desc: Uint8Array, walk: HintWalk, descOffset: number, step: number, stepIndex: number): number {
|
|
281
|
-
const node = inspect(desc, descOffset);
|
|
123
|
+
///////////////////////////////////////////////////////////////////////////
|
|
124
|
+
// Metadata validity
|
|
125
|
+
///////////////////////////////////////////////////////////////////////////
|
|
282
126
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (step >= tupleFieldCount(desc, descOffset)) throw uncompilablePath(stepIndex);
|
|
286
|
-
enterNode(walk, node.isDynamic && !walk.entered);
|
|
127
|
+
/** Composite node metadata that disagrees with the node's children. */
|
|
128
|
+
export type MetaFault = "NODE_LENGTH" | "STATIC_WORDS";
|
|
287
129
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
walk.entered = false;
|
|
291
|
-
return field.nodeOffset;
|
|
292
|
-
}
|
|
130
|
+
/** A node's span and its type's static word count, or the first node whose metadata disagrees with them. */
|
|
131
|
+
type MetaWalk = { span: number; staticWords: number } | { fault: MetaFault; offset: number };
|
|
293
132
|
|
|
294
|
-
|
|
295
|
-
|
|
133
|
+
/** Walk the node at `offset` by the spans of its children. */
|
|
134
|
+
function walkMeta(desc: Uint8Array, offset: number): MetaWalk {
|
|
135
|
+
const node = readNode(desc, offset);
|
|
136
|
+
if (lookupTypeCode(node.typeCode).typeClass === "elementary") {
|
|
137
|
+
return { span: DF.TYPECODE_SIZE, staticWords: node.staticWords };
|
|
296
138
|
}
|
|
297
|
-
if (node.typeCode === TypeCode.STATIC_ARRAY && !isQuantifier(step) && step >= staticArrayLength(desc, descOffset)) {
|
|
298
|
-
throw uncompilablePath(stepIndex);
|
|
299
|
-
}
|
|
300
|
-
const { meta, elemIsDynamic } = arrayMeta(desc, descOffset, node.typeCode);
|
|
301
|
-
|
|
302
|
-
if (isQuantifier(step)) {
|
|
303
|
-
if (walk.kind !== PF.HINT_KIND_NONE) throw uncompilablePath(stepIndex);
|
|
304
|
-
enterNode(walk, node.isDynamic && !walk.entered);
|
|
305
139
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
140
|
+
let span: number;
|
|
141
|
+
let staticWords = 0;
|
|
142
|
+
if (node.typeCode === TypeCode.TUPLE) {
|
|
143
|
+
span = DF.TUPLE_HEADER_SIZE;
|
|
144
|
+
let hasDynamicField = false;
|
|
145
|
+
const fieldCount = tupleFieldCount(desc, offset);
|
|
146
|
+
for (let i = 0; i < fieldCount; i++) {
|
|
147
|
+
const field = walkMeta(desc, offset + span);
|
|
148
|
+
if ("fault" in field) return field;
|
|
149
|
+
span += field.span;
|
|
150
|
+
staticWords += field.staticWords;
|
|
151
|
+
if (field.staticWords === 0) hasDynamicField = true;
|
|
152
|
+
}
|
|
153
|
+
if (hasDynamicField) staticWords = 0;
|
|
318
154
|
} else {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** Serialize hop entries at `offset` and return the offset past them. */
|
|
328
|
-
function writeHops(out: Uint8Array, offset: number, hops: Hop[]): number {
|
|
329
|
-
for (const [delta, index, meta] of hops) {
|
|
330
|
-
writeBE32(out, offset, delta);
|
|
331
|
-
writeBE16(out, offset + PF.HINT_HOP_INDEX_OFFSET, index);
|
|
332
|
-
writeBE16(out, offset + PF.HINT_HOP_META_OFFSET, meta);
|
|
333
|
-
offset += PF.HINT_HOP_SIZE;
|
|
155
|
+
const element = walkMeta(desc, arrayElementOffset(offset));
|
|
156
|
+
if ("fault" in element) return element;
|
|
157
|
+
span = DF.ARRAY_HEADER_SIZE + element.span;
|
|
158
|
+
if (node.typeCode === TypeCode.STATIC_ARRAY) {
|
|
159
|
+
staticWords = element.staticWords * readU16(desc, offset + span);
|
|
160
|
+
span += DF.ARRAY_LENGTH_SIZE;
|
|
161
|
+
}
|
|
334
162
|
}
|
|
335
|
-
return offset;
|
|
163
|
+
if (span !== node.nodeLength) return { fault: "NODE_LENGTH", offset };
|
|
164
|
+
if (staticWords !== node.staticWords) return { fault: "STATIC_WORDS", offset };
|
|
165
|
+
return { span, staticWords };
|
|
336
166
|
}
|
|
337
167
|
|
|
338
|
-
/**
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const frameSize = quantified
|
|
352
|
-
? PF.HINT_FRAME_PREFIX_SIZE + PF.HINT_HEADER_SIZE + suffixHopCount * PF.HINT_HOP_SIZE
|
|
353
|
-
: 0;
|
|
354
|
-
const out = new Uint8Array(PF.HINT_HEADER_SIZE + mainHopCount * PF.HINT_HOP_SIZE + frameSize + PF.HINT_TARGET_SIZE);
|
|
355
|
-
|
|
356
|
-
let offset = 0;
|
|
357
|
-
out[offset] = (walk.kind << PF.HINT_KIND_SHIFT) | mainHopCount;
|
|
358
|
-
offset = writeHops(out, offset + PF.HINT_HEADER_SIZE, walk.mainHops);
|
|
359
|
-
|
|
360
|
-
if (quantified) {
|
|
361
|
-
writeBE32(out, offset, walk.frame.arrayDelta);
|
|
362
|
-
writeBE16(out, offset + PF.HINT_FRAME_COUNT_OFFSET, walk.frame.count);
|
|
363
|
-
writeBE16(out, offset + PF.HINT_FRAME_META_OFFSET, walk.frame.meta);
|
|
364
|
-
out[offset + PF.HINT_FRAME_PREFIX_SIZE] = suffixHopCount;
|
|
365
|
-
offset = writeHops(out, offset + PF.HINT_FRAME_PREFIX_SIZE + PF.HINT_HEADER_SIZE, suffixHops);
|
|
168
|
+
/**
|
|
169
|
+
* Find the first composite node whose metadata disagrees with its children (DV-1, DV-2). A node's children are checked
|
|
170
|
+
* before the node.
|
|
171
|
+
* @param desc - A well-formed descriptor.
|
|
172
|
+
* @returns The disagreement and the node's offset, or `undefined` when every node agrees.
|
|
173
|
+
*/
|
|
174
|
+
function tryValidateMeta(desc: Uint8Array): { fault: MetaFault; offset: number } | undefined {
|
|
175
|
+
let cursor = DF.HEADER_SIZE;
|
|
176
|
+
const count = paramCount(desc);
|
|
177
|
+
for (let i = 0; i < count; i++) {
|
|
178
|
+
const walk = walkMeta(desc, cursor);
|
|
179
|
+
if ("fault" in walk) return walk;
|
|
180
|
+
cursor += walk.span;
|
|
366
181
|
}
|
|
367
|
-
|
|
368
|
-
writeBE32(out, offset, walk.delta);
|
|
369
|
-
writeBE16(out, offset + PF.HINT_TARGET_META_OFFSET, targetMeta);
|
|
370
|
-
out[offset + PF.HINT_TARGET_TYPECODE_OFFSET] = typeCode;
|
|
371
|
-
return out;
|
|
182
|
+
return undefined;
|
|
372
183
|
}
|
|
373
184
|
|
|
374
185
|
/**
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
* @
|
|
378
|
-
* @param steps - Path steps, length >= 1.
|
|
379
|
-
* @returns The hint block bytes.
|
|
380
|
-
* @throws {CallciumError} When a step leaves the structure the descriptor declares, quantifies over
|
|
381
|
-
* a non-array node, or repeats a quantifier.
|
|
186
|
+
* Validate every composite node's metadata against its children (DV-1, DV-2).
|
|
187
|
+
* @param desc - A well-formed descriptor.
|
|
188
|
+
* @throws {CallciumError} With `NODE_LENGTH_MISMATCH` or `STATIC_WORDS_MISMATCH` at the first node at fault.
|
|
382
189
|
*/
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
|
|
190
|
+
function validateMeta(desc: Uint8Array): void {
|
|
191
|
+
const found = tryValidateMeta(desc);
|
|
192
|
+
if (found?.fault === "NODE_LENGTH") {
|
|
193
|
+
throw new CallciumError(
|
|
194
|
+
"NODE_LENGTH_MISMATCH",
|
|
195
|
+
"Composite node length differs from the span of its children",
|
|
196
|
+
found.offset,
|
|
197
|
+
);
|
|
386
198
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const walk: HintWalk = {
|
|
394
|
-
chain: [],
|
|
395
|
-
mainHops: [],
|
|
396
|
-
delta: arg.span,
|
|
397
|
-
frame: { arrayDelta: 0, count: 0, meta: 0 },
|
|
398
|
-
kind: PF.HINT_KIND_NONE,
|
|
399
|
-
entered: false,
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
let descOffset = arg.nodeOffset;
|
|
403
|
-
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
|
|
404
|
-
descOffset = walkStep(desc, walk, descOffset, steps[stepIndex]!, stepIndex);
|
|
199
|
+
if (found?.fault === "STATIC_WORDS") {
|
|
200
|
+
throw new CallciumError(
|
|
201
|
+
"STATIC_WORDS_MISMATCH",
|
|
202
|
+
"Composite node static word count differs from its type's head size",
|
|
203
|
+
found.offset,
|
|
204
|
+
);
|
|
405
205
|
}
|
|
406
|
-
|
|
407
|
-
const target = inspect(desc, descOffset);
|
|
408
|
-
enterNode(walk, target.isDynamic && !walk.entered);
|
|
409
|
-
|
|
410
|
-
const targetMeta = target.typeCode === TypeCode.DYNAMIC_ARRAY ? arrayMeta(desc, descOffset, target.typeCode).meta : 0;
|
|
411
|
-
|
|
412
|
-
return encodeHint(walk, targetMeta, target.typeCode, steps.length);
|
|
413
206
|
}
|
|
414
207
|
|
|
415
208
|
/** Inspect and navigate raw descriptor bytes. */
|
|
416
209
|
export const Descriptor = {
|
|
417
210
|
paramCount,
|
|
418
|
-
paramOffset,
|
|
419
211
|
inspect,
|
|
420
|
-
typeAt,
|
|
421
|
-
tupleFieldOffset,
|
|
422
212
|
tupleFieldCount,
|
|
423
213
|
staticArrayLength,
|
|
424
214
|
nodeLength,
|
|
425
215
|
arrayElementOffset,
|
|
426
|
-
|
|
427
|
-
|
|
216
|
+
validateMeta,
|
|
217
|
+
tryValidateMeta,
|
|
428
218
|
};
|
package/src/errors.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
+
import type { Issue, Violation } from "./types";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Machine-readable error code for structural decoding and validation failures.
|
|
3
5
|
*
|
|
4
|
-
* Each code names one invariant. Where Solidity detects the same invariant, the code is its error
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
+
* Each code names one invariant. Where Solidity detects the same invariant, the code is its error name transliterated
|
|
7
|
+
* from `PascalCase` to `SCREAMING_SNAKE_CASE`. The codes under "Inputs and validation" have no counterpart: they name
|
|
8
|
+
* input a Solidity signature cannot express.
|
|
9
9
|
*/
|
|
10
10
|
export type CallciumErrorCode =
|
|
11
11
|
// Policy and descriptor framing.
|
|
12
12
|
| "UNSUPPORTED_VERSION"
|
|
13
|
+
| "UNSUPPORTED_MINOR"
|
|
14
|
+
| "MINOR_MISMATCH"
|
|
13
15
|
| "MALFORMED_HEADER"
|
|
14
16
|
| "UNEXPECTED_END"
|
|
15
17
|
| "TRAILING_BYTES"
|
|
16
18
|
// Groups and rules.
|
|
17
19
|
| "EMPTY_POLICY"
|
|
18
20
|
| "EMPTY_GROUP"
|
|
19
|
-
| "GROUP_TOO_SMALL"
|
|
20
21
|
| "GROUP_SIZE_MISMATCH"
|
|
21
22
|
| "GROUP_OVERFLOW"
|
|
22
23
|
| "RULE_TOO_SMALL"
|
|
23
24
|
| "RULE_SIZE_MISMATCH"
|
|
24
|
-
| "RULE_FIELD_OUT_OF_BOUNDS"
|
|
25
25
|
| "RULE_OVERFLOW"
|
|
26
26
|
// Paths.
|
|
27
27
|
| "EMPTY_PATH"
|
|
@@ -31,22 +31,25 @@ export type CallciumErrorCode =
|
|
|
31
31
|
| "TUPLE_FIELD_OUT_OF_BOUNDS"
|
|
32
32
|
| "STATIC_ARRAY_INDEX_OUT_OF_BOUNDS"
|
|
33
33
|
| "NOT_COMPOSITE"
|
|
34
|
-
| "UNCOMPILABLE_PATH"
|
|
35
34
|
| "QUANTIFIER_ON_NON_ARRAY"
|
|
36
|
-
| "
|
|
35
|
+
| "UNKNOWN_PATH_STEP"
|
|
36
|
+
// Loops.
|
|
37
|
+
| "UNKNOWN_QUANTIFIER"
|
|
38
|
+
| "INVALID_NESTED_LOOP"
|
|
37
39
|
// Scope and context.
|
|
38
|
-
| "
|
|
40
|
+
| "UNKNOWN_SCOPE"
|
|
39
41
|
| "INVALID_CONTEXT_PATH"
|
|
40
42
|
| "UNKNOWN_CONTEXT_PROPERTY"
|
|
41
43
|
// Operators and hints.
|
|
42
44
|
| "UNKNOWN_OPERATOR"
|
|
43
45
|
| "INVALID_OPERATOR_BYTES"
|
|
44
46
|
| "OPERATOR_TARGET_MISMATCH"
|
|
47
|
+
| "INVALID_CONTEXT_OPERAND"
|
|
48
|
+
| "DATA_LENGTH_MISMATCH"
|
|
45
49
|
| "UNSORTED_IN_SET"
|
|
46
50
|
| "MALFORMED_HINT"
|
|
47
51
|
// Constraint building.
|
|
48
52
|
| "NO_CONSTRAINT_OPERATORS"
|
|
49
|
-
| "DUPLICATE_PATH_IN_GROUP"
|
|
50
53
|
| "EMPTY_SET"
|
|
51
54
|
| "SET_TOO_LARGE"
|
|
52
55
|
| "INVALID_RANGE"
|
|
@@ -62,6 +65,12 @@ export type CallciumErrorCode =
|
|
|
62
65
|
| "STATIC_WORDS_TOO_LARGE"
|
|
63
66
|
| "PARAM_COUNT_MISMATCH"
|
|
64
67
|
| "TOO_MANY_PARAMS"
|
|
68
|
+
| "NODE_LENGTH_MISMATCH"
|
|
69
|
+
| "STATIC_WORDS_MISMATCH"
|
|
70
|
+
// Type codes.
|
|
71
|
+
| "INVALID_UINT_BITS"
|
|
72
|
+
| "INVALID_INT_BITS"
|
|
73
|
+
| "INVALID_BYTES_LENGTH"
|
|
65
74
|
// Type strings and signatures.
|
|
66
75
|
| "MALFORMED_TYPE_STRING"
|
|
67
76
|
| "UNKNOWN_TYPE"
|
|
@@ -73,18 +82,19 @@ export type CallciumErrorCode =
|
|
|
73
82
|
| "RULE_COUNT_OVERFLOW"
|
|
74
83
|
| "RULE_SIZE_OVERFLOW"
|
|
75
84
|
| "DESC_LENGTH_OVERFLOW"
|
|
85
|
+
// Strict build.
|
|
86
|
+
| "VALIDATION_ERROR"
|
|
76
87
|
// Inputs and validation.
|
|
77
|
-
| "UNKNOWN_QUANTIFIER"
|
|
78
88
|
| "INVALID_HEX"
|
|
79
89
|
| "MALFORMED_SELECTOR"
|
|
80
90
|
| "PATH_STEP_OVERFLOW"
|
|
81
91
|
| "OPERAND_OVERFLOW"
|
|
82
92
|
| "CONTEXT_VALUE_OVERFLOW"
|
|
83
|
-
| "
|
|
93
|
+
| "MALFORMED_CONTEXT_VALUE"
|
|
94
|
+
| "MALFORMED_OPERAND";
|
|
84
95
|
|
|
85
96
|
/**
|
|
86
|
-
* Thrown
|
|
87
|
-
* The optional `offset` indicates the byte position in the source blob where the error was detected.
|
|
97
|
+
* Thrown on malformed or inadmissible SDK input. `offset`, when set, is the error's byte position in the source blob.
|
|
88
98
|
*/
|
|
89
99
|
export class CallciumError extends Error {
|
|
90
100
|
public readonly code: CallciumErrorCode;
|
|
@@ -96,7 +106,7 @@ export class CallciumError extends Error {
|
|
|
96
106
|
super(`${prefix}${message}`);
|
|
97
107
|
this.name = "CallciumError";
|
|
98
108
|
this.code = code;
|
|
99
|
-
//
|
|
109
|
+
// `exactOptionalPropertyTypes` admits no `undefined` in the optional property.
|
|
100
110
|
if (offset !== undefined) this.offset = offset;
|
|
101
111
|
}
|
|
102
112
|
}
|
|
@@ -104,14 +114,13 @@ export class CallciumError extends Error {
|
|
|
104
114
|
/**
|
|
105
115
|
* Thrown by `PolicyBuilder.build` when validation reports any issue.
|
|
106
116
|
*
|
|
107
|
-
* Carries every issue the validator found. The `Error.message` is the first issue's
|
|
108
|
-
*
|
|
109
|
-
* issues to users should iterate `issues` directly.
|
|
117
|
+
* Carries every issue the validator found. The `Error.message` is the first issue's message — a diagnostic summary, not
|
|
118
|
+
* a presentation contract. To render issues to users, iterate `issues` directly.
|
|
110
119
|
*/
|
|
111
120
|
export class ValidationError extends CallciumError {
|
|
112
|
-
public readonly issues:
|
|
121
|
+
public readonly issues: Issue[];
|
|
113
122
|
|
|
114
|
-
constructor(issues:
|
|
123
|
+
constructor(issues: Issue[]) {
|
|
115
124
|
super("VALIDATION_ERROR", issues[0]?.message ?? "Policy validation failed");
|
|
116
125
|
this.name = "ValidationError";
|
|
117
126
|
this.issues = issues;
|
|
@@ -119,18 +128,17 @@ export class ValidationError extends CallciumError {
|
|
|
119
128
|
}
|
|
120
129
|
|
|
121
130
|
/**
|
|
122
|
-
* Thrown by `PolicyEnforcer.enforce` when calldata fails policy enforcement.
|
|
123
|
-
*
|
|
131
|
+
* Thrown by `PolicyEnforcer.enforce` when calldata fails policy enforcement. Distinct from `ValidationError`, which
|
|
132
|
+
* signals static issues in the policy itself.
|
|
124
133
|
*
|
|
125
|
-
* Carries the full list of structured violations (one per failed group). The
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* rendering violations to users should iterate `violations` directly.
|
|
134
|
+
* Carries the full list of structured violations (one per failed group). The `Error.message` is a minimal non-lossy
|
|
135
|
+
* diagnostic summary built from the first violation's structured fields — not a presentation contract. To render
|
|
136
|
+
* violations to users, iterate `violations` directly.
|
|
129
137
|
*/
|
|
130
138
|
export class PolicyViolationError extends Error {
|
|
131
|
-
public readonly violations:
|
|
139
|
+
public readonly violations: Violation[];
|
|
132
140
|
|
|
133
|
-
constructor(violations:
|
|
141
|
+
constructor(violations: Violation[]) {
|
|
134
142
|
super(formatDiagnostic(violations[0]));
|
|
135
143
|
this.name = "PolicyViolationError";
|
|
136
144
|
this.violations = violations;
|
|
@@ -138,7 +146,7 @@ export class PolicyViolationError extends Error {
|
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
/** Build a stack-trace-friendly diagnostic line from a violation's code and coordinates. */
|
|
141
|
-
function formatDiagnostic(violation:
|
|
149
|
+
function formatDiagnostic(violation: Violation | undefined): string {
|
|
142
150
|
if (!violation) return "Policy violation";
|
|
143
151
|
const location = "group" in violation ? ` at group ${violation.group} rule ${violation.rule}` : "";
|
|
144
152
|
return `Policy violation: ${violation.code}${location}`;
|