@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/policy-coder.ts
CHANGED
|
@@ -1,695 +1,1328 @@
|
|
|
1
|
-
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
2
1
|
import { concatBytes } from "@noble/hashes/utils.js";
|
|
3
2
|
|
|
4
|
-
import { bytesToHex, hexToBytes,
|
|
3
|
+
import { bigintToHex, bytesToHex, hexToBytes, readU16, readU32, toHex, writeBE16, writeBE32 } from "./bytes";
|
|
5
4
|
import {
|
|
6
|
-
|
|
5
|
+
comparisonOf,
|
|
7
6
|
fieldMax,
|
|
8
7
|
formatCode,
|
|
8
|
+
isComparisonCode,
|
|
9
|
+
isQuantifierCode,
|
|
10
|
+
isScopeCode,
|
|
11
|
+
isSourceCode,
|
|
12
|
+
LoopQuantifier,
|
|
9
13
|
MAX_CONTEXT_PROPERTY_ID,
|
|
10
14
|
Op,
|
|
11
15
|
PolicyFormat as PF,
|
|
12
16
|
Scope,
|
|
17
|
+
Source,
|
|
18
|
+
sourceOf,
|
|
13
19
|
TypeCode,
|
|
20
|
+
type QuantifierCode,
|
|
21
|
+
type ScopeCode,
|
|
14
22
|
} from "./constants";
|
|
15
23
|
import { Descriptor } from "./descriptor";
|
|
16
|
-
import { decodeDescriptor } from "./descriptor-coder";
|
|
17
|
-
import { CallciumError
|
|
24
|
+
import { decodeDescriptor, inspectDescriptor } from "./descriptor-coder";
|
|
25
|
+
import { CallciumError } from "./errors";
|
|
26
|
+
import { Hint } from "./hint";
|
|
18
27
|
import {
|
|
19
|
-
|
|
28
|
+
acceptsContextSource,
|
|
29
|
+
canonicalize,
|
|
30
|
+
contextPropertyType,
|
|
31
|
+
hasCalldataLength,
|
|
32
|
+
isLeftAligned,
|
|
20
33
|
isLengthOp,
|
|
21
|
-
|
|
34
|
+
isValidPayloadSize,
|
|
35
|
+
LENGTH_TYPE,
|
|
36
|
+
operandTypeOf,
|
|
37
|
+
operandWidth,
|
|
38
|
+
readMember,
|
|
39
|
+
readOperator,
|
|
22
40
|
toBigInt,
|
|
23
|
-
isAddressableTarget,
|
|
24
|
-
isValidOperatorData,
|
|
25
|
-
toOperatorBytes,
|
|
26
41
|
} from "./operators";
|
|
27
|
-
import { parsePathSteps } from "./path";
|
|
42
|
+
import { encodePath, parsePathSteps } from "./path";
|
|
28
43
|
|
|
44
|
+
import type { CallciumErrorCode } from "./errors";
|
|
45
|
+
import type { Hex } from "./types";
|
|
29
46
|
import type {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
ArrayMeta,
|
|
48
|
+
BodyRule,
|
|
49
|
+
DecodedFrameHint,
|
|
33
50
|
DecodedGroup,
|
|
34
|
-
|
|
51
|
+
DecodedHint,
|
|
52
|
+
DecodedHop,
|
|
53
|
+
DecodedOperator,
|
|
35
54
|
DecodedPolicy,
|
|
36
55
|
DecodedRule,
|
|
56
|
+
DecodedTest,
|
|
57
|
+
Field,
|
|
58
|
+
LoopRule,
|
|
37
59
|
PolicyData,
|
|
60
|
+
Rule,
|
|
38
61
|
} from "./types";
|
|
39
62
|
|
|
40
63
|
///////////////////////////////////////////////////////////////////////////
|
|
41
|
-
// Policy
|
|
64
|
+
// Policy validation
|
|
42
65
|
///////////////////////////////////////////////////////////////////////////
|
|
43
66
|
|
|
44
|
-
/** Wrap a value with its byte span for positional tracking. */
|
|
45
|
-
function field<T>(value: T, start: number, end: number): Field<T> {
|
|
46
|
-
return { value, span: { start, end } };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
67
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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.
|
|
52
71
|
*/
|
|
53
|
-
function
|
|
54
|
-
let hex: Hex | undefined;
|
|
55
|
-
return {
|
|
56
|
-
get value(): Hex {
|
|
57
|
-
return (hex ??= toHex(data, start, end));
|
|
58
|
-
},
|
|
59
|
-
set value(replacement: Hex) {
|
|
60
|
-
hex = replacement;
|
|
61
|
-
},
|
|
62
|
-
span: { start, end },
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
///////////////////////////////////////////////////////////////////////////
|
|
67
|
-
// Hint block
|
|
68
|
-
///////////////////////////////////////////////////////////////////////////
|
|
69
|
-
|
|
70
|
-
/** Resolve the hint block size at `hintStart` from its header and suffix header bytes. */
|
|
71
|
-
function resolveHintSize(data: Uint8Array, hintStart: number, ruleEnd: number, ruleOffset: number): number {
|
|
72
|
-
const overrun = (): never => {
|
|
73
|
-
throw new CallciumError("RULE_FIELD_OUT_OF_BOUNDS", "Rule is too small to hold a hint block", ruleOffset);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
if (hintStart + PF.HINT_HEADER_SIZE > ruleEnd) overrun();
|
|
77
|
-
const header = data[hintStart]!;
|
|
78
|
-
const hopsEnd = hintStart + PF.HINT_HEADER_SIZE + (header & PF.HINT_HOP_COUNT_MASK) * PF.HINT_HOP_SIZE;
|
|
79
|
-
let hintSize = hopsEnd - hintStart + PF.HINT_TARGET_SIZE;
|
|
80
|
-
|
|
81
|
-
// A quantifier frame sits between the main chain and the target, sized by its own header byte.
|
|
82
|
-
if (header >> PF.HINT_KIND_SHIFT !== PF.HINT_KIND_NONE) {
|
|
83
|
-
const suffixHeaderOffset = hopsEnd + PF.HINT_FRAME_PREFIX_SIZE;
|
|
84
|
-
if (suffixHeaderOffset >= ruleEnd) overrun();
|
|
85
|
-
const suffixHeader = data[suffixHeaderOffset]!;
|
|
86
|
-
hintSize +=
|
|
87
|
-
PF.HINT_FRAME_PREFIX_SIZE + PF.HINT_HEADER_SIZE + (suffixHeader & PF.HINT_HOP_COUNT_MASK) * PF.HINT_HOP_SIZE;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (hintStart + hintSize > ruleEnd) overrun();
|
|
91
|
-
return hintSize;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Throw a MALFORMED_HINT error with the given detail. */
|
|
95
|
-
function malformedHint(detail: string, ruleOffset: number): never {
|
|
96
|
-
throw new CallciumError("MALFORMED_HINT", detail, ruleOffset);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Require every hop entry in `[start, end)` to carry no reserved or unused state. */
|
|
100
|
-
function validateHops(data: Uint8Array, start: number, end: number, ruleOffset: number): void {
|
|
101
|
-
for (let offset = start; offset < end; offset += PF.HINT_HOP_SIZE) {
|
|
102
|
-
const index = readU16(data, offset + PF.HINT_HOP_INDEX_OFFSET);
|
|
103
|
-
if (index === PF.HINT_INDEX_RESERVED) malformedHint("Hop index is a reserved value", ruleOffset);
|
|
104
|
-
|
|
105
|
-
const meta = readU16(data, offset + PF.HINT_HOP_META_OFFSET);
|
|
106
|
-
// A plain hop carries no element meta; an element hop addresses no offset of its own.
|
|
107
|
-
if (index === PF.HINT_NO_INDEX) {
|
|
108
|
-
if (meta !== 0) malformedHint("Plain hop carries a meta word", ruleOffset);
|
|
109
|
-
} else {
|
|
110
|
-
if (readU32(data, offset) !== 0) malformedHint("Element hop carries a delta", ruleOffset);
|
|
111
|
-
if ((meta & PF.HINT_META_RESERVED_MASK) !== 0) malformedHint("Hop meta carries reserved bits", ruleOffset);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Require the hint block at `hintStart` to carry no reserved or unused state. */
|
|
117
|
-
function validateHint(data: Uint8Array, hintStart: number, hintSize: number, ruleOffset: number): void {
|
|
118
|
-
const header = data[hintStart]!;
|
|
119
|
-
const kind = header >> PF.HINT_KIND_SHIFT;
|
|
120
|
-
if (kind > PF.HINT_KIND_MAX) malformedHint("Header kind is a reserved value", ruleOffset);
|
|
121
|
-
|
|
122
|
-
const hopsStart = hintStart + PF.HINT_HEADER_SIZE;
|
|
123
|
-
const hopsEnd = hopsStart + (header & PF.HINT_HOP_COUNT_MASK) * PF.HINT_HOP_SIZE;
|
|
124
|
-
validateHops(data, hopsStart, hopsEnd, ruleOffset);
|
|
125
|
-
|
|
126
|
-
const targetOffset = hintStart + hintSize - PF.HINT_TARGET_SIZE;
|
|
127
|
-
if (kind !== PF.HINT_KIND_NONE) {
|
|
128
|
-
const frameMeta = readU16(data, hopsEnd + PF.HINT_FRAME_META_OFFSET);
|
|
129
|
-
if ((frameMeta & PF.HINT_META_RESERVED_MASK) !== 0) malformedHint("Frame meta carries reserved bits", ruleOffset);
|
|
130
|
-
const suffixHeader = data[hopsEnd + PF.HINT_FRAME_PREFIX_SIZE]!;
|
|
131
|
-
if ((suffixHeader & PF.HINT_SUFFIX_RESERVED_MASK) !== 0) {
|
|
132
|
-
malformedHint("Suffix header carries reserved bits", ruleOffset);
|
|
133
|
-
}
|
|
134
|
-
validateHops(data, hopsEnd + PF.HINT_FRAME_PREFIX_SIZE + PF.HINT_HEADER_SIZE, targetOffset, ruleOffset);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// A target meta word describes a dynamic array and is absent for every other type.
|
|
138
|
-
const targetMeta = readU16(data, targetOffset + PF.HINT_TARGET_META_OFFSET);
|
|
139
|
-
const targetTypeCode = data[targetOffset + PF.HINT_TARGET_TYPECODE_OFFSET]!;
|
|
140
|
-
const targetMetaOk =
|
|
141
|
-
targetTypeCode === TypeCode.DYNAMIC_ARRAY ? (targetMeta & PF.HINT_META_RESERVED_MASK) === 0 : targetMeta === 0;
|
|
142
|
-
if (!targetMetaOk) malformedHint("Target meta carries reserved or unused state", ruleOffset);
|
|
143
|
-
|
|
144
|
-
// An operator reads either a scalar word or a declared length, so a target carrying neither — a tuple, a static
|
|
145
|
-
// array, or an undefined code — addresses nothing.
|
|
146
|
-
if (!isAddressableTarget(targetTypeCode)) malformedHint("Target type code addresses no value", ruleOffset);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Read the type code the hint block at `hintStart` declares for its target. */
|
|
150
|
-
function hintTargetTypeCode(data: Uint8Array, hintStart: number, hintSize: number): number {
|
|
151
|
-
return data[hintStart + hintSize - PF.HINT_TARGET_SIZE + PF.HINT_TARGET_TYPECODE_OFFSET]!;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
///////////////////////////////////////////////////////////////////////////
|
|
155
|
-
// Policy decoder
|
|
156
|
-
///////////////////////////////////////////////////////////////////////////
|
|
157
|
-
|
|
158
|
-
/** Decode a policy blob, returning the structural representation and the blob bytes it spans. */
|
|
159
|
-
export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Array } {
|
|
72
|
+
export function validatePolicy(blob: Hex): void {
|
|
160
73
|
const data = hexToBytes(blob);
|
|
161
74
|
if (data.length < PF.DESC_OFFSET) {
|
|
162
75
|
throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short");
|
|
163
76
|
}
|
|
164
77
|
|
|
165
78
|
const headerByte = data[0]!;
|
|
166
|
-
const
|
|
167
|
-
if (
|
|
168
|
-
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})`);
|
|
169
82
|
}
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
);
|
|
172
90
|
}
|
|
173
91
|
|
|
174
|
-
const selectorStart = PF.SELECTOR_OFFSET;
|
|
175
|
-
const selectorEnd = selectorStart + PF.SELECTOR_SIZE;
|
|
176
|
-
const selectorHex = toHex(data, selectorStart, selectorEnd);
|
|
177
|
-
|
|
178
92
|
const isSelectorless = (headerByte & PF.FLAG_NO_SELECTOR) !== 0;
|
|
179
|
-
if (isSelectorless &&
|
|
93
|
+
if (isSelectorless && readU32(data, PF.SELECTOR_OFFSET) !== 0) {
|
|
180
94
|
throw new CallciumError("MALFORMED_HEADER", "Selectorless policy must have a zeroed selector slot");
|
|
181
95
|
}
|
|
182
96
|
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
if (descLengthValue < 2) {
|
|
186
|
-
throw new CallciumError("MALFORMED_HEADER", "Descriptor length must be at least 2");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const descStart = PF.DESC_OFFSET;
|
|
190
|
-
const descEnd = descStart + descLengthValue;
|
|
191
|
-
const groupCountOffset = descEnd;
|
|
192
|
-
|
|
97
|
+
const descLengthValue = readU16(data, PF.DESC_LENGTH_OFFSET);
|
|
98
|
+
const groupCountOffset = PF.DESC_OFFSET + descLengthValue;
|
|
193
99
|
if (data.length < groupCountOffset + PF.GROUP_COUNT_SIZE) {
|
|
194
100
|
throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short for descriptor and group count");
|
|
195
101
|
}
|
|
102
|
+
decodeDescriptor(data.subarray(PF.DESC_OFFSET, groupCountOffset));
|
|
196
103
|
|
|
197
|
-
// Decode the embedded descriptor, offsetting spans to be relative to the policy blob.
|
|
198
|
-
const descSlice = data.subarray(descStart, descEnd);
|
|
199
|
-
const { descriptor: desc } = decodeDescriptor(descSlice);
|
|
200
|
-
const params: DecodedParam[] = desc.params.map((param) => ({
|
|
201
|
-
...param,
|
|
202
|
-
span: {
|
|
203
|
-
start: descStart + param.span.start,
|
|
204
|
-
end: descStart + param.span.end,
|
|
205
|
-
},
|
|
206
|
-
}));
|
|
207
|
-
|
|
208
|
-
const descriptorRaw = hexField(data, descStart, descEnd);
|
|
209
|
-
|
|
210
|
-
const groupCountStart = groupCountOffset;
|
|
211
|
-
const groupCountEnd = groupCountStart + PF.GROUP_COUNT_SIZE;
|
|
212
104
|
const groupCountValue = data[groupCountOffset]!;
|
|
213
105
|
if (groupCountValue === 0) {
|
|
214
106
|
throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
|
|
215
107
|
}
|
|
216
108
|
|
|
217
|
-
let offset =
|
|
218
|
-
const groups: DecodedGroup[] = [];
|
|
219
|
-
|
|
109
|
+
let offset = groupCountOffset + PF.GROUP_COUNT_SIZE;
|
|
220
110
|
for (let groupIndex = 0; groupIndex < groupCountValue; groupIndex++) {
|
|
221
111
|
if (offset + PF.GROUP_HEADER_SIZE > data.length) {
|
|
222
112
|
throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading group header", offset);
|
|
223
113
|
}
|
|
224
114
|
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
const groupBodyStart = offset + PF.GROUP_HEADER_SIZE;
|
|
228
|
-
const groupEnd = groupBodyStart + groupSizeValue;
|
|
229
|
-
|
|
115
|
+
const groupSizeValue = readU32(data, offset + PF.GROUP_RULECOUNT_SIZE);
|
|
116
|
+
const groupEnd = offset + PF.GROUP_HEADER_SIZE + groupSizeValue;
|
|
230
117
|
if (groupEnd > data.length) {
|
|
231
118
|
throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
|
|
232
119
|
}
|
|
233
120
|
|
|
234
|
-
const
|
|
235
|
-
const ruleCountValue = readU16(data, ruleCountStart);
|
|
121
|
+
const ruleCountValue = readU16(data, offset);
|
|
236
122
|
if (ruleCountValue === 0) {
|
|
237
123
|
throw new CallciumError("EMPTY_GROUP", "Group must contain at least one rule", offset);
|
|
238
124
|
}
|
|
239
|
-
|
|
240
|
-
|
|
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);
|
|
241
130
|
}
|
|
242
131
|
|
|
243
|
-
|
|
244
|
-
|
|
132
|
+
if (ruleOffset !== groupEnd) groupSizeMismatch(offset);
|
|
133
|
+
offset = groupEnd;
|
|
134
|
+
}
|
|
245
135
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
+
}
|
|
141
|
+
|
|
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);
|
|
250
204
|
|
|
251
|
-
|
|
252
|
-
|
|
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) {
|
|
253
247
|
throw new CallciumError(
|
|
254
|
-
"
|
|
255
|
-
|
|
248
|
+
"INVALID_CONTEXT_PATH",
|
|
249
|
+
"Context-scope rules must have exactly one path step",
|
|
256
250
|
ruleOffset,
|
|
257
251
|
);
|
|
258
252
|
}
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
);
|
|
261
262
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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);
|
|
267
271
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
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);
|
|
281
288
|
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
default:
|
|
292
|
+
return scopeValue satisfies never;
|
|
293
|
+
}
|
|
282
294
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
const operatorEnd = validateOperator(data, ruleOffset, operatorOffset, ruleEnd, typeCode);
|
|
296
|
+
if (operatorEnd !== ruleEnd) ruleSizeMismatch(ruleOffset);
|
|
297
|
+
}
|
|
286
298
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
}
|
|
296
327
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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);
|
|
303
336
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
"OPERATOR_TARGET_MISMATCH",
|
|
311
|
-
"Operator family does not match the type the target declares",
|
|
312
|
-
ruleOffset,
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
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
|
+
}
|
|
316
343
|
|
|
317
|
-
|
|
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
|
+
}
|
|
318
351
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
}
|
|
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
|
+
}
|
|
331
362
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
}
|
|
342
382
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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)) {
|
|
348
401
|
throw new CallciumError(
|
|
349
|
-
"
|
|
350
|
-
|
|
402
|
+
"INVALID_CONTEXT_OPERAND",
|
|
403
|
+
"Context operand applies only to EQ, GT, LT, GTE and LTE",
|
|
351
404
|
ruleOffset,
|
|
352
405
|
);
|
|
353
406
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const contextPropertyId = readU16(data, pathStart);
|
|
365
|
-
if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) {
|
|
366
|
-
throw new CallciumError(
|
|
367
|
-
"UNKNOWN_CONTEXT_PROPERTY",
|
|
368
|
-
`Context rule references undefined context property ${formatCode(contextPropertyId, PF.PATH_STEP_SIZE)}`,
|
|
369
|
-
ruleOffset,
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const pathHex = toHex(data, pathStart, pathStart + pathLength);
|
|
375
|
-
rules.push({
|
|
376
|
-
ruleSize: field(ruleSizeValue, ruleOffset, ruleOffset + PF.RULE_SIZE_SIZE),
|
|
377
|
-
scope: field(scopeValue, scopeOffset, scopeOffset + 1),
|
|
378
|
-
pathDepth: field(depthValue, depthOffset, depthOffset + 1),
|
|
379
|
-
path: field(pathHex, pathStart, pathStart + pathLength),
|
|
380
|
-
...(hintSize > 0 && {
|
|
381
|
-
hint: hexField(data, hintStart, hintStart + hintSize),
|
|
382
|
-
}),
|
|
383
|
-
opCode: field(opCodeValue, opCodeOffset, opCodeOffset + PF.RULE_OPCODE_SIZE),
|
|
384
|
-
dataLength: field(dataLengthValue, dataLengthOffset, dataLengthOffset + PF.RULE_DATALENGTH_SIZE),
|
|
385
|
-
data: hexField(data, dataStart, dataStart + dataLengthValue),
|
|
386
|
-
span: { start: ruleOffset, end: ruleOffset + ruleSizeValue },
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
if (ruleOffset + ruleSizeValue > groupEnd) {
|
|
390
|
-
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
|
+
);
|
|
391
417
|
}
|
|
392
|
-
|
|
418
|
+
break;
|
|
393
419
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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;
|
|
397
428
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
ruleCount: field(ruleCountValue, ruleCountStart, ruleCountStart + PF.GROUP_RULECOUNT_SIZE),
|
|
401
|
-
groupSize: field(groupSizeValue, groupSizeStart, groupSizeStart + PF.GROUP_SIZE_SIZE),
|
|
402
|
-
rules,
|
|
403
|
-
span: { start: offset, end: groupEnd },
|
|
404
|
-
});
|
|
405
|
-
offset = groupEnd;
|
|
429
|
+
default:
|
|
430
|
+
return source satisfies never;
|
|
406
431
|
}
|
|
432
|
+
return operatorEnd;
|
|
433
|
+
}
|
|
407
434
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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;
|
|
411
454
|
}
|
|
455
|
+
}
|
|
412
456
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
}
|
|
461
|
+
|
|
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
|
+
}
|
|
434
480
|
|
|
435
|
-
|
|
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
|
+
);
|
|
436
488
|
}
|
|
437
489
|
|
|
438
490
|
///////////////////////////////////////////////////////////////////////////
|
|
439
|
-
//
|
|
491
|
+
// Encoding
|
|
440
492
|
///////////////////////////////////////////////////////////////////////////
|
|
441
493
|
|
|
442
|
-
|
|
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 };
|
|
499
|
+
|
|
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
|
+
}
|
|
443
539
|
|
|
444
|
-
/**
|
|
445
|
-
|
|
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
|
+
}
|
|
446
545
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
}
|
|
450
616
|
|
|
451
|
-
|
|
452
|
-
|
|
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) {
|
|
453
623
|
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step");
|
|
454
624
|
}
|
|
455
|
-
if (
|
|
625
|
+
if (path.length % PF.PATH_STEP_SIZE !== 0) {
|
|
456
626
|
throw new CallciumError("MALFORMED_PATH", "Path byte length must be even");
|
|
457
627
|
}
|
|
458
|
-
const depth = path.length /
|
|
628
|
+
const depth = depthBefore + path.length / PF.PATH_STEP_SIZE;
|
|
459
629
|
if (depth > PF.MAX_PATH_DEPTH) {
|
|
460
630
|
throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${PF.MAX_PATH_DEPTH}`);
|
|
461
631
|
}
|
|
462
|
-
|
|
463
|
-
const hint =
|
|
464
|
-
constraint.scope === Scope.CALLDATA
|
|
465
|
-
? Descriptor.compileHint(desc, parsePathSteps(constraint.path))
|
|
466
|
-
: new Uint8Array(0);
|
|
467
|
-
return { path, hint };
|
|
632
|
+
return depth;
|
|
468
633
|
}
|
|
469
634
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// with its siblings. A constraint carrying no operator produces no rule, so its path is never read.
|
|
474
|
-
let target: ConstraintTarget | undefined;
|
|
635
|
+
///////////////////////////////////////////////////////////////////////////
|
|
636
|
+
// Normalization
|
|
637
|
+
///////////////////////////////////////////////////////////////////////////
|
|
475
638
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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));
|
|
481
646
|
}
|
|
482
647
|
|
|
483
|
-
/**
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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;
|
|
488
682
|
}
|
|
489
|
-
|
|
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 };
|
|
490
699
|
}
|
|
491
700
|
|
|
492
|
-
/**
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (
|
|
501
|
-
|
|
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));
|
|
502
722
|
});
|
|
723
|
+
return joined;
|
|
503
724
|
}
|
|
504
725
|
|
|
505
|
-
/**
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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];
|
|
509
738
|
}
|
|
510
739
|
|
|
511
740
|
/**
|
|
512
|
-
*
|
|
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.
|
|
513
743
|
*/
|
|
514
|
-
function
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
requireFieldFits(ruleSize, PF.RULE_SIZE_SIZE, "RULE_SIZE_OVERFLOW", "Rule size");
|
|
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
|
+
}
|
|
522
751
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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)];
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** The bound a rule places: whether it bounds a length, and whether from below. */
|
|
782
|
+
type Bound = { isLength: boolean; isLower: boolean };
|
|
533
783
|
|
|
534
|
-
|
|
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;
|
|
535
805
|
}
|
|
536
806
|
|
|
537
|
-
/**
|
|
538
|
-
|
|
539
|
-
|
|
807
|
+
/**
|
|
808
|
+
* Return the bound the rule places, an unnegated constant `GTE`, `LTE`, `LENGTH_GTE` or `LENGTH_LTE`, or undefined.
|
|
809
|
+
*/
|
|
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
|
+
}
|
|
540
826
|
}
|
|
541
827
|
|
|
542
828
|
///////////////////////////////////////////////////////////////////////////
|
|
543
|
-
//
|
|
829
|
+
// Rule bytes
|
|
544
830
|
///////////////////////////////////////////////////////////////////////////
|
|
545
831
|
|
|
546
|
-
/**
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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);
|
|
551
872
|
}
|
|
552
873
|
|
|
553
874
|
/**
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
* @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.
|
|
557
877
|
*/
|
|
558
|
-
function
|
|
559
|
-
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
|
+
}
|
|
560
884
|
|
|
561
|
-
|
|
562
|
-
|
|
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
|
+
}
|
|
563
902
|
}
|
|
564
|
-
|
|
903
|
+
const dataBytes = concatBytes(...data);
|
|
904
|
+
return concatBytes(Uint8Array.of(opCode), be16(dataBytes.length), dataBytes);
|
|
905
|
+
}
|
|
565
906
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const rules = group.flatMap((constraint) => flattenConstraint(constraint, descBytes));
|
|
570
|
-
sortRules(rules);
|
|
571
|
-
return rules;
|
|
572
|
-
});
|
|
907
|
+
///////////////////////////////////////////////////////////////////////////
|
|
908
|
+
// Decoding
|
|
909
|
+
///////////////////////////////////////////////////////////////////////////
|
|
573
910
|
|
|
574
|
-
|
|
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);
|
|
575
920
|
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
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);
|
|
579
934
|
}
|
|
580
|
-
|
|
581
|
-
return { wireBytes: encodeGroupRules(rules), ruleCount: rules.length };
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
// Several groups are ordered by ascending keccak256 over their rule bytes; a lone group is already in canonical
|
|
585
|
-
// position, so it needs no hash.
|
|
586
|
-
if (encodedGroups.length > 1) {
|
|
587
|
-
const hashes = new Map(encodedGroups.map((group) => [group, keccak_256(group.wireBytes)]));
|
|
588
|
-
encodedGroups.sort((a, b) => compareBytes(hashes.get(a)!, hashes.get(b)!));
|
|
935
|
+
groups.push(rules);
|
|
589
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
|
+
}
|
|
590
944
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
"MALFORMED_SELECTOR",
|
|
595
|
-
`Selector must be ${PF.SELECTOR_SIZE} bytes, got ${selectorBytes.length}`,
|
|
596
|
-
);
|
|
597
|
-
}
|
|
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);
|
|
598
948
|
|
|
599
|
-
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
|
+
}
|
|
600
974
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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;
|
|
605
995
|
}
|
|
606
996
|
|
|
607
|
-
const
|
|
608
|
-
|
|
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
|
+
}
|
|
609
1002
|
|
|
610
|
-
|
|
611
|
-
|
|
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
|
+
}
|
|
612
1012
|
|
|
613
|
-
|
|
614
|
-
|
|
1013
|
+
///////////////////////////////////////////////////////////////////////////
|
|
1014
|
+
// Inspection
|
|
1015
|
+
///////////////////////////////////////////////////////////////////////////
|
|
615
1016
|
|
|
616
|
-
|
|
617
|
-
|
|
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);
|
|
618
1027
|
|
|
619
|
-
|
|
620
|
-
|
|
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);
|
|
621
1042
|
|
|
622
|
-
|
|
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
|
+
}
|
|
623
1059
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
+
}
|
|
628
1070
|
|
|
629
|
-
|
|
630
|
-
|
|
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);
|
|
631
1074
|
|
|
632
|
-
|
|
633
|
-
|
|
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);
|
|
634
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
|
+
}
|
|
635
1106
|
|
|
636
|
-
|
|
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
|
+
};
|
|
637
1144
|
}
|
|
638
1145
|
|
|
639
|
-
/**
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
} else {
|
|
661
|
-
const constraint: Constraint = {
|
|
662
|
-
scope: rule.scope.value,
|
|
663
|
-
path: rule.path.value,
|
|
664
|
-
operators: [opHex],
|
|
665
|
-
...(hintHex !== undefined && { hint: hintHex }),
|
|
666
|
-
span: rule.span,
|
|
667
|
-
};
|
|
668
|
-
constraintMap.set(key, constraint);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
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
|
+
}
|
|
671
1167
|
|
|
672
|
-
|
|
673
|
-
|
|
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
|
+
}
|
|
674
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);
|
|
675
1184
|
return {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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 },
|
|
681
1191
|
};
|
|
682
1192
|
}
|
|
683
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
|
+
|
|
684
1286
|
/**
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
* @returns The inspected policy with per-field spans for every structural element.
|
|
688
|
-
* @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.
|
|
689
1289
|
*/
|
|
690
|
-
function
|
|
691
|
-
|
|
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;
|
|
692
1325
|
}
|
|
693
1326
|
|
|
694
|
-
/** Encode, decode
|
|
1327
|
+
/** Encode policies in canonical form, and decode and inspect policy blobs. */
|
|
695
1328
|
export const PolicyCoder = { encode, decode, inspect };
|