@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/policy-coder.ts
CHANGED
|
@@ -1,704 +1,1328 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { concatBytes } from "@noble/hashes/utils.js";
|
|
2
2
|
|
|
3
|
-
import { bytesToHex, hexToBytes,
|
|
3
|
+
import { bigintToHex, bytesToHex, hexToBytes, readU16, readU32, toHex, writeBE16, writeBE32 } from "./bytes";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
comparisonOf,
|
|
6
6
|
fieldMax,
|
|
7
7
|
formatCode,
|
|
8
|
+
isComparisonCode,
|
|
9
|
+
isQuantifierCode,
|
|
10
|
+
isScopeCode,
|
|
11
|
+
isSourceCode,
|
|
12
|
+
LoopQuantifier,
|
|
8
13
|
MAX_CONTEXT_PROPERTY_ID,
|
|
9
14
|
Op,
|
|
10
15
|
PolicyFormat as PF,
|
|
11
16
|
Scope,
|
|
17
|
+
Source,
|
|
18
|
+
sourceOf,
|
|
12
19
|
TypeCode,
|
|
20
|
+
type QuantifierCode,
|
|
21
|
+
type ScopeCode,
|
|
13
22
|
} from "./constants";
|
|
14
23
|
import { Descriptor } from "./descriptor";
|
|
15
|
-
import { decodeDescriptor } from "./descriptor-coder";
|
|
16
|
-
import { CallciumError
|
|
24
|
+
import { decodeDescriptor, inspectDescriptor } from "./descriptor-coder";
|
|
25
|
+
import { CallciumError } from "./errors";
|
|
26
|
+
import { Hint } from "./hint";
|
|
17
27
|
import {
|
|
28
|
+
acceptsContextSource,
|
|
29
|
+
canonicalize,
|
|
30
|
+
contextPropertyType,
|
|
31
|
+
hasCalldataLength,
|
|
32
|
+
isLeftAligned,
|
|
18
33
|
isLengthOp,
|
|
19
|
-
|
|
34
|
+
isValidPayloadSize,
|
|
35
|
+
LENGTH_TYPE,
|
|
36
|
+
operandTypeOf,
|
|
37
|
+
operandWidth,
|
|
38
|
+
readMember,
|
|
39
|
+
readOperator,
|
|
20
40
|
toBigInt,
|
|
21
|
-
isAddressableTarget,
|
|
22
|
-
isValidOperatorData,
|
|
23
|
-
toOperatorBytes,
|
|
24
41
|
} from "./operators";
|
|
25
|
-
import { parsePathSteps } from "./path";
|
|
42
|
+
import { encodePath, parsePathSteps } from "./path";
|
|
26
43
|
|
|
44
|
+
import type { CallciumErrorCode } from "./errors";
|
|
45
|
+
import type { Hex } from "./types";
|
|
27
46
|
import type {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
ArrayMeta,
|
|
48
|
+
BodyRule,
|
|
49
|
+
DecodedFrameHint,
|
|
31
50
|
DecodedGroup,
|
|
32
|
-
|
|
51
|
+
DecodedHint,
|
|
52
|
+
DecodedHop,
|
|
53
|
+
DecodedOperator,
|
|
33
54
|
DecodedPolicy,
|
|
34
55
|
DecodedRule,
|
|
56
|
+
DecodedTest,
|
|
57
|
+
Field,
|
|
58
|
+
LoopRule,
|
|
35
59
|
PolicyData,
|
|
60
|
+
Rule,
|
|
36
61
|
} from "./types";
|
|
37
62
|
|
|
38
63
|
///////////////////////////////////////////////////////////////////////////
|
|
39
|
-
// Policy
|
|
64
|
+
// Policy validation
|
|
40
65
|
///////////////////////////////////////////////////////////////////////////
|
|
41
66
|
|
|
42
|
-
/** Wrap a value with its byte span for positional tracking. */
|
|
43
|
-
function field<T>(value: T, start: number, end: number): Field<T> {
|
|
44
|
-
return { value, span: { start, end } };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
67
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
68
|
+
* Validate a policy blob against every storage-time invariant.
|
|
69
|
+
* @param blob - The policy blob.
|
|
70
|
+
* @throws {CallciumError} On the first invariant the blob breaks, at the offset of the group or rule containing it.
|
|
51
71
|
*/
|
|
52
|
-
function
|
|
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
|
-
|
|
65
|
-
///////////////////////////////////////////////////////////////////////////
|
|
66
|
-
// Hint block
|
|
67
|
-
///////////////////////////////////////////////////////////////////////////
|
|
68
|
-
|
|
69
|
-
/** Resolve the hint block size at `hintStart` from its header and suffix header bytes. */
|
|
70
|
-
function resolveHintSize(data: Uint8Array, hintStart: number, ruleEnd: number, ruleOffset: number): number {
|
|
71
|
-
const overrun = (): never => {
|
|
72
|
-
throw new CallciumError("RULE_FIELD_OUT_OF_BOUNDS", "Rule is too small to hold a hint block", ruleOffset);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
if (hintStart + PF.HINT_HEADER_SIZE > ruleEnd) overrun();
|
|
76
|
-
const header = data[hintStart]!;
|
|
77
|
-
const hopsEnd = hintStart + PF.HINT_HEADER_SIZE + (header & PF.HINT_HOP_COUNT_MASK) * PF.HINT_HOP_SIZE;
|
|
78
|
-
let hintSize = hopsEnd - hintStart + PF.HINT_TARGET_SIZE;
|
|
79
|
-
|
|
80
|
-
// A quantifier frame sits between the main chain and the target, sized by its own header byte.
|
|
81
|
-
if (header >> PF.HINT_KIND_SHIFT !== PF.HINT_KIND_NONE) {
|
|
82
|
-
const suffixHeaderOffset = hopsEnd + PF.HINT_FRAME_PREFIX_SIZE;
|
|
83
|
-
if (suffixHeaderOffset >= ruleEnd) overrun();
|
|
84
|
-
const suffixHeader = data[suffixHeaderOffset]!;
|
|
85
|
-
hintSize +=
|
|
86
|
-
PF.HINT_FRAME_PREFIX_SIZE + PF.HINT_HEADER_SIZE + (suffixHeader & PF.HINT_HOP_COUNT_MASK) * PF.HINT_HOP_SIZE;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (hintStart + hintSize > ruleEnd) overrun();
|
|
90
|
-
return hintSize;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Throw a MALFORMED_HINT error with the given detail. */
|
|
94
|
-
function malformedHint(detail: string, ruleOffset: number): never {
|
|
95
|
-
throw new CallciumError("MALFORMED_HINT", detail, ruleOffset);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Require every hop entry in `[start, end)` to carry no reserved or unused state. */
|
|
99
|
-
function validateHops(data: Uint8Array, start: number, end: number, ruleOffset: number): void {
|
|
100
|
-
for (let offset = start; offset < end; offset += PF.HINT_HOP_SIZE) {
|
|
101
|
-
const index = readU16(data, offset + PF.HINT_HOP_INDEX_OFFSET);
|
|
102
|
-
if (index === PF.HINT_INDEX_RESERVED) malformedHint("Hop index is a reserved value", ruleOffset);
|
|
103
|
-
|
|
104
|
-
const meta = readU16(data, offset + PF.HINT_HOP_META_OFFSET);
|
|
105
|
-
// A plain hop carries no element meta; an element hop addresses no offset of its own.
|
|
106
|
-
if (index === PF.HINT_NO_INDEX) {
|
|
107
|
-
if (meta !== 0) malformedHint("Plain hop carries a meta word", ruleOffset);
|
|
108
|
-
} else {
|
|
109
|
-
if (readU32(data, offset) !== 0) malformedHint("Element hop carries a delta", ruleOffset);
|
|
110
|
-
if ((meta & PF.HINT_META_RESERVED_MASK) !== 0) malformedHint("Hop meta carries reserved bits", ruleOffset);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Require the hint block at `hintStart` to carry no reserved or unused state. */
|
|
116
|
-
function validateHint(data: Uint8Array, hintStart: number, hintSize: number, ruleOffset: number): void {
|
|
117
|
-
const header = data[hintStart]!;
|
|
118
|
-
const kind = header >> PF.HINT_KIND_SHIFT;
|
|
119
|
-
if (kind > PF.HINT_KIND_MAX) malformedHint("Header kind is a reserved value", ruleOffset);
|
|
120
|
-
|
|
121
|
-
const hopsStart = hintStart + PF.HINT_HEADER_SIZE;
|
|
122
|
-
const hopsEnd = hopsStart + (header & PF.HINT_HOP_COUNT_MASK) * PF.HINT_HOP_SIZE;
|
|
123
|
-
validateHops(data, hopsStart, hopsEnd, ruleOffset);
|
|
124
|
-
|
|
125
|
-
const targetOffset = hintStart + hintSize - PF.HINT_TARGET_SIZE;
|
|
126
|
-
if (kind !== PF.HINT_KIND_NONE) {
|
|
127
|
-
const frameMeta = readU16(data, hopsEnd + PF.HINT_FRAME_META_OFFSET);
|
|
128
|
-
if ((frameMeta & PF.HINT_META_RESERVED_MASK) !== 0) malformedHint("Frame meta carries reserved bits", ruleOffset);
|
|
129
|
-
const suffixHeader = data[hopsEnd + PF.HINT_FRAME_PREFIX_SIZE]!;
|
|
130
|
-
if ((suffixHeader & PF.HINT_SUFFIX_RESERVED_MASK) !== 0) {
|
|
131
|
-
malformedHint("Suffix header carries reserved bits", ruleOffset);
|
|
132
|
-
}
|
|
133
|
-
validateHops(data, hopsEnd + PF.HINT_FRAME_PREFIX_SIZE + PF.HINT_HEADER_SIZE, targetOffset, ruleOffset);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// A target meta word describes a dynamic array and is absent for every other type.
|
|
137
|
-
const targetMeta = readU16(data, targetOffset + PF.HINT_TARGET_META_OFFSET);
|
|
138
|
-
const targetTypeCode = data[targetOffset + PF.HINT_TARGET_TYPECODE_OFFSET]!;
|
|
139
|
-
const targetMetaOk =
|
|
140
|
-
targetTypeCode === TypeCode.DYNAMIC_ARRAY ? (targetMeta & PF.HINT_META_RESERVED_MASK) === 0 : targetMeta === 0;
|
|
141
|
-
if (!targetMetaOk) malformedHint("Target meta carries reserved or unused state", ruleOffset);
|
|
142
|
-
|
|
143
|
-
// An operator reads either a scalar word or a declared length, so a target carrying neither —
|
|
144
|
-
// a tuple, a static array, or an undefined code — addresses nothing.
|
|
145
|
-
if (!isAddressableTarget(targetTypeCode)) malformedHint("Target type code addresses no value", ruleOffset);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Read the type code the hint block at `hintStart` declares for its target. */
|
|
149
|
-
function hintTargetTypeCode(data: Uint8Array, hintStart: number, hintSize: number): number {
|
|
150
|
-
return data[hintStart + hintSize - PF.HINT_TARGET_SIZE + PF.HINT_TARGET_TYPECODE_OFFSET]!;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
///////////////////////////////////////////////////////////////////////////
|
|
154
|
-
// Policy decoder
|
|
155
|
-
///////////////////////////////////////////////////////////////////////////
|
|
156
|
-
|
|
157
|
-
/** Decode a policy blob, returning the structural representation and the blob bytes it spans. */
|
|
158
|
-
export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Array } {
|
|
72
|
+
export function validatePolicy(blob: Hex): void {
|
|
159
73
|
const data = hexToBytes(blob);
|
|
160
74
|
if (data.length < PF.DESC_OFFSET) {
|
|
161
75
|
throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short");
|
|
162
76
|
}
|
|
163
77
|
|
|
164
78
|
const headerByte = data[0]!;
|
|
165
|
-
const
|
|
166
|
-
if (
|
|
167
|
-
throw new CallciumError("UNSUPPORTED_VERSION", `
|
|
79
|
+
const major = headerByte >> PF.MAJOR_SHIFT;
|
|
80
|
+
if (major !== PF.MAJOR) {
|
|
81
|
+
throw new CallciumError("UNSUPPORTED_VERSION", `Major version ${major} is not supported (expected ${PF.MAJOR})`);
|
|
168
82
|
}
|
|
169
|
-
|
|
170
|
-
|
|
83
|
+
// A blob that uses no extension declares MINOR, the lowest minor covering its features.
|
|
84
|
+
const minor = (headerByte >> PF.MINOR_SHIFT) & PF.MINOR_MASK;
|
|
85
|
+
if (minor !== PF.MINOR) {
|
|
86
|
+
throw new CallciumError(
|
|
87
|
+
"MINOR_MISMATCH",
|
|
88
|
+
`Minor version ${minor} is not the lowest covering the policy's features (expected ${PF.MINOR})`,
|
|
89
|
+
);
|
|
171
90
|
}
|
|
172
91
|
|
|
173
|
-
const selectorStart = PF.SELECTOR_OFFSET;
|
|
174
|
-
const selectorEnd = selectorStart + PF.SELECTOR_SIZE;
|
|
175
|
-
const selectorHex = toHex(data, selectorStart, selectorEnd);
|
|
176
|
-
|
|
177
92
|
const isSelectorless = (headerByte & PF.FLAG_NO_SELECTOR) !== 0;
|
|
178
|
-
if (isSelectorless &&
|
|
93
|
+
if (isSelectorless && readU32(data, PF.SELECTOR_OFFSET) !== 0) {
|
|
179
94
|
throw new CallciumError("MALFORMED_HEADER", "Selectorless policy must have a zeroed selector slot");
|
|
180
95
|
}
|
|
181
96
|
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
if (descLengthValue < 2) {
|
|
185
|
-
throw new CallciumError("MALFORMED_HEADER", "Descriptor length must be at least 2");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const descStart = PF.DESC_OFFSET;
|
|
189
|
-
const descEnd = descStart + descLengthValue;
|
|
190
|
-
const groupCountOffset = descEnd;
|
|
191
|
-
|
|
97
|
+
const descLengthValue = readU16(data, PF.DESC_LENGTH_OFFSET);
|
|
98
|
+
const groupCountOffset = PF.DESC_OFFSET + descLengthValue;
|
|
192
99
|
if (data.length < groupCountOffset + PF.GROUP_COUNT_SIZE) {
|
|
193
100
|
throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short for descriptor and group count");
|
|
194
101
|
}
|
|
102
|
+
decodeDescriptor(data.subarray(PF.DESC_OFFSET, groupCountOffset));
|
|
195
103
|
|
|
196
|
-
// Decode the embedded descriptor, offsetting spans to be relative to the policy blob.
|
|
197
|
-
const descSlice = data.subarray(descStart, descEnd);
|
|
198
|
-
const { descriptor: desc } = decodeDescriptor(descSlice);
|
|
199
|
-
const params: DecodedParam[] = desc.params.map((param) => ({
|
|
200
|
-
...param,
|
|
201
|
-
span: {
|
|
202
|
-
start: descStart + param.span.start,
|
|
203
|
-
end: descStart + param.span.end,
|
|
204
|
-
},
|
|
205
|
-
}));
|
|
206
|
-
|
|
207
|
-
const descriptorRaw = hexField(data, descStart, descEnd);
|
|
208
|
-
|
|
209
|
-
const groupCountStart = groupCountOffset;
|
|
210
|
-
const groupCountEnd = groupCountStart + PF.GROUP_COUNT_SIZE;
|
|
211
104
|
const groupCountValue = data[groupCountOffset]!;
|
|
212
105
|
if (groupCountValue === 0) {
|
|
213
106
|
throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
|
|
214
107
|
}
|
|
215
108
|
|
|
216
|
-
let offset =
|
|
217
|
-
const groups: DecodedGroup[] = [];
|
|
218
|
-
|
|
109
|
+
let offset = groupCountOffset + PF.GROUP_COUNT_SIZE;
|
|
219
110
|
for (let groupIndex = 0; groupIndex < groupCountValue; groupIndex++) {
|
|
220
111
|
if (offset + PF.GROUP_HEADER_SIZE > data.length) {
|
|
221
112
|
throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading group header", offset);
|
|
222
113
|
}
|
|
223
114
|
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const groupBodyStart = offset + PF.GROUP_HEADER_SIZE;
|
|
227
|
-
const groupEnd = groupBodyStart + groupSizeValue;
|
|
228
|
-
|
|
115
|
+
const groupSizeValue = readU32(data, offset + PF.GROUP_RULECOUNT_SIZE);
|
|
116
|
+
const groupEnd = offset + PF.GROUP_HEADER_SIZE + groupSizeValue;
|
|
229
117
|
if (groupEnd > data.length) {
|
|
230
118
|
throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
|
|
231
119
|
}
|
|
232
120
|
|
|
233
|
-
const
|
|
234
|
-
const ruleCountValue = readU16(data, ruleCountStart);
|
|
121
|
+
const ruleCountValue = readU16(data, offset);
|
|
235
122
|
if (ruleCountValue === 0) {
|
|
236
123
|
throw new CallciumError("EMPTY_GROUP", "Group must contain at least one rule", offset);
|
|
237
124
|
}
|
|
238
|
-
|
|
239
|
-
|
|
125
|
+
|
|
126
|
+
let ruleOffset = offset + PF.GROUP_HEADER_SIZE;
|
|
127
|
+
for (let ruleIndex = 0; ruleIndex < ruleCountValue; ruleIndex++) {
|
|
128
|
+
if (ruleOffset + PF.RULE_SIZE_SIZE > groupEnd) groupSizeMismatch(offset);
|
|
129
|
+
ruleOffset = validateRule(data, ruleOffset, groupEnd);
|
|
240
130
|
}
|
|
241
131
|
|
|
242
|
-
|
|
243
|
-
|
|
132
|
+
if (ruleOffset !== groupEnd) groupSizeMismatch(offset);
|
|
133
|
+
offset = groupEnd;
|
|
134
|
+
}
|
|
244
135
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
136
|
+
// Reject trailing bytes after the last group.
|
|
137
|
+
if (offset !== data.length) {
|
|
138
|
+
throw new CallciumError("TRAILING_BYTES", "Trailing bytes after last group", offset);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
249
141
|
|
|
250
|
-
|
|
251
|
-
|
|
142
|
+
/** Validate a single rule and return the offset of the next rule. */
|
|
143
|
+
function validateRule(data: Uint8Array, ruleOffset: number, groupEnd: number): number {
|
|
144
|
+
const ruleEnd = ruleOffset + ruleSize(data, ruleOffset, groupEnd);
|
|
145
|
+
const scopeValue = scope(data, ruleOffset, ruleEnd);
|
|
146
|
+
switch (scopeValue) {
|
|
147
|
+
case Scope.LOOP:
|
|
148
|
+
validateLoop(data, ruleOffset, ruleEnd);
|
|
149
|
+
break;
|
|
150
|
+
case Scope.CONTEXT:
|
|
151
|
+
case Scope.CALLDATA:
|
|
152
|
+
validateTest(data, ruleOffset, ruleEnd, scopeValue, 0);
|
|
153
|
+
break;
|
|
154
|
+
default:
|
|
155
|
+
return scopeValue satisfies never;
|
|
156
|
+
}
|
|
157
|
+
return ruleEnd;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Return the size of the rule at `ruleOffset`, which must lie within `end`. */
|
|
161
|
+
function ruleSize(data: Uint8Array, ruleOffset: number, end: number): number {
|
|
162
|
+
const ruleSizeValue = readU16(data, ruleOffset);
|
|
163
|
+
if (ruleSizeValue < PF.RULE_SIZE_SIZE) {
|
|
164
|
+
throw new CallciumError(
|
|
165
|
+
"RULE_TOO_SMALL",
|
|
166
|
+
`Rule size ${ruleSizeValue} is below minimum ${PF.RULE_SIZE_SIZE}`,
|
|
167
|
+
ruleOffset,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (ruleOffset + ruleSizeValue > end) {
|
|
171
|
+
throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
|
|
172
|
+
}
|
|
173
|
+
return ruleSizeValue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Return the scope of the rule at `ruleOffset`, which must be a defined value. */
|
|
177
|
+
function scope(data: Uint8Array, ruleOffset: number, ruleEnd: number): ScopeCode {
|
|
178
|
+
if (ruleOffset + PF.RULE_SCOPE_OFFSET + PF.RULE_SCOPE_SIZE > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
179
|
+
const scopeValue = data[ruleOffset + PF.RULE_SCOPE_OFFSET]!;
|
|
180
|
+
if (!isScopeCode(scopeValue)) {
|
|
181
|
+
throw new CallciumError("UNKNOWN_SCOPE", `Unknown scope value ${scopeValue}`, ruleOffset);
|
|
182
|
+
}
|
|
183
|
+
return scopeValue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Validate the loop rule at `ruleOffset` and the tests of its body. */
|
|
187
|
+
function validateLoop(data: Uint8Array, ruleOffset: number, ruleEnd: number): void {
|
|
188
|
+
if (ruleOffset + PF.RULE_QUANTIFIER_OFFSET + PF.RULE_QUANTIFIER_SIZE > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
189
|
+
const quantifier = data[ruleOffset + PF.RULE_QUANTIFIER_OFFSET]!;
|
|
190
|
+
if (!isQuantifierCode(quantifier)) {
|
|
191
|
+
throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier value ${quantifier}`, ruleOffset);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const { pathEnd: hintOffset, depth } = validatePath(data, ruleOffset, ruleOffset + PF.LOOP_DEPTH_OFFSET, ruleEnd);
|
|
195
|
+
if (depth === 0) {
|
|
196
|
+
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
|
|
197
|
+
}
|
|
198
|
+
if (depth > PF.MAX_PATH_DEPTH) pathTooDeep(depth, ruleOffset);
|
|
199
|
+
|
|
200
|
+
const frameOffset = validateHint(data, ruleOffset, hintOffset, ruleEnd);
|
|
201
|
+
if (frameOffset + PF.HINT_FRAME_PREFIX_SIZE + PF.GROUP_RULECOUNT_SIZE > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
202
|
+
const frameMeta = readU16(data, frameOffset + PF.HINT_FRAME_META_OFFSET);
|
|
203
|
+
if ((frameMeta & PF.HINT_META_RESERVED_MASK) !== 0) malformedHint("Frame meta carries reserved bits", ruleOffset);
|
|
204
|
+
|
|
205
|
+
const bodyOffset = frameOffset + PF.HINT_FRAME_PREFIX_SIZE;
|
|
206
|
+
const ruleCountValue = readU16(data, bodyOffset);
|
|
207
|
+
if (ruleCountValue === 0) {
|
|
208
|
+
throw new CallciumError("EMPTY_GROUP", "Loop body must contain at least one rule", ruleOffset);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let testOffset = bodyOffset + PF.GROUP_RULECOUNT_SIZE;
|
|
212
|
+
for (let ruleIndex = 0; ruleIndex < ruleCountValue; ruleIndex++) {
|
|
213
|
+
if (testOffset + PF.RULE_SIZE_SIZE > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
214
|
+
const testEnd = testOffset + ruleSize(data, testOffset, ruleEnd);
|
|
215
|
+
const scopeValue = scope(data, testOffset, testEnd);
|
|
216
|
+
switch (scopeValue) {
|
|
217
|
+
case Scope.LOOP:
|
|
218
|
+
throw new CallciumError("INVALID_NESTED_LOOP", "Loop body holds a loop", testOffset);
|
|
219
|
+
case Scope.CONTEXT:
|
|
220
|
+
case Scope.CALLDATA:
|
|
221
|
+
validateTest(data, testOffset, testEnd, scopeValue, depth);
|
|
222
|
+
break;
|
|
223
|
+
default:
|
|
224
|
+
return scopeValue satisfies never;
|
|
225
|
+
}
|
|
226
|
+
testOffset = testEnd;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (testOffset !== ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Validate the context or calldata test at `ruleOffset`, `outerDepth` path steps below the arguments. */
|
|
233
|
+
function validateTest(
|
|
234
|
+
data: Uint8Array,
|
|
235
|
+
ruleOffset: number,
|
|
236
|
+
ruleEnd: number,
|
|
237
|
+
scopeValue: typeof Scope.CONTEXT | typeof Scope.CALLDATA,
|
|
238
|
+
outerDepth: number,
|
|
239
|
+
): void {
|
|
240
|
+
let operatorOffset: number;
|
|
241
|
+
let typeCode: number;
|
|
242
|
+
switch (scopeValue) {
|
|
243
|
+
case Scope.CONTEXT: {
|
|
244
|
+
// Context-scope rules must have exactly one path step naming a defined property.
|
|
245
|
+
if (ruleOffset + PF.RULE_PATH_OFFSET > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
246
|
+
if (data[ruleOffset + PF.RULE_DEPTH_OFFSET] !== 1) {
|
|
252
247
|
throw new CallciumError(
|
|
253
|
-
"
|
|
254
|
-
|
|
248
|
+
"INVALID_CONTEXT_PATH",
|
|
249
|
+
"Context-scope rules must have exactly one path step",
|
|
255
250
|
ruleOffset,
|
|
256
251
|
);
|
|
257
252
|
}
|
|
258
|
-
|
|
259
|
-
|
|
253
|
+
operatorOffset = ruleOffset + PF.RULE_PATH_OFFSET + PF.PROPERTY_ID_SIZE;
|
|
254
|
+
if (operatorOffset > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
255
|
+
const contextPropertyId = readU16(data, ruleOffset + PF.RULE_PATH_OFFSET);
|
|
256
|
+
if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) {
|
|
257
|
+
throw new CallciumError(
|
|
258
|
+
"UNKNOWN_CONTEXT_PROPERTY",
|
|
259
|
+
`Unknown context property ID ${formatCode(contextPropertyId, PF.PROPERTY_ID_SIZE)}`,
|
|
260
|
+
ruleOffset,
|
|
261
|
+
);
|
|
260
262
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
typeCode = contextPropertyType(contextPropertyId);
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case Scope.CALLDATA: {
|
|
267
|
+
const { pathEnd: hintOffset, depth } = validatePath(data, ruleOffset, ruleOffset + PF.RULE_DEPTH_OFFSET, ruleEnd);
|
|
268
|
+
// A root test lies outside every loop, and only a root path must be non-empty.
|
|
269
|
+
if (outerDepth === 0 && depth === 0) {
|
|
270
|
+
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
|
|
266
271
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
272
|
+
if (outerDepth + depth > PF.MAX_PATH_DEPTH) pathTooDeep(outerDepth + depth, ruleOffset);
|
|
273
|
+
|
|
274
|
+
const targetOffset = validateHint(data, ruleOffset, hintOffset, ruleEnd);
|
|
275
|
+
operatorOffset = targetOffset + PF.HINT_TARGET_SIZE;
|
|
276
|
+
if (operatorOffset > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
277
|
+
const targetMeta = readU16(data, targetOffset + PF.HINT_TARGET_META_OFFSET);
|
|
278
|
+
typeCode = data[targetOffset + PF.HINT_TARGET_TYPECODE_OFFSET]!;
|
|
279
|
+
// A target meta word describes a dynamic array and is absent for every other type.
|
|
280
|
+
const targetMetaOk =
|
|
281
|
+
typeCode === TypeCode.DYNAMIC_ARRAY ? (targetMeta & PF.HINT_META_RESERVED_MASK) === 0 : targetMeta === 0;
|
|
282
|
+
if (!targetMetaOk) malformedHint("Target meta carries reserved or unused state", ruleOffset);
|
|
283
|
+
|
|
284
|
+
// An operator reads either a scalar word or a declared length, so a target carrying neither — a tuple, a static
|
|
285
|
+
// array, or an undefined code — addresses nothing.
|
|
286
|
+
if (operandWidth(typeCode) === undefined && !hasCalldataLength(typeCode)) {
|
|
287
|
+
malformedHint("Target type code addresses no value", ruleOffset);
|
|
280
288
|
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
default:
|
|
292
|
+
return scopeValue satisfies never;
|
|
293
|
+
}
|
|
281
294
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
295
|
+
const operatorEnd = validateOperator(data, ruleOffset, operatorOffset, ruleEnd, typeCode);
|
|
296
|
+
if (operatorEnd !== ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
297
|
+
}
|
|
285
298
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Require the path whose depth field sits at `depthOffset` to step below the reserved marker range. Return the
|
|
301
|
+
* offset past the path and its depth.
|
|
302
|
+
*/
|
|
303
|
+
function validatePath(
|
|
304
|
+
data: Uint8Array,
|
|
305
|
+
ruleOffset: number,
|
|
306
|
+
depthOffset: number,
|
|
307
|
+
ruleEnd: number,
|
|
308
|
+
): { pathEnd: number; depth: number } {
|
|
309
|
+
if (depthOffset + PF.RULE_DEPTH_SIZE > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
310
|
+
const depth = data[depthOffset]!;
|
|
311
|
+
const pathStart = depthOffset + PF.RULE_DEPTH_SIZE;
|
|
312
|
+
const pathEnd = pathStart + depth * PF.PATH_STEP_SIZE;
|
|
313
|
+
if (pathEnd > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
314
|
+
|
|
315
|
+
for (let offset = pathStart; offset < pathEnd; offset += PF.PATH_STEP_SIZE) {
|
|
316
|
+
const step = readU16(data, offset);
|
|
317
|
+
if (step >= PF.PATH_MARKER_BASE) {
|
|
318
|
+
throw new CallciumError(
|
|
319
|
+
"UNKNOWN_PATH_STEP",
|
|
320
|
+
`Path step ${formatCode(step, PF.PATH_STEP_SIZE)} is in the reserved marker range`,
|
|
321
|
+
ruleOffset,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return { pathEnd, depth };
|
|
326
|
+
}
|
|
295
327
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Require the hint header and hops at `hintOffset` to carry no reserved or unused state, and return the offset past
|
|
330
|
+
* the hops.
|
|
331
|
+
*/
|
|
332
|
+
function validateHint(data: Uint8Array, ruleOffset: number, hintOffset: number, ruleEnd: number): number {
|
|
333
|
+
if (hintOffset + PF.HINT_HEADER_SIZE > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
334
|
+
const header = data[hintOffset]!;
|
|
335
|
+
if ((header & PF.HINT_HEADER_RESERVED_MASK) !== 0) malformedHint("Header carries reserved bits", ruleOffset);
|
|
302
336
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
"OPERATOR_TARGET_MISMATCH",
|
|
310
|
-
"Operator family does not match the type the target declares",
|
|
311
|
-
ruleOffset,
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
337
|
+
const hopsStart = hintOffset + PF.HINT_HEADER_SIZE;
|
|
338
|
+
const hopsEnd = hopsStart + (header & PF.HINT_HOP_COUNT_MASK) * PF.HINT_HOP_SIZE;
|
|
339
|
+
if (hopsEnd > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
340
|
+
validateHops(data, hopsStart, hopsEnd, ruleOffset);
|
|
341
|
+
return hopsEnd;
|
|
342
|
+
}
|
|
315
343
|
|
|
316
|
-
|
|
344
|
+
/** Require every hop entry in `[start, end)` to carry no reserved or unused state. */
|
|
345
|
+
function validateHops(data: Uint8Array, start: number, end: number, ruleOffset: number): void {
|
|
346
|
+
for (let offset = start; offset < end; offset += PF.HINT_HOP_SIZE) {
|
|
347
|
+
const index = readU16(data, offset + PF.HINT_HOP_INDEX_OFFSET);
|
|
348
|
+
if (index !== PF.HINT_NO_INDEX && index >= PF.PATH_MARKER_BASE) {
|
|
349
|
+
malformedHint("Hop index is a reserved value", ruleOffset);
|
|
350
|
+
}
|
|
317
351
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
}
|
|
352
|
+
const meta = readU16(data, offset + PF.HINT_HOP_META_OFFSET);
|
|
353
|
+
// A plain hop carries no element meta; an element hop addresses no offset of its own.
|
|
354
|
+
if (index === PF.HINT_NO_INDEX) {
|
|
355
|
+
if (meta !== 0) malformedHint("Plain hop carries a meta word", ruleOffset);
|
|
356
|
+
} else {
|
|
357
|
+
if (readU32(data, offset) !== 0) malformedHint("Element hop carries a delta", ruleOffset);
|
|
358
|
+
if ((meta & PF.HINT_META_RESERVED_MASK) !== 0) malformedHint("Hop meta carries reserved bits", ruleOffset);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
330
362
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
363
|
+
/** Validate the operator at `offset` against a target of `typeCode` and return the offset past it. */
|
|
364
|
+
function validateOperator(
|
|
365
|
+
data: Uint8Array,
|
|
366
|
+
ruleOffset: number,
|
|
367
|
+
offset: number,
|
|
368
|
+
ruleEnd: number,
|
|
369
|
+
typeCode: number,
|
|
370
|
+
): number {
|
|
371
|
+
if (offset + PF.RULE_OPCODE_SIZE + PF.RULE_DATALENGTH_SIZE > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
372
|
+
const opCodeValue = data[offset]!;
|
|
373
|
+
const opBase = comparisonOf(opCodeValue);
|
|
374
|
+
const source = sourceOf(opCodeValue);
|
|
375
|
+
if (!isComparisonCode(opBase) || !isSourceCode(source)) {
|
|
376
|
+
throw new CallciumError(
|
|
377
|
+
"UNKNOWN_OPERATOR",
|
|
378
|
+
`Unknown operator code ${formatCode(opCodeValue & ~Op.NOT, PF.RULE_OPCODE_SIZE)}`,
|
|
379
|
+
ruleOffset,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
341
382
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
383
|
+
// A length operator reads the declared length a dynamic target resolves to, and a value operator reads a scalar
|
|
384
|
+
// word, so the target type fixes which family the rule may carry.
|
|
385
|
+
const isLength = isLengthOp(opBase);
|
|
386
|
+
if (hasCalldataLength(typeCode) !== isLength) {
|
|
387
|
+
throw new CallciumError(
|
|
388
|
+
"OPERATOR_TARGET_MISMATCH",
|
|
389
|
+
"Operator family does not match the type the target declares",
|
|
390
|
+
ruleOffset,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const dataLengthValue = readU16(data, offset + PF.RULE_OPCODE_SIZE);
|
|
395
|
+
const dataStart = offset + PF.RULE_OPCODE_SIZE + PF.RULE_DATALENGTH_SIZE;
|
|
396
|
+
const operatorEnd = dataStart + dataLengthValue;
|
|
397
|
+
|
|
398
|
+
switch (source) {
|
|
399
|
+
case Source.CONTEXT: {
|
|
400
|
+
if (!acceptsContextSource(opBase)) {
|
|
347
401
|
throw new CallciumError(
|
|
348
|
-
"
|
|
349
|
-
|
|
402
|
+
"INVALID_CONTEXT_OPERAND",
|
|
403
|
+
"Context operand applies only to EQ, GT, LT, GTE and LTE",
|
|
350
404
|
ruleOffset,
|
|
351
405
|
);
|
|
352
406
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const contextPropertyId = readU16(data, pathStart);
|
|
364
|
-
if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) {
|
|
365
|
-
throw new CallciumError(
|
|
366
|
-
"UNKNOWN_CONTEXT_PROPERTY",
|
|
367
|
-
`Context rule references undefined context property ${formatCode(contextPropertyId, PF.PATH_STEP_SIZE)}`,
|
|
368
|
-
ruleOffset,
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const pathHex = toHex(data, pathStart, pathStart + pathLength);
|
|
374
|
-
rules.push({
|
|
375
|
-
ruleSize: field(ruleSizeValue, ruleOffset, ruleOffset + PF.RULE_SIZE_SIZE),
|
|
376
|
-
scope: field(scopeValue, scopeOffset, scopeOffset + 1),
|
|
377
|
-
pathDepth: field(depthValue, depthOffset, depthOffset + 1),
|
|
378
|
-
path: field(pathHex, pathStart, pathStart + pathLength),
|
|
379
|
-
...(hintSize > 0 && {
|
|
380
|
-
hint: hexField(data, hintStart, hintStart + hintSize),
|
|
381
|
-
}),
|
|
382
|
-
opCode: field(opCodeValue, opCodeOffset, opCodeOffset + PF.RULE_OPCODE_SIZE),
|
|
383
|
-
dataLength: field(dataLengthValue, dataLengthOffset, dataLengthOffset + PF.RULE_DATALENGTH_SIZE),
|
|
384
|
-
data: hexField(data, dataStart, dataStart + dataLengthValue),
|
|
385
|
-
span: { start: ruleOffset, end: ruleOffset + ruleSizeValue },
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
if (ruleOffset + ruleSizeValue > groupEnd) {
|
|
389
|
-
throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
|
|
407
|
+
if (dataLengthValue !== PF.PROPERTY_ID_SIZE) dataLengthMismatch(dataLengthValue, PF.PROPERTY_ID_SIZE, ruleOffset);
|
|
408
|
+
if (operatorEnd > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
409
|
+
// A context operand must name a defined context property.
|
|
410
|
+
const contextPropertyId = readU16(data, dataStart);
|
|
411
|
+
if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) {
|
|
412
|
+
throw new CallciumError(
|
|
413
|
+
"UNKNOWN_CONTEXT_PROPERTY",
|
|
414
|
+
`Unknown context property ID ${formatCode(contextPropertyId, PF.PROPERTY_ID_SIZE)}`,
|
|
415
|
+
ruleOffset,
|
|
416
|
+
);
|
|
390
417
|
}
|
|
391
|
-
|
|
418
|
+
break;
|
|
392
419
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
420
|
+
case Source.CONSTANT: {
|
|
421
|
+
// The target is a static elementary type or carries a calldata length, so the operand has a width.
|
|
422
|
+
const width = operandWidth(isLength ? LENGTH_TYPE : typeCode)!;
|
|
423
|
+
if (!isValidPayloadSize(opBase, dataLengthValue, width)) dataLengthMismatch(dataLengthValue, width, ruleOffset);
|
|
424
|
+
if (operatorEnd > ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
425
|
+
// IN operands must be strictly ascending (unsigned); strictness also rejects duplicates.
|
|
426
|
+
if (opBase === Op.IN) validateInAscending(data, dataStart, dataLengthValue, width, ruleOffset);
|
|
427
|
+
break;
|
|
396
428
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
ruleCount: field(ruleCountValue, ruleCountStart, ruleCountStart + PF.GROUP_RULECOUNT_SIZE),
|
|
400
|
-
groupSize: field(groupSizeValue, groupSizeStart, groupSizeStart + PF.GROUP_SIZE_SIZE),
|
|
401
|
-
rules,
|
|
402
|
-
span: { start: offset, end: groupEnd },
|
|
403
|
-
});
|
|
404
|
-
offset = groupEnd;
|
|
429
|
+
default:
|
|
430
|
+
return source satisfies never;
|
|
405
431
|
}
|
|
432
|
+
return operatorEnd;
|
|
433
|
+
}
|
|
406
434
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Require the IN operand members to be strictly ascending by unsigned value. Sign extension and left alignment keep the
|
|
437
|
+
* unsigned order of a member's bytes, so the bytes order the members as their loaded words.
|
|
438
|
+
*/
|
|
439
|
+
function validateInAscending(
|
|
440
|
+
data: Uint8Array,
|
|
441
|
+
dataStart: number,
|
|
442
|
+
dataLength: number,
|
|
443
|
+
width: number,
|
|
444
|
+
ruleOffset: number,
|
|
445
|
+
): void {
|
|
446
|
+
const memberCount = dataLength / width;
|
|
447
|
+
let prev = readMember(data, dataStart, width);
|
|
448
|
+
for (let member = 1; member < memberCount; member++) {
|
|
449
|
+
const cur = readMember(data, dataStart + member * width, width);
|
|
450
|
+
if (cur <= prev) {
|
|
451
|
+
throw new CallciumError("UNSORTED_IN_SET", "IN operands must be strictly ascending", ruleOffset);
|
|
452
|
+
}
|
|
453
|
+
prev = cur;
|
|
410
454
|
}
|
|
455
|
+
}
|
|
411
456
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
descriptor: {
|
|
417
|
-
get raw(): Hex {
|
|
418
|
-
return descriptorRaw.value;
|
|
419
|
-
},
|
|
420
|
-
set raw(replacement: Hex) {
|
|
421
|
-
descriptorRaw.value = replacement;
|
|
422
|
-
},
|
|
423
|
-
header: field({ version: desc.version, paramCount: desc.params.length }, descStart, descStart + DF.HEADER_SIZE),
|
|
424
|
-
params,
|
|
425
|
-
span: { start: descStart, end: descEnd },
|
|
426
|
-
},
|
|
427
|
-
groupCount: field(groupCountValue, groupCountStart, groupCountEnd),
|
|
428
|
-
groups,
|
|
429
|
-
span: { start: 0, end: data.length },
|
|
430
|
-
version,
|
|
431
|
-
isSelectorless,
|
|
432
|
-
};
|
|
457
|
+
/** Throw a GROUP_SIZE_MISMATCH error for the group at `groupOffset`. */
|
|
458
|
+
function groupSizeMismatch(groupOffset: number): never {
|
|
459
|
+
throw new CallciumError("GROUP_SIZE_MISMATCH", "Rules do not exactly fill the declared group size", groupOffset);
|
|
460
|
+
}
|
|
433
461
|
|
|
434
|
-
|
|
462
|
+
/** Throw a RULE_SIZE_MISMATCH error for the rule at `ruleOffset`. */
|
|
463
|
+
function ruleSizeMismatch(ruleOffset: number): never {
|
|
464
|
+
throw new CallciumError(
|
|
465
|
+
"RULE_SIZE_MISMATCH",
|
|
466
|
+
"Rule content does not exactly fill the declared rule size",
|
|
467
|
+
ruleOffset,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Throw a MALFORMED_HINT error with the given detail. */
|
|
472
|
+
function malformedHint(detail: string, ruleOffset: number): never {
|
|
473
|
+
throw new CallciumError("MALFORMED_HINT", detail, ruleOffset);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Throw a PATH_TOO_DEEP error for a path of `depth` steps counted across the enclosing loop. */
|
|
477
|
+
function pathTooDeep(depth: number, ruleOffset: number): never {
|
|
478
|
+
throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${PF.MAX_PATH_DEPTH}`, ruleOffset);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Throw a DATA_LENGTH_MISMATCH error for `dataLength` against operands of `width` bytes. */
|
|
482
|
+
function dataLengthMismatch(dataLength: number, width: number, ruleOffset: number): never {
|
|
483
|
+
throw new CallciumError(
|
|
484
|
+
"DATA_LENGTH_MISMATCH",
|
|
485
|
+
`Operator data length ${dataLength} does not hold ${width}-byte operands`,
|
|
486
|
+
ruleOffset,
|
|
487
|
+
);
|
|
435
488
|
}
|
|
436
489
|
|
|
437
490
|
///////////////////////////////////////////////////////////////////////////
|
|
438
|
-
//
|
|
491
|
+
// Encoding
|
|
439
492
|
///////////////////////////////////////////////////////////////////////////
|
|
440
493
|
|
|
441
|
-
|
|
494
|
+
/**
|
|
495
|
+
* A rule with its size-free bytes, its key in the canonical order, and its bytes after its `ruleSize`, each body rule
|
|
496
|
+
* framed.
|
|
497
|
+
*/
|
|
498
|
+
type Keyed = { rule: Rule; key: Uint8Array; content: Uint8Array };
|
|
442
499
|
|
|
443
|
-
/**
|
|
444
|
-
|
|
500
|
+
/**
|
|
501
|
+
* Encode the canonical form of a policy: each group normalized, its rules and the groups sorted by their
|
|
502
|
+
* size-free bytes, repeats dropped.
|
|
503
|
+
* @param data - The policy.
|
|
504
|
+
* @returns The policy blob as a 0x-prefixed hex string.
|
|
505
|
+
* @throws {CallciumError} When a field does not have the shape the encoder reads, or a path does not compile.
|
|
506
|
+
* When an operator does not apply to its target, or a constant is not a value of its operand type.
|
|
507
|
+
* When a count or size overflows its field.
|
|
508
|
+
*/
|
|
509
|
+
function encode(data: PolicyData): Hex {
|
|
510
|
+
if (data.groups.length === 0) {
|
|
511
|
+
throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
|
|
512
|
+
}
|
|
513
|
+
const desc = hexToBytes(data.descriptor);
|
|
514
|
+
requireFieldFits(desc.length, PF.DESC_LENGTH_SIZE, "DESC_LENGTH_OVERFLOW", "Descriptor length");
|
|
515
|
+
|
|
516
|
+
const groups = sortDedupe(
|
|
517
|
+
data.groups.map((rules, groupIndex) => encodeGroup(desc, rules, groupIndex)),
|
|
518
|
+
(group) => group.key,
|
|
519
|
+
);
|
|
520
|
+
requireFieldFits(groups.length, PF.GROUP_COUNT_SIZE, "GROUP_COUNT_OVERFLOW", "Group count");
|
|
521
|
+
|
|
522
|
+
// Selector slot is defined as zero for selectorless policies regardless of what the caller provides.
|
|
523
|
+
const selector = data.isSelectorless ? new Uint8Array(PF.SELECTOR_SIZE) : hexToBytes(data.selector);
|
|
524
|
+
if (selector.length !== PF.SELECTOR_SIZE) {
|
|
525
|
+
throw new CallciumError("MALFORMED_SELECTOR", `Selector must be ${PF.SELECTOR_SIZE} bytes, got ${selector.length}`);
|
|
526
|
+
}
|
|
527
|
+
const header = (PF.MAJOR << PF.MAJOR_SHIFT) | (PF.MINOR << PF.MINOR_SHIFT);
|
|
528
|
+
return bytesToHex(
|
|
529
|
+
concatBytes(
|
|
530
|
+
Uint8Array.of(data.isSelectorless ? header | PF.FLAG_NO_SELECTOR : header),
|
|
531
|
+
selector,
|
|
532
|
+
be16(desc.length),
|
|
533
|
+
desc,
|
|
534
|
+
Uint8Array.of(groups.length),
|
|
535
|
+
...groups.map((group) => group.encoded),
|
|
536
|
+
),
|
|
537
|
+
);
|
|
538
|
+
}
|
|
445
539
|
|
|
446
|
-
/**
|
|
447
|
-
function
|
|
448
|
-
|
|
540
|
+
/** Encode a group in canonical form: its size-free bytes, and its bytes with the group header. */
|
|
541
|
+
function encodeGroup(desc: Uint8Array, rules: Rule[], groupIndex: number): { key: Uint8Array; encoded: Uint8Array } {
|
|
542
|
+
if (rules.length === 0) {
|
|
543
|
+
throw new CallciumError("EMPTY_GROUP", `Group ${groupIndex} is empty`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Every rule in canonical form, bodies first; then loops joined, context tests hoisted, and the rules sorted.
|
|
547
|
+
const normalized = rules.map((rule, ruleIndex) => {
|
|
548
|
+
checkRule(rule, groupIndex, ruleIndex);
|
|
549
|
+
return normalizeRule(desc, rule);
|
|
550
|
+
});
|
|
551
|
+
const finished = finish(desc, hoistContext(joinLoops(desc, normalized)));
|
|
552
|
+
|
|
553
|
+
requireFieldFits(finished.length, PF.GROUP_RULECOUNT_SIZE, "RULE_COUNT_OVERFLOW", `Group ${groupIndex} rule count`);
|
|
554
|
+
const ruleBytes = concatBytes(
|
|
555
|
+
...finished.map(({ content }) => {
|
|
556
|
+
const size = content.length + PF.RULE_SIZE_SIZE;
|
|
557
|
+
requireFieldFits(size, PF.RULE_SIZE_SIZE, "RULE_SIZE_OVERFLOW", "Rule size");
|
|
558
|
+
return concatBytes(be16(size), content);
|
|
559
|
+
}),
|
|
560
|
+
);
|
|
561
|
+
return {
|
|
562
|
+
key: concatBytes(...finished.map(({ key }) => key)),
|
|
563
|
+
encoded: concatBytes(be16(finished.length), be32(ruleBytes.length), ruleBytes),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Require the rule's fields to have the shapes the encoder reads. */
|
|
568
|
+
function checkRule(rule: Rule, groupIndex: number, ruleIndex: number): void {
|
|
569
|
+
if (rule.scope !== Scope.LOOP) {
|
|
570
|
+
checkTest(rule.scope, rule.path, rule.operator, 0, groupIndex, ruleIndex);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const quantifier: number = rule.quantifier;
|
|
574
|
+
if (!isQuantifierCode(quantifier)) {
|
|
575
|
+
throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier value ${quantifier}`);
|
|
576
|
+
}
|
|
577
|
+
const arrayDepth = checkPath(hexToBytes(rule.path), true, 0);
|
|
578
|
+
for (const test of rule.body) checkTest(test.scope, test.path, test.operator, arrayDepth, groupIndex, ruleIndex);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Require a test's operator to be an opcode and one or more 32-byte words, and its path to have the shape its scope
|
|
583
|
+
* reads. For a calldata test, the path comes after `depthBefore` steps of an enclosing loop.
|
|
584
|
+
*/
|
|
585
|
+
function checkTest(
|
|
586
|
+
testScope: number,
|
|
587
|
+
path: Hex,
|
|
588
|
+
operator: Hex,
|
|
589
|
+
depthBefore: number,
|
|
590
|
+
groupIndex: number,
|
|
591
|
+
ruleIndex: number,
|
|
592
|
+
): void {
|
|
593
|
+
const operatorBytes = hexToBytes(operator);
|
|
594
|
+
const isWords = operatorBytes.length > 1 && (operatorBytes.length - 1) % 32 === 0;
|
|
595
|
+
// A context property operand is one ID within its field.
|
|
596
|
+
const isContextId =
|
|
597
|
+
sourceOf(operatorBytes[0] ?? 0) !== Source.CONTEXT ||
|
|
598
|
+
(operatorBytes.length === 33 && toBigInt(operatorBytes, 1) <= BigInt(fieldMax(PF.PROPERTY_ID_SIZE)));
|
|
599
|
+
if (!isWords || !isContextId) {
|
|
600
|
+
throw new CallciumError(
|
|
601
|
+
"INVALID_OPERATOR_BYTES",
|
|
602
|
+
`Group ${groupIndex} rule ${ruleIndex} operator must be an opcode followed by 32-byte operand words`,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
const pathBytes = hexToBytes(path);
|
|
606
|
+
if (testScope === Scope.CONTEXT) {
|
|
607
|
+
if (pathBytes.length !== PF.PROPERTY_ID_SIZE) {
|
|
608
|
+
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step");
|
|
609
|
+
}
|
|
610
|
+
} else if (testScope === Scope.CALLDATA) {
|
|
611
|
+
checkPath(pathBytes, depthBefore === 0, depthBefore);
|
|
612
|
+
} else {
|
|
613
|
+
throw new CallciumError("UNKNOWN_SCOPE", `Unknown scope value ${testScope}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
449
616
|
|
|
450
|
-
|
|
451
|
-
|
|
617
|
+
/**
|
|
618
|
+
* Require a calldata path to be whole steps, nonempty where `required`, and within `MAX_PATH_DEPTH` with the
|
|
619
|
+
* `depthBefore` steps of an enclosing loop. Return the depth reached.
|
|
620
|
+
*/
|
|
621
|
+
function checkPath(path: Uint8Array, required: boolean, depthBefore: number): number {
|
|
622
|
+
if (required && path.length === 0) {
|
|
452
623
|
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step");
|
|
453
624
|
}
|
|
454
|
-
if (
|
|
625
|
+
if (path.length % PF.PATH_STEP_SIZE !== 0) {
|
|
455
626
|
throw new CallciumError("MALFORMED_PATH", "Path byte length must be even");
|
|
456
627
|
}
|
|
457
|
-
const depth = path.length /
|
|
628
|
+
const depth = depthBefore + path.length / PF.PATH_STEP_SIZE;
|
|
458
629
|
if (depth > PF.MAX_PATH_DEPTH) {
|
|
459
630
|
throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${PF.MAX_PATH_DEPTH}`);
|
|
460
631
|
}
|
|
632
|
+
return depth;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
///////////////////////////////////////////////////////////////////////////
|
|
636
|
+
// Normalization
|
|
637
|
+
///////////////////////////////////////////////////////////////////////////
|
|
461
638
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
639
|
+
/**
|
|
640
|
+
* Return the rule in canonical form. Its operator is positive and no one-member set, a context pair has the lower ID
|
|
641
|
+
* as subject, and a loop's body is finished.
|
|
642
|
+
*/
|
|
643
|
+
function normalizeRule(desc: Uint8Array, rule: Rule): Rule {
|
|
644
|
+
if (rule.scope !== Scope.LOOP) return normalizeTest(rule);
|
|
645
|
+
return loop(desc, rule.quantifier, rule.path, rule.body.map(normalizeTest));
|
|
467
646
|
}
|
|
468
647
|
|
|
469
|
-
/**
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
648
|
+
/**
|
|
649
|
+
* Return the test with its operator in canonical form and, for a context test against another context property, the
|
|
650
|
+
* lower ID as subject.
|
|
651
|
+
*/
|
|
652
|
+
function normalizeTest(test: BodyRule): BodyRule {
|
|
653
|
+
const operator = normalizeOperator(hexToBytes(test.operator));
|
|
654
|
+
const opCode = operator[0]!;
|
|
655
|
+
const normalized: BodyRule = { scope: test.scope, path: test.path, operator: bytesToHex(operator) };
|
|
656
|
+
if (test.scope !== Scope.CONTEXT || sourceOf(opCode) !== Source.CONTEXT) return normalized;
|
|
657
|
+
|
|
658
|
+
const swapped = swapOf(comparisonOf(opCode));
|
|
659
|
+
const other = toBigInt(operator, 1);
|
|
660
|
+
const property = readU16(hexToBytes(test.path), 0);
|
|
661
|
+
if (swapped === undefined || other >= BigInt(property)) return normalized;
|
|
662
|
+
|
|
663
|
+
const word = new Uint8Array(32);
|
|
664
|
+
writeBE16(word, 30, property);
|
|
665
|
+
return {
|
|
666
|
+
scope: test.scope,
|
|
667
|
+
path: encodePath([Number(other)]),
|
|
668
|
+
operator: bytesToHex(concatBytes(Uint8Array.of((opCode & ~Op.COMPARISON_MASK) | swapped), word)),
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Return the operator without NOT where a positive comparison expresses the negation, and a one-member constant set as
|
|
674
|
+
* an equality.
|
|
675
|
+
*/
|
|
676
|
+
function normalizeOperator(operator: Uint8Array): Uint8Array {
|
|
677
|
+
let opCode = operator[0]!;
|
|
678
|
+
const comparison = comparisonOf(opCode);
|
|
679
|
+
if ((opCode & Op.NOT) !== 0) {
|
|
680
|
+
const positive = negationOf(comparison);
|
|
681
|
+
if (positive !== undefined) opCode = (opCode & ~(Op.NOT | Op.COMPARISON_MASK)) | positive;
|
|
682
|
+
}
|
|
683
|
+
if (comparison === Op.IN && sourceOf(opCode) === Source.CONSTANT && operator.length === 33) {
|
|
684
|
+
opCode = (opCode & ~Op.COMPARISON_MASK) | Op.EQ;
|
|
685
|
+
}
|
|
686
|
+
const normalized = operator.slice();
|
|
687
|
+
normalized[0] = opCode;
|
|
688
|
+
return normalized;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** Return the loop over the array at `arrayPath` with its body finished. */
|
|
692
|
+
function loop(desc: Uint8Array, quantifier: QuantifierCode, arrayPath: Hex, tests: BodyRule[]): LoopRule {
|
|
693
|
+
const steps = parsePathSteps(arrayPath);
|
|
694
|
+
const compiled = Hint.requireCompiled(Hint.compileLoop(desc, steps), steps, 0);
|
|
695
|
+
const body = finish(desc, tests, compiled.node)
|
|
696
|
+
.map(({ rule }) => rule)
|
|
697
|
+
.filter((rule): rule is BodyRule => rule.scope !== Scope.LOOP);
|
|
698
|
+
return { scope: Scope.LOOP, quantifier, path: arrayPath, body };
|
|
699
|
+
}
|
|
475
700
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
701
|
+
/**
|
|
702
|
+
* Join every `ALL` loop into the first `ALL` loop over the same array. `ANY` loops stay apart: they are independent
|
|
703
|
+
* existentials.
|
|
704
|
+
*/
|
|
705
|
+
function joinLoops(desc: Uint8Array, rules: Rule[]): Rule[] {
|
|
706
|
+
const consumed = new Set<number>();
|
|
707
|
+
const joined: Rule[] = [];
|
|
708
|
+
rules.forEach((rule, ruleIndex) => {
|
|
709
|
+
if (consumed.has(ruleIndex)) return;
|
|
710
|
+
if (!isAllLoop(rule)) {
|
|
711
|
+
joined.push(rule);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const tests = [...rule.body];
|
|
715
|
+
for (let laterIndex = ruleIndex + 1; laterIndex < rules.length; laterIndex++) {
|
|
716
|
+
const later = rules[laterIndex]!;
|
|
717
|
+
if (!isAllLoop(later) || later.path.toLowerCase() !== rule.path.toLowerCase()) continue;
|
|
718
|
+
tests.push(...later.body);
|
|
719
|
+
consumed.add(laterIndex);
|
|
720
|
+
}
|
|
721
|
+
joined.push(loop(desc, LoopQuantifier.ALL, rule.path, tests));
|
|
480
722
|
});
|
|
723
|
+
return joined;
|
|
481
724
|
}
|
|
482
725
|
|
|
483
|
-
/**
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
726
|
+
/**
|
|
727
|
+
* Move the context tests of every `ANY` loop whose body contains a calldata test into the group:
|
|
728
|
+
* `∃i.(P ∧ Q(i)) ≡ P ∧ ∃i.Q(i)`.
|
|
729
|
+
*/
|
|
730
|
+
function hoistContext(rules: Rule[]): Rule[] {
|
|
731
|
+
const hoisted: Rule[] = [];
|
|
732
|
+
const kept = rules.map((rule) => {
|
|
733
|
+
if (!hoists(rule)) return rule;
|
|
734
|
+
hoisted.push(...rule.body.filter((test) => test.scope === Scope.CONTEXT));
|
|
735
|
+
return { ...rule, body: rule.body.filter((test) => test.scope !== Scope.CONTEXT) };
|
|
736
|
+
});
|
|
737
|
+
return [...kept, ...hoisted];
|
|
490
738
|
}
|
|
491
739
|
|
|
492
|
-
/**
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
740
|
+
/**
|
|
741
|
+
* Return the rules sorted by their size-free bytes, repeats dropped and lone range pairs fused. Fusing counts distinct
|
|
742
|
+
* rules, so fusing follows dropping repeats. A range can repeat a rule already present, so sorting follows fusing.
|
|
743
|
+
*/
|
|
744
|
+
function finish(desc: Uint8Array, rules: Rule[], start?: number): Keyed[] {
|
|
745
|
+
const keyed = sortDedupe(
|
|
746
|
+
rules.map((rule) => keyedRule(desc, rule, start)),
|
|
747
|
+
({ key }) => key,
|
|
748
|
+
);
|
|
749
|
+
return sortDedupe(fuseRanges(desc, keyed, start), ({ key }) => key);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Fuse each lone pair of bounds on one subject into a range: the lower bound becomes the range, the upper bound leaves.
|
|
754
|
+
* The range is defined as the pair, so no bound needs comparing.
|
|
755
|
+
*/
|
|
756
|
+
function fuseRanges(desc: Uint8Array, keyed: Keyed[], start?: number): Keyed[] {
|
|
757
|
+
return keyed.flatMap((entry) => {
|
|
758
|
+
const rule = entry.rule;
|
|
759
|
+
const bound = boundOf(rule);
|
|
760
|
+
const partner = bound === undefined ? undefined : lonePartner(keyed, rule, bound);
|
|
761
|
+
if (bound === undefined || partner === undefined || rule.scope === Scope.LOOP) return [entry];
|
|
762
|
+
if (!bound.isLower) return [];
|
|
763
|
+
|
|
764
|
+
const operator = hexToBytes(rule.operator);
|
|
765
|
+
const upper = hexToBytes(partner.operator);
|
|
766
|
+
const range: BodyRule = {
|
|
767
|
+
scope: rule.scope,
|
|
768
|
+
path: rule.path,
|
|
769
|
+
operator: bytesToHex(
|
|
770
|
+
concatBytes(
|
|
771
|
+
Uint8Array.of(bound.isLength ? Op.LENGTH_BETWEEN : Op.BETWEEN),
|
|
772
|
+
operator.subarray(1),
|
|
773
|
+
upper.subarray(1),
|
|
774
|
+
),
|
|
775
|
+
),
|
|
776
|
+
};
|
|
777
|
+
return [keyedRule(desc, range, start)];
|
|
502
778
|
});
|
|
503
779
|
}
|
|
504
780
|
|
|
505
|
-
/**
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
781
|
+
/** The bound a rule places: whether it bounds a length, and whether from below. */
|
|
782
|
+
type Bound = { isLength: boolean; isLower: boolean };
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Return the other bound of the lone pair `rule` belongs to, or undefined unless one rule bounds each side of its
|
|
786
|
+
* subject.
|
|
787
|
+
*/
|
|
788
|
+
function lonePartner(keyed: Keyed[], rule: Rule, bound: Bound): BodyRule | undefined {
|
|
789
|
+
let sameSide = 0;
|
|
790
|
+
let partner: BodyRule | undefined;
|
|
791
|
+
let otherSide = 0;
|
|
792
|
+
for (const { rule: candidate } of keyed) {
|
|
793
|
+
const candidateBound = boundOf(candidate);
|
|
794
|
+
if (candidateBound === undefined || candidate.scope === Scope.LOOP || candidate.scope !== rule.scope) continue;
|
|
795
|
+
if (candidateBound.isLength !== bound.isLength || candidate.path.toLowerCase() !== rule.path.toLowerCase())
|
|
796
|
+
continue;
|
|
797
|
+
if (candidateBound.isLower === bound.isLower) {
|
|
798
|
+
sameSide++;
|
|
799
|
+
} else {
|
|
800
|
+
otherSide++;
|
|
801
|
+
partner = candidate;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return sameSide === 1 && otherSide === 1 ? partner : undefined;
|
|
509
805
|
}
|
|
510
806
|
|
|
511
807
|
/**
|
|
512
|
-
*
|
|
513
|
-
* Path shape and operator framing are established by `flattenConstraint`.
|
|
808
|
+
* Return the bound the rule places, an unnegated constant `GTE`, `LTE`, `LENGTH_GTE` or `LENGTH_LTE`, or undefined.
|
|
514
809
|
*/
|
|
515
|
-
function
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
buf[opOffset] = rule.operator[0]!;
|
|
532
|
-
writeBE16(buf, opOffset + 1, data.length);
|
|
533
|
-
buf.set(data, opOffset + 3);
|
|
534
|
-
|
|
535
|
-
return buf;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/** Serialize all rules in a group to a single byte array. */
|
|
539
|
-
function encodeGroupRules(rules: Rule[]): Uint8Array {
|
|
540
|
-
const parts = rules.map(encodeRule);
|
|
541
|
-
const totalSize = parts.reduce((sum, p) => sum + p.length, 0);
|
|
542
|
-
const buf = new Uint8Array(totalSize);
|
|
543
|
-
let offset = 0;
|
|
544
|
-
for (const part of parts) {
|
|
545
|
-
buf.set(part, offset);
|
|
546
|
-
offset += part.length;
|
|
547
|
-
}
|
|
548
|
-
return buf;
|
|
810
|
+
function boundOf(rule: Rule): Bound | undefined {
|
|
811
|
+
if (rule.scope === Scope.LOOP) return undefined;
|
|
812
|
+
const operator = hexToBytes(rule.operator);
|
|
813
|
+
if (operator.length !== 33) return undefined;
|
|
814
|
+
switch (operator[0]!) {
|
|
815
|
+
case Op.GTE:
|
|
816
|
+
return { isLength: false, isLower: true };
|
|
817
|
+
case Op.LTE:
|
|
818
|
+
return { isLength: false, isLower: false };
|
|
819
|
+
case Op.LENGTH_GTE:
|
|
820
|
+
return { isLength: true, isLower: true };
|
|
821
|
+
case Op.LENGTH_LTE:
|
|
822
|
+
return { isLength: true, isLower: false };
|
|
823
|
+
default:
|
|
824
|
+
return undefined;
|
|
825
|
+
}
|
|
549
826
|
}
|
|
550
827
|
|
|
551
828
|
///////////////////////////////////////////////////////////////////////////
|
|
552
|
-
//
|
|
829
|
+
// Rule bytes
|
|
553
830
|
///////////////////////////////////////////////////////////////////////////
|
|
554
831
|
|
|
555
|
-
/**
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
832
|
+
/**
|
|
833
|
+
* Return the rule with its size-free bytes and its content. Its paths compile from the starting point: the arguments,
|
|
834
|
+
* or the loop element whose node lies at `start`.
|
|
835
|
+
*/
|
|
836
|
+
function keyedRule(desc: Uint8Array, rule: Rule, start?: number): Keyed {
|
|
837
|
+
if (rule.scope !== Scope.LOOP) {
|
|
838
|
+
const key = testBytes(desc, rule, start);
|
|
839
|
+
return { rule, key, content: key };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const steps = parsePathSteps(rule.path);
|
|
843
|
+
const compiled = Hint.requireCompiled(Hint.compileLoop(desc, steps), steps, 0);
|
|
844
|
+
const path = hexToBytes(rule.path);
|
|
845
|
+
const head = concatBytes(
|
|
846
|
+
Uint8Array.of(Scope.LOOP, rule.quantifier, steps.length),
|
|
847
|
+
path,
|
|
848
|
+
compiled.hint,
|
|
849
|
+
be16(rule.body.length),
|
|
850
|
+
);
|
|
851
|
+
const tests = rule.body.map((test) => testBytes(desc, test, compiled.node));
|
|
852
|
+
return {
|
|
853
|
+
rule,
|
|
854
|
+
key: concatBytes(head, ...tests),
|
|
855
|
+
content: concatBytes(head, ...tests.map((test) => concatBytes(be16(test.length + PF.RULE_SIZE_SIZE), test))),
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/** Return a test's size-free bytes, its path compiled from the starting point at `start`. */
|
|
860
|
+
function testBytes(desc: Uint8Array, test: BodyRule, start?: number): Uint8Array {
|
|
861
|
+
const path = hexToBytes(test.path);
|
|
862
|
+
if (test.scope === Scope.CONTEXT) {
|
|
863
|
+
const contextPropertyId = readU16(path, 0);
|
|
864
|
+
const operator = writeOperator(hexToBytes(test.operator), contextPropertyType(contextPropertyId));
|
|
865
|
+
return concatBytes(Uint8Array.of(Scope.CONTEXT, CONTEXT_PATH_DEPTH), path, operator);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const steps = parsePathSteps(test.path);
|
|
869
|
+
const compiled = Hint.requireCompiled(Hint.compileTest(desc, steps, start), steps, 0);
|
|
870
|
+
const operator = writeOperator(hexToBytes(test.operator), desc[compiled.node]!);
|
|
871
|
+
return concatBytes(Uint8Array.of(Scope.CALLDATA, steps.length), path, compiled.hint, operator);
|
|
560
872
|
}
|
|
561
873
|
|
|
562
874
|
/**
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
* @returns The encoded policy as a 0x-prefixed hex string.
|
|
875
|
+
* Return the operator as the wire carries it against a target of `typeCode`: `opCode | dataLength | data`. Each
|
|
876
|
+
* constant narrows to its operand type's width. A context property is written as its ID.
|
|
566
877
|
*/
|
|
567
|
-
function
|
|
568
|
-
const
|
|
878
|
+
function writeOperator(operator: Uint8Array, typeCode: number): Uint8Array {
|
|
879
|
+
const opCode = operator[0]!;
|
|
880
|
+
const operandType = operandTypeOf(comparisonOf(opCode), typeCode);
|
|
881
|
+
if (operandType === undefined) {
|
|
882
|
+
throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Comparison does not apply to the target type");
|
|
883
|
+
}
|
|
569
884
|
|
|
570
|
-
|
|
571
|
-
|
|
885
|
+
const data: Uint8Array[] = [];
|
|
886
|
+
if (sourceOf(opCode) === Source.CONTEXT) {
|
|
887
|
+
data.push(operator.subarray(31, 33));
|
|
888
|
+
} else {
|
|
889
|
+
const width = operandWidth(operandType)!;
|
|
890
|
+
for (let offset = 1; offset < operator.length; offset += 32) {
|
|
891
|
+
const word = toBigInt(operator, offset);
|
|
892
|
+
if (canonicalize(word, operandType) !== word) {
|
|
893
|
+
throw new CallciumError(
|
|
894
|
+
"OUT_OF_PHYSICAL_BOUNDS",
|
|
895
|
+
`Operand ${bigintToHex(word)} is outside the range of its operand type`,
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
// A narrow value fills the low bytes of its word, a left-aligned one the high bytes.
|
|
899
|
+
const start = isLeftAligned(operandType) ? offset : offset + 32 - width;
|
|
900
|
+
data.push(operator.subarray(start, start + width));
|
|
901
|
+
}
|
|
572
902
|
}
|
|
573
|
-
|
|
903
|
+
const dataBytes = concatBytes(...data);
|
|
904
|
+
return concatBytes(Uint8Array.of(opCode), be16(dataBytes.length), dataBytes);
|
|
905
|
+
}
|
|
574
906
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const rules = group.flatMap((constraint) => flattenConstraint(constraint, descBytes));
|
|
579
|
-
sortRules(rules);
|
|
580
|
-
return rules;
|
|
581
|
-
});
|
|
907
|
+
///////////////////////////////////////////////////////////////////////////
|
|
908
|
+
// Decoding
|
|
909
|
+
///////////////////////////////////////////////////////////////////////////
|
|
582
910
|
|
|
583
|
-
|
|
911
|
+
/**
|
|
912
|
+
* Decode a policy blob into the policy it states.
|
|
913
|
+
* @param blob - The policy blob.
|
|
914
|
+
* @returns The decoded policy, each operand loaded to its 32-byte word.
|
|
915
|
+
* @throws {CallciumError} When the blob fails storage validation or its descriptor fails DV-1 or DV-2.
|
|
916
|
+
*/
|
|
917
|
+
function decode(blob: Hex): PolicyData {
|
|
918
|
+
validatePolicy(blob);
|
|
919
|
+
const data = hexToBytes(blob);
|
|
584
920
|
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
921
|
+
const isSelectorless = (data[0]! & PF.FLAG_NO_SELECTOR) !== 0;
|
|
922
|
+
const groupCountOffset = PF.DESC_OFFSET + readU16(data, PF.DESC_LENGTH_OFFSET);
|
|
923
|
+
Descriptor.validateMeta(data.subarray(PF.DESC_OFFSET, groupCountOffset));
|
|
924
|
+
const groups: Rule[][] = [];
|
|
925
|
+
let offset = groupCountOffset + PF.GROUP_COUNT_SIZE;
|
|
926
|
+
const groupCount = data[groupCountOffset]!;
|
|
927
|
+
for (let groupIndex = 0; groupIndex < groupCount; groupIndex++) {
|
|
928
|
+
const rules: Rule[] = [];
|
|
929
|
+
const ruleCount = readU16(data, offset);
|
|
930
|
+
offset += PF.GROUP_HEADER_SIZE;
|
|
931
|
+
for (let ruleIndex = 0; ruleIndex < ruleCount; ruleIndex++) {
|
|
932
|
+
rules.push(readRule(data, offset));
|
|
933
|
+
offset += readU16(data, offset);
|
|
588
934
|
}
|
|
589
|
-
|
|
590
|
-
return { wireBytes: encodeGroupRules(rules), ruleCount: rules.length };
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
// Several groups are ordered by ascending keccak256 over their rule bytes; a lone group is
|
|
594
|
-
// already in canonical position, so it needs no hash.
|
|
595
|
-
if (encodedGroups.length > 1) {
|
|
596
|
-
const hashes = new Map(encodedGroups.map((group) => [group, keccak_256(group.wireBytes)]));
|
|
597
|
-
encodedGroups.sort((a, b) => compareBytes(hashes.get(a)!, hashes.get(b)!));
|
|
935
|
+
groups.push(rules);
|
|
598
936
|
}
|
|
937
|
+
return {
|
|
938
|
+
isSelectorless,
|
|
939
|
+
selector: isSelectorless ? "0x00000000" : toHex(data, PF.SELECTOR_OFFSET, PF.SELECTOR_OFFSET + PF.SELECTOR_SIZE),
|
|
940
|
+
descriptor: toHex(data, PF.DESC_OFFSET, groupCountOffset),
|
|
941
|
+
groups,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
599
944
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
"MALFORMED_SELECTOR",
|
|
604
|
-
`Selector must be ${PF.SELECTOR_SIZE} bytes, got ${selectorBytes.length}`,
|
|
605
|
-
);
|
|
606
|
-
}
|
|
945
|
+
/** Read the root rule at `ruleOffset`. */
|
|
946
|
+
function readRule(data: Uint8Array, ruleOffset: number): Rule {
|
|
947
|
+
if (data[ruleOffset + PF.RULE_SCOPE_OFFSET] !== Scope.LOOP) return readTest(data, ruleOffset);
|
|
607
948
|
|
|
608
|
-
const
|
|
949
|
+
const quantifier = data[ruleOffset + PF.RULE_QUANTIFIER_OFFSET]!;
|
|
950
|
+
if (!isQuantifierCode(quantifier)) {
|
|
951
|
+
throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier value ${quantifier}`, ruleOffset);
|
|
952
|
+
}
|
|
953
|
+
const pathStart = ruleOffset + PF.LOOP_DEPTH_OFFSET + PF.RULE_DEPTH_SIZE;
|
|
954
|
+
const hintOffset = pathStart + data[ruleOffset + PF.LOOP_DEPTH_OFFSET]! * PF.PATH_STEP_SIZE;
|
|
955
|
+
|
|
956
|
+
// The body follows the frame hint: its hops, the frame prefix, then `ruleCount`.
|
|
957
|
+
const bodyOffset =
|
|
958
|
+
hintOffset + PF.HINT_HEADER_SIZE + data[hintOffset]! * PF.HINT_HOP_SIZE + PF.HINT_FRAME_PREFIX_SIZE;
|
|
959
|
+
const body: BodyRule[] = [];
|
|
960
|
+
let testOffset = bodyOffset + PF.GROUP_RULECOUNT_SIZE;
|
|
961
|
+
const bodyCount = readU16(data, bodyOffset);
|
|
962
|
+
for (let bodyIndex = 0; bodyIndex < bodyCount; bodyIndex++) {
|
|
963
|
+
body.push(readTest(data, testOffset));
|
|
964
|
+
testOffset += readU16(data, testOffset);
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
scope: Scope.LOOP,
|
|
968
|
+
quantifier,
|
|
969
|
+
path: toHex(data, pathStart, hintOffset),
|
|
970
|
+
hint: toHex(data, hintOffset, bodyOffset),
|
|
971
|
+
body,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
609
974
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
975
|
+
/** Read the test or context test at `ruleOffset`, each constant loaded to its 32-byte word. */
|
|
976
|
+
function readTest(data: Uint8Array, ruleOffset: number): BodyRule {
|
|
977
|
+
const testScope = data[ruleOffset + PF.RULE_SCOPE_OFFSET]!;
|
|
978
|
+
if (testScope !== Scope.CONTEXT && testScope !== Scope.CALLDATA) {
|
|
979
|
+
throw new CallciumError("UNKNOWN_SCOPE", `Unknown scope value ${testScope}`, ruleOffset);
|
|
980
|
+
}
|
|
981
|
+
const pathStart = ruleOffset + PF.RULE_PATH_OFFSET;
|
|
982
|
+
let pathEnd: number;
|
|
983
|
+
let opOffset: number;
|
|
984
|
+
let typeCode: number;
|
|
985
|
+
if (testScope === Scope.CONTEXT) {
|
|
986
|
+
pathEnd = pathStart + PF.PROPERTY_ID_SIZE;
|
|
987
|
+
opOffset = pathEnd;
|
|
988
|
+
typeCode = contextPropertyType(readU16(data, pathStart));
|
|
989
|
+
} else {
|
|
990
|
+
// The hint addresses the target: its hops, then the target block ending in the type code.
|
|
991
|
+
pathEnd = pathStart + data[ruleOffset + PF.RULE_DEPTH_OFFSET]! * PF.PATH_STEP_SIZE;
|
|
992
|
+
const targetOffset = pathEnd + PF.HINT_HEADER_SIZE + data[pathEnd]! * PF.HINT_HOP_SIZE;
|
|
993
|
+
typeCode = data[targetOffset + PF.HINT_TARGET_TYPECODE_OFFSET]!;
|
|
994
|
+
opOffset = targetOffset + PF.HINT_TARGET_SIZE;
|
|
614
995
|
}
|
|
615
996
|
|
|
616
|
-
const
|
|
617
|
-
|
|
997
|
+
const operator = operatorHex(readOperator(data, opOffset, typeCode));
|
|
998
|
+
const path = toHex(data, pathStart, pathEnd);
|
|
999
|
+
if (testScope === Scope.CONTEXT) return { scope: testScope, path, operator };
|
|
1000
|
+
return { scope: testScope, path, hint: toHex(data, pathEnd, opOffset), operator };
|
|
1001
|
+
}
|
|
618
1002
|
|
|
619
|
-
|
|
620
|
-
|
|
1003
|
+
/**
|
|
1004
|
+
* Spell a decoded operator as policy data does. The result is the operator's code, then one 32-byte word per constant
|
|
1005
|
+
* operand, or the context property's ID as a 32-byte word.
|
|
1006
|
+
*/
|
|
1007
|
+
export function operatorHex(operator: DecodedOperator): Hex {
|
|
1008
|
+
const words =
|
|
1009
|
+
operator.source === Source.CONTEXT ? [bigintToHex(BigInt(operator.contextPropertyId))] : operator.values;
|
|
1010
|
+
return `0x${operator.opCode.toString(16).padStart(2, "0")}${words.map((word) => word.slice(2)).join("")}`;
|
|
1011
|
+
}
|
|
621
1012
|
|
|
622
|
-
|
|
623
|
-
|
|
1013
|
+
///////////////////////////////////////////////////////////////////////////
|
|
1014
|
+
// Inspection
|
|
1015
|
+
///////////////////////////////////////////////////////////////////////////
|
|
624
1016
|
|
|
625
|
-
|
|
626
|
-
|
|
1017
|
+
/**
|
|
1018
|
+
* Inspect a policy blob: every field it stores with the bytes that contain it. The descriptor is a node tree, the hints
|
|
1019
|
+
* are split into their fields and the operators are parsed.
|
|
1020
|
+
* @param blob - The policy blob.
|
|
1021
|
+
* @returns The inspected policy, each operand loaded to its 32-byte word.
|
|
1022
|
+
* @throws {CallciumError} When the blob fails storage validation or its descriptor fails DV-1 or DV-2.
|
|
1023
|
+
*/
|
|
1024
|
+
function inspect(blob: Hex): DecodedPolicy {
|
|
1025
|
+
validatePolicy(blob);
|
|
1026
|
+
const data = hexToBytes(blob);
|
|
627
1027
|
|
|
628
|
-
|
|
629
|
-
|
|
1028
|
+
const headerByte = data[0]!;
|
|
1029
|
+
const header = fieldOf(
|
|
1030
|
+
{
|
|
1031
|
+
major: headerByte >> PF.MAJOR_SHIFT,
|
|
1032
|
+
minor: (headerByte >> PF.MINOR_SHIFT) & PF.MINOR_MASK,
|
|
1033
|
+
isSelectorless: (headerByte & PF.FLAG_NO_SELECTOR) !== 0,
|
|
1034
|
+
},
|
|
1035
|
+
0,
|
|
1036
|
+
PF.SELECTOR_OFFSET,
|
|
1037
|
+
);
|
|
1038
|
+
const descLength = uintField(data, PF.DESC_LENGTH_OFFSET, PF.DESC_LENGTH_OFFSET + PF.DESC_LENGTH_SIZE);
|
|
1039
|
+
const groupCountOffset = PF.DESC_OFFSET + descLength.value;
|
|
1040
|
+
const descriptor = inspectDescriptor(data.subarray(PF.DESC_OFFSET, groupCountOffset), PF.DESC_OFFSET);
|
|
1041
|
+
const groupCount = uintField(data, groupCountOffset, groupCountOffset + PF.GROUP_COUNT_SIZE);
|
|
630
1042
|
|
|
631
|
-
|
|
1043
|
+
const groups: DecodedGroup[] = [];
|
|
1044
|
+
let offset = groupCount.span.end;
|
|
1045
|
+
for (let groupIndex = 0; groupIndex < groupCount.value; groupIndex++) {
|
|
1046
|
+
const ruleCount = uintField(data, offset, offset + PF.GROUP_RULECOUNT_SIZE);
|
|
1047
|
+
const groupSize = uintField(data, ruleCount.span.end, offset + PF.GROUP_HEADER_SIZE);
|
|
1048
|
+
const groupEnd = groupSize.span.end + groupSize.value;
|
|
1049
|
+
const rules: DecodedRule[] = [];
|
|
1050
|
+
let ruleOffset = groupSize.span.end;
|
|
1051
|
+
for (let ruleIndex = 0; ruleIndex < ruleCount.value; ruleIndex++) {
|
|
1052
|
+
const rule = inspectRule(data, ruleOffset);
|
|
1053
|
+
rules.push(rule);
|
|
1054
|
+
ruleOffset = rule.span.end;
|
|
1055
|
+
}
|
|
1056
|
+
groups.push({ ruleCount, groupSize, rules, span: { start: offset, end: groupEnd } });
|
|
1057
|
+
offset = groupEnd;
|
|
1058
|
+
}
|
|
632
1059
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1060
|
+
return {
|
|
1061
|
+
header,
|
|
1062
|
+
selector: hexField(data, PF.SELECTOR_OFFSET, PF.SELECTOR_OFFSET + PF.SELECTOR_SIZE),
|
|
1063
|
+
descLength,
|
|
1064
|
+
descriptor,
|
|
1065
|
+
groupCount,
|
|
1066
|
+
groups,
|
|
1067
|
+
span: { start: 0, end: data.length },
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
637
1070
|
|
|
638
|
-
|
|
639
|
-
|
|
1071
|
+
/** Inspect the root rule at `ruleOffset`. */
|
|
1072
|
+
function inspectRule(data: Uint8Array, ruleOffset: number): DecodedRule {
|
|
1073
|
+
if (data[ruleOffset + PF.RULE_SCOPE_OFFSET] !== Scope.LOOP) return inspectTest(data, ruleOffset);
|
|
640
1074
|
|
|
641
|
-
|
|
642
|
-
|
|
1075
|
+
const size = uintField(data, ruleOffset, ruleOffset + PF.RULE_SIZE_SIZE);
|
|
1076
|
+
const quantifierOffset = ruleOffset + PF.RULE_QUANTIFIER_OFFSET;
|
|
1077
|
+
const quantifier = data[quantifierOffset]!;
|
|
1078
|
+
if (!isQuantifierCode(quantifier)) {
|
|
1079
|
+
throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier value ${quantifier}`, ruleOffset);
|
|
643
1080
|
}
|
|
1081
|
+
const depthOffset = ruleOffset + PF.LOOP_DEPTH_OFFSET;
|
|
1082
|
+
const pathDepth = uintField(data, depthOffset, depthOffset + PF.RULE_DEPTH_SIZE);
|
|
1083
|
+
const path = hexField(data, pathDepth.span.end, pathDepth.span.end + pathDepth.value * PF.PATH_STEP_SIZE);
|
|
1084
|
+
const hint = inspectFrameHint(data, path.span.end);
|
|
1085
|
+
const ruleCount = uintField(data, hint.span.end, hint.span.end + PF.GROUP_RULECOUNT_SIZE);
|
|
1086
|
+
const body: DecodedTest[] = [];
|
|
1087
|
+
let testOffset = ruleCount.span.end;
|
|
1088
|
+
for (let bodyIndex = 0; bodyIndex < ruleCount.value; bodyIndex++) {
|
|
1089
|
+
const test = inspectTest(data, testOffset);
|
|
1090
|
+
body.push(test);
|
|
1091
|
+
testOffset = test.span.end;
|
|
1092
|
+
}
|
|
1093
|
+
const scopeOffset = ruleOffset + PF.RULE_SCOPE_OFFSET;
|
|
1094
|
+
return {
|
|
1095
|
+
ruleSize: size,
|
|
1096
|
+
scope: fieldOf(Scope.LOOP, scopeOffset, scopeOffset + PF.RULE_SCOPE_SIZE),
|
|
1097
|
+
quantifier: fieldOf(quantifier, quantifierOffset, quantifierOffset + PF.RULE_QUANTIFIER_SIZE),
|
|
1098
|
+
pathDepth,
|
|
1099
|
+
path,
|
|
1100
|
+
hint,
|
|
1101
|
+
ruleCount,
|
|
1102
|
+
body,
|
|
1103
|
+
span: { start: ruleOffset, end: ruleOffset + size.value },
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
644
1106
|
|
|
645
|
-
|
|
1107
|
+
/** Inspect the test or context test at `ruleOffset`. */
|
|
1108
|
+
function inspectTest(data: Uint8Array, ruleOffset: number): DecodedTest {
|
|
1109
|
+
const size = uintField(data, ruleOffset, ruleOffset + PF.RULE_SIZE_SIZE);
|
|
1110
|
+
const scopeOffset = ruleOffset + PF.RULE_SCOPE_OFFSET;
|
|
1111
|
+
const testScope = data[scopeOffset]!;
|
|
1112
|
+
if (testScope !== Scope.CONTEXT && testScope !== Scope.CALLDATA) {
|
|
1113
|
+
throw new CallciumError("UNKNOWN_SCOPE", `Unknown scope value ${testScope}`, ruleOffset);
|
|
1114
|
+
}
|
|
1115
|
+
const pathDepth = uintField(data, ruleOffset + PF.RULE_DEPTH_OFFSET, ruleOffset + PF.RULE_PATH_OFFSET);
|
|
1116
|
+
const pathStart = ruleOffset + PF.RULE_PATH_OFFSET;
|
|
1117
|
+
let path: Field<Hex>;
|
|
1118
|
+
let hint: DecodedHint | undefined;
|
|
1119
|
+
let targetType: number;
|
|
1120
|
+
if (testScope === Scope.CONTEXT) {
|
|
1121
|
+
path = hexField(data, pathStart, pathStart + PF.PROPERTY_ID_SIZE);
|
|
1122
|
+
targetType = contextPropertyType(readU16(data, pathStart));
|
|
1123
|
+
} else {
|
|
1124
|
+
path = hexField(data, pathStart, pathStart + pathDepth.value * PF.PATH_STEP_SIZE);
|
|
1125
|
+
hint = inspectTestHint(data, path.span.end);
|
|
1126
|
+
targetType = hint.typeCode.value;
|
|
1127
|
+
}
|
|
1128
|
+
const opOffset = hint === undefined ? path.span.end : hint.span.end;
|
|
1129
|
+
const opCode = uintField(data, opOffset, opOffset + PF.RULE_OPCODE_SIZE);
|
|
1130
|
+
const dataLength = uintField(data, opCode.span.end, opCode.span.end + PF.RULE_DATALENGTH_SIZE);
|
|
1131
|
+
return {
|
|
1132
|
+
ruleSize: size,
|
|
1133
|
+
scope: fieldOf(testScope, scopeOffset, scopeOffset + PF.RULE_SCOPE_SIZE),
|
|
1134
|
+
pathDepth,
|
|
1135
|
+
path,
|
|
1136
|
+
...(hint !== undefined && { hint }),
|
|
1137
|
+
targetType,
|
|
1138
|
+
opCode,
|
|
1139
|
+
dataLength,
|
|
1140
|
+
data: hexField(data, dataLength.span.end, dataLength.span.end + dataLength.value),
|
|
1141
|
+
operator: readOperator(data, opOffset, targetType),
|
|
1142
|
+
span: { start: ruleOffset, end: ruleOffset + size.value },
|
|
1143
|
+
};
|
|
646
1144
|
}
|
|
647
1145
|
|
|
648
|
-
/**
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
} else {
|
|
670
|
-
const constraint: Constraint = {
|
|
671
|
-
scope: rule.scope.value,
|
|
672
|
-
path: rule.path.value,
|
|
673
|
-
operators: [opHex],
|
|
674
|
-
...(hintHex !== undefined && { hint: hintHex }),
|
|
675
|
-
span: rule.span,
|
|
676
|
-
};
|
|
677
|
-
constraintMap.set(key, constraint);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
1146
|
+
/** Inspect the header and hops of the hint at `hintOffset`, with the offset past its last hop. */
|
|
1147
|
+
function inspectHops(
|
|
1148
|
+
data: Uint8Array,
|
|
1149
|
+
hintOffset: number,
|
|
1150
|
+
): { hopCount: Field<number>; hops: DecodedHop[]; end: number } {
|
|
1151
|
+
const hopCount = uintField(data, hintOffset, hintOffset + PF.HINT_HEADER_SIZE);
|
|
1152
|
+
const hops: DecodedHop[] = [];
|
|
1153
|
+
let offset = hopCount.span.end;
|
|
1154
|
+
for (let hopIndex = 0; hopIndex < hopCount.value; hopIndex++) {
|
|
1155
|
+
const index = uintField(data, offset + PF.HINT_HOP_INDEX_OFFSET, offset + PF.HINT_HOP_META_OFFSET);
|
|
1156
|
+
hops.push({
|
|
1157
|
+
kind: index.value === PF.HINT_NO_INDEX ? "plain" : "element",
|
|
1158
|
+
delta: uintField(data, offset, offset + PF.HINT_HOP_INDEX_OFFSET),
|
|
1159
|
+
index,
|
|
1160
|
+
meta: metaField(data, offset + PF.HINT_HOP_META_OFFSET, offset + PF.HINT_HOP_SIZE),
|
|
1161
|
+
span: { start: offset, end: offset + PF.HINT_HOP_SIZE },
|
|
1162
|
+
});
|
|
1163
|
+
offset += PF.HINT_HOP_SIZE;
|
|
1164
|
+
}
|
|
1165
|
+
return { hopCount, hops, end: offset };
|
|
1166
|
+
}
|
|
680
1167
|
|
|
681
|
-
|
|
682
|
-
|
|
1168
|
+
/** Inspect the test hint at `hintOffset`. */
|
|
1169
|
+
function inspectTestHint(data: Uint8Array, hintOffset: number): DecodedHint {
|
|
1170
|
+
const { hopCount, hops, end: target } = inspectHops(data, hintOffset);
|
|
1171
|
+
return {
|
|
1172
|
+
hopCount,
|
|
1173
|
+
hops,
|
|
1174
|
+
targetDelta: uintField(data, target, target + PF.HINT_TARGET_META_OFFSET),
|
|
1175
|
+
targetMeta: metaField(data, target + PF.HINT_TARGET_META_OFFSET, target + PF.HINT_TARGET_TYPECODE_OFFSET),
|
|
1176
|
+
typeCode: uintField(data, target + PF.HINT_TARGET_TYPECODE_OFFSET, target + PF.HINT_TARGET_SIZE),
|
|
1177
|
+
span: { start: hintOffset, end: target + PF.HINT_TARGET_SIZE },
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
683
1180
|
|
|
1181
|
+
/** Inspect the frame hint at `hintOffset`. */
|
|
1182
|
+
function inspectFrameHint(data: Uint8Array, hintOffset: number): DecodedFrameHint {
|
|
1183
|
+
const { hopCount, hops, end: frame } = inspectHops(data, hintOffset);
|
|
684
1184
|
return {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1185
|
+
hopCount,
|
|
1186
|
+
hops,
|
|
1187
|
+
arrayDelta: uintField(data, frame, frame + PF.HINT_FRAME_COUNT_OFFSET),
|
|
1188
|
+
count: uintField(data, frame + PF.HINT_FRAME_COUNT_OFFSET, frame + PF.HINT_FRAME_META_OFFSET),
|
|
1189
|
+
meta: metaField(data, frame + PF.HINT_FRAME_META_OFFSET, frame + PF.HINT_FRAME_PREFIX_SIZE),
|
|
1190
|
+
span: { start: hintOffset, end: frame + PF.HINT_FRAME_PREFIX_SIZE },
|
|
690
1191
|
};
|
|
691
1192
|
}
|
|
692
1193
|
|
|
1194
|
+
/** A value with the bytes from `start` to `end` that contain it. */
|
|
1195
|
+
function fieldOf<T>(value: T, start: number, end: number): Field<T> {
|
|
1196
|
+
return { value, span: { start, end } };
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/** The bytes from `start` to `end` as a hex field. */
|
|
1200
|
+
function hexField(data: Uint8Array, start: number, end: number): Field<Hex> {
|
|
1201
|
+
return fieldOf(toHex(data, start, end), start, end);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/** The big-endian unsigned integer the bytes from `start` to `end` represent. */
|
|
1205
|
+
function uintField(data: Uint8Array, start: number, end: number): Field<number> {
|
|
1206
|
+
let value = 0;
|
|
1207
|
+
for (let offset = start; offset < end; offset++) value = value * 256 + data[offset]!;
|
|
1208
|
+
return fieldOf(value, start, end);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/** The meta word the bytes from `start` to `end` represent. */
|
|
1212
|
+
function metaField(data: Uint8Array, start: number, end: number): Field<ArrayMeta> {
|
|
1213
|
+
const meta = readU16(data, start);
|
|
1214
|
+
const value = {
|
|
1215
|
+
isElementDynamic: (meta & PF.HINT_META_ELEM_DYNAMIC) !== 0,
|
|
1216
|
+
isDynamicArray: (meta & PF.HINT_META_DYNAMIC_ARRAY) !== 0,
|
|
1217
|
+
strideWords: meta & PF.HINT_META_STRIDE_MASK,
|
|
1218
|
+
};
|
|
1219
|
+
return fieldOf(value, start, end);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
///////////////////////////////////////////////////////////////////////////
|
|
1223
|
+
// Helpers
|
|
1224
|
+
///////////////////////////////////////////////////////////////////////////
|
|
1225
|
+
|
|
1226
|
+
/** The `pathDepth` of a context test, whose path is its property ID. */
|
|
1227
|
+
const CONTEXT_PATH_DEPTH = 1;
|
|
1228
|
+
|
|
1229
|
+
/** Sort items ascending by their key bytes and drop repeats. */
|
|
1230
|
+
function sortDedupe<T>(items: T[], key: (item: T) => Uint8Array): T[] {
|
|
1231
|
+
const sorted = items.toSorted((first, second) => compareBytes(key(first), key(second)));
|
|
1232
|
+
return sorted.filter((item, index) => index === 0 || compareBytes(key(item), key(sorted[index - 1]!)) !== 0);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** Compare two byte arrays lexicographically. A prefix sorts first. */
|
|
1236
|
+
function compareBytes(first: Uint8Array, second: Uint8Array): number {
|
|
1237
|
+
const length = Math.min(first.length, second.length);
|
|
1238
|
+
for (let i = 0; i < length; i++) {
|
|
1239
|
+
if (first[i] !== second[i]) return first[i]! - second[i]!;
|
|
1240
|
+
}
|
|
1241
|
+
return first.length - second.length;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/** Whether the rule is an `ALL` loop. */
|
|
1245
|
+
function isAllLoop(rule: Rule): rule is LoopRule {
|
|
1246
|
+
return rule.scope === Scope.LOOP && rule.quantifier === LoopQuantifier.ALL;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/** Whether the rule is an `ANY` loop whose body contains a calldata test, which gives up its context tests. */
|
|
1250
|
+
function hoists(rule: Rule): rule is LoopRule {
|
|
1251
|
+
return (
|
|
1252
|
+
rule.scope === Scope.LOOP &&
|
|
1253
|
+
rule.quantifier === LoopQuantifier.ANY &&
|
|
1254
|
+
rule.body.some((test) => test.scope === Scope.CALLDATA)
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/** Return the comparison equal to `NOT comparison`, or undefined where none is. */
|
|
1259
|
+
function negationOf(comparison: number): number | undefined {
|
|
1260
|
+
switch (comparison) {
|
|
1261
|
+
case Op.GT:
|
|
1262
|
+
return Op.LTE;
|
|
1263
|
+
case Op.LT:
|
|
1264
|
+
return Op.GTE;
|
|
1265
|
+
case Op.GTE:
|
|
1266
|
+
return Op.LT;
|
|
1267
|
+
case Op.LTE:
|
|
1268
|
+
return Op.GT;
|
|
1269
|
+
case Op.BITMASK_ANY:
|
|
1270
|
+
return Op.BITMASK_NONE;
|
|
1271
|
+
case Op.BITMASK_NONE:
|
|
1272
|
+
return Op.BITMASK_ANY;
|
|
1273
|
+
case Op.LENGTH_GT:
|
|
1274
|
+
return Op.LENGTH_LTE;
|
|
1275
|
+
case Op.LENGTH_LT:
|
|
1276
|
+
return Op.LENGTH_GTE;
|
|
1277
|
+
case Op.LENGTH_GTE:
|
|
1278
|
+
return Op.LENGTH_LT;
|
|
1279
|
+
case Op.LENGTH_LTE:
|
|
1280
|
+
return Op.LENGTH_GT;
|
|
1281
|
+
default:
|
|
1282
|
+
return undefined;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
693
1286
|
/**
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
* @returns The inspected policy with per-field spans for every structural element.
|
|
697
|
-
* @throws {CallciumError} If the blob is structurally malformed.
|
|
1287
|
+
* Return the comparison that holds with subject and operand exchanged, or undefined where a context operand takes
|
|
1288
|
+
* none.
|
|
698
1289
|
*/
|
|
699
|
-
function
|
|
700
|
-
|
|
1290
|
+
function swapOf(comparison: number): number | undefined {
|
|
1291
|
+
switch (comparison) {
|
|
1292
|
+
case Op.EQ:
|
|
1293
|
+
return Op.EQ;
|
|
1294
|
+
case Op.GT:
|
|
1295
|
+
return Op.LT;
|
|
1296
|
+
case Op.LT:
|
|
1297
|
+
return Op.GT;
|
|
1298
|
+
case Op.GTE:
|
|
1299
|
+
return Op.LTE;
|
|
1300
|
+
case Op.LTE:
|
|
1301
|
+
return Op.GTE;
|
|
1302
|
+
default:
|
|
1303
|
+
return undefined;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/** Require a value to fit the big-endian field of `byteWidth` bytes. */
|
|
1308
|
+
function requireFieldFits(value: number, byteWidth: number, code: CallciumErrorCode, subject: string): void {
|
|
1309
|
+
const max = fieldMax(byteWidth);
|
|
1310
|
+
if (value > max) throw new CallciumError(code, `${subject} ${value} exceeds maximum ${max}`);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/** Return `value` as a 2-byte big-endian field. */
|
|
1314
|
+
function be16(value: number): Uint8Array {
|
|
1315
|
+
const field = new Uint8Array(2);
|
|
1316
|
+
writeBE16(field, 0, value);
|
|
1317
|
+
return field;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/** Return `value` as a 4-byte big-endian field. */
|
|
1321
|
+
function be32(value: number): Uint8Array {
|
|
1322
|
+
const field = new Uint8Array(4);
|
|
1323
|
+
writeBE32(field, 0, value);
|
|
1324
|
+
return field;
|
|
701
1325
|
}
|
|
702
1326
|
|
|
703
|
-
/** Encode, decode
|
|
1327
|
+
/** Encode policies in canonical form, and decode and inspect policy blobs. */
|
|
704
1328
|
export const PolicyCoder = { encode, decode, inspect };
|