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