@callcium/sdk 0.6.0 → 0.8.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 +77 -2
- package/dist/index.d.ts +114 -43
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +838 -1440
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
- package/src/bytes.ts +100 -0
- package/src/calldata-reader.ts +38 -0
- package/src/constants.ts +395 -0
- package/src/constraint.ts +313 -0
- package/src/descriptor-coder.ts +461 -0
- package/src/descriptor.ts +399 -0
- package/src/errors.ts +138 -0
- package/src/index.ts +105 -0
- package/src/operators.ts +204 -0
- package/src/policy-builder.ts +250 -0
- package/src/policy-coder.ts +668 -0
- package/src/policy-enforcer.ts +600 -0
- package/src/policy-validator.ts +1112 -0
- package/src/signature.ts +97 -0
- package/src/type-desc.ts +205 -0
- package/src/types.ts +280 -0
- package/src/validation-issue.ts +569 -0
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
import { bigintToHex, bytesToHex, hexToBytes } from "./bytes";
|
|
2
|
+
import {
|
|
3
|
+
classifyTypeCode,
|
|
4
|
+
isQuantifier,
|
|
5
|
+
isValidOperatorData,
|
|
6
|
+
Limits,
|
|
7
|
+
lookupContextProperty,
|
|
8
|
+
MAX_CONTEXT_PROPERTY_ID,
|
|
9
|
+
Op,
|
|
10
|
+
Quantifier,
|
|
11
|
+
Scope,
|
|
12
|
+
TypeCode,
|
|
13
|
+
} from "./constants";
|
|
14
|
+
import { Descriptor, type TypeInfo } from "./descriptor";
|
|
15
|
+
import { decodeDescriptor } from "./descriptor-coder";
|
|
16
|
+
import { CallciumError, type CallciumErrorCode } from "./errors";
|
|
17
|
+
import { canonicalize, isLeftAligned, isSigned, isLengthOp, isLengthValidType } from "./operators";
|
|
18
|
+
import { parsePathSteps } from "./policy-coder";
|
|
19
|
+
import * as ValidationIssue from "./validation-issue";
|
|
20
|
+
|
|
21
|
+
import type { Constraint, Hex, Issue, PolicyData } from "./types";
|
|
22
|
+
|
|
23
|
+
///////////////////////////////////////////////////////////////////////////
|
|
24
|
+
// Internal types
|
|
25
|
+
///////////////////////////////////////////////////////////////////////////
|
|
26
|
+
|
|
27
|
+
/** Codes a path walk raises when the path does not navigate the descriptor (PV-1). */
|
|
28
|
+
const NAVIGABILITY_CODES: ReadonlySet<CallciumErrorCode> = new Set<CallciumErrorCode>([
|
|
29
|
+
"EMPTY_PATH",
|
|
30
|
+
"PARAM_INDEX_OUT_OF_BOUNDS",
|
|
31
|
+
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
32
|
+
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
33
|
+
"NOT_COMPOSITE",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
type BoundDomain = {
|
|
37
|
+
isSigned: boolean;
|
|
38
|
+
min: bigint;
|
|
39
|
+
max: bigint;
|
|
40
|
+
hasEq: boolean;
|
|
41
|
+
eq: bigint;
|
|
42
|
+
hasLower: boolean;
|
|
43
|
+
lower: bigint;
|
|
44
|
+
lowerInclusive: boolean;
|
|
45
|
+
hasUpper: boolean;
|
|
46
|
+
upper: bigint;
|
|
47
|
+
upperInclusive: boolean;
|
|
48
|
+
holes: bigint[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type BitmaskDomain = {
|
|
52
|
+
mustBeOne: bigint;
|
|
53
|
+
mustBeZero: bigint;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type SetDomain = {
|
|
57
|
+
hasIn: boolean;
|
|
58
|
+
inValues: bigint[];
|
|
59
|
+
notInValues: bigint[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type ConstraintContext = {
|
|
63
|
+
scope: number;
|
|
64
|
+
path: Hex;
|
|
65
|
+
/** Path steps of every constraint sharing this context. */
|
|
66
|
+
steps: number[];
|
|
67
|
+
typeInfo: TypeInfo;
|
|
68
|
+
numeric: BoundDomain;
|
|
69
|
+
bitmask: BitmaskDomain;
|
|
70
|
+
length: BoundDomain;
|
|
71
|
+
set: SetDomain;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
///////////////////////////////////////////////////////////////////////////
|
|
75
|
+
// Constants
|
|
76
|
+
///////////////////////////////////////////////////////////////////////////
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Maximum length value for dynamic types.
|
|
80
|
+
*/
|
|
81
|
+
const LENGTH_MAX = 0xffffffffn;
|
|
82
|
+
|
|
83
|
+
// Mask for reinterpreting uint256 as int256 (two's complement).
|
|
84
|
+
const UINT256_MAX = (1n << 256n) - 1n;
|
|
85
|
+
const INT256_SIGN_BIT = 1n << 255n;
|
|
86
|
+
|
|
87
|
+
///////////////////////////////////////////////////////////////////////////
|
|
88
|
+
// Signed-aware comparisons
|
|
89
|
+
///////////////////////////////////////////////////////////////////////////
|
|
90
|
+
|
|
91
|
+
/** Signed-aware difference of two raw uint256 values: negative if a<b, zero if equal, positive if a>b. */
|
|
92
|
+
function signedCompare(a: bigint, b: bigint, signed: boolean): bigint {
|
|
93
|
+
if (!signed) return a - b;
|
|
94
|
+
const toS = (v: bigint) => (v >= INT256_SIGN_BIT ? v - (1n << 256n) : v);
|
|
95
|
+
return toS(a) - toS(b);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
///////////////////////////////////////////////////////////////////////////
|
|
99
|
+
// Type classification
|
|
100
|
+
///////////////////////////////////////////////////////////////////////////
|
|
101
|
+
|
|
102
|
+
/** True for uint and int type codes. */
|
|
103
|
+
function isNumericType(typeCode: number): boolean {
|
|
104
|
+
return (
|
|
105
|
+
(typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) ||
|
|
106
|
+
(typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** True for uint and bytes32 type codes. */
|
|
111
|
+
function isBitmaskCompatible(typeCode: number): boolean {
|
|
112
|
+
return (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) || typeCode === TypeCode.FIXED_BYTES_MIN + 31;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Physical domain limits for a type code (returned as raw uint256 bigints). */
|
|
116
|
+
function getDomainLimits(typeCode: number): { min: bigint; max: bigint } {
|
|
117
|
+
if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
|
|
118
|
+
const bits = BigInt(typeCode - TypeCode.UINT_MIN + 1) * 8n;
|
|
119
|
+
return { min: 0n, max: bits === 256n ? UINT256_MAX : (1n << bits) - 1n };
|
|
120
|
+
}
|
|
121
|
+
if (typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX) {
|
|
122
|
+
const bits = BigInt(typeCode - TypeCode.INT_MIN + 1) * 8n;
|
|
123
|
+
if (bits === 256n) {
|
|
124
|
+
// int256: min = uint256(type(int256).min), max = uint256(type(int256).max).
|
|
125
|
+
return { min: INT256_SIGN_BIT, max: INT256_SIGN_BIT - 1n };
|
|
126
|
+
}
|
|
127
|
+
const half = 1n << (bits - 1n);
|
|
128
|
+
// Stored as raw uint256: min is two's complement of -(2^(bits-1)).
|
|
129
|
+
const max = half - 1n;
|
|
130
|
+
const min = (UINT256_MAX - half + 1n) & UINT256_MAX;
|
|
131
|
+
return { min, max };
|
|
132
|
+
}
|
|
133
|
+
if (typeCode === TypeCode.BOOL) return { min: 0n, max: 1n };
|
|
134
|
+
if (typeCode === TypeCode.ADDRESS) return { min: 0n, max: (1n << 160n) - 1n };
|
|
135
|
+
return { min: 0n, max: UINT256_MAX };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
///////////////////////////////////////////////////////////////////////////
|
|
139
|
+
// Operator classification
|
|
140
|
+
///////////////////////////////////////////////////////////////////////////
|
|
141
|
+
|
|
142
|
+
/** True for EQ, GT, LT, GTE, LTE, BETWEEN, IN, BITMASK_*. */
|
|
143
|
+
function isValueOp(opBase: number): boolean {
|
|
144
|
+
return opBase <= Op.BITMASK_NONE;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** True for GT, LT, GTE, LTE, BETWEEN. */
|
|
148
|
+
function isComparisonOp(opBase: number): boolean {
|
|
149
|
+
return opBase >= Op.GT && opBase <= Op.BETWEEN;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** True for BITMASK_ALL, BITMASK_ANY, BITMASK_NONE. */
|
|
153
|
+
function isBitmaskOp(opBase: number): boolean {
|
|
154
|
+
return opBase >= Op.BITMASK_ALL && opBase <= Op.BITMASK_NONE;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
///////////////////////////////////////////////////////////////////////////
|
|
158
|
+
// Type compatibility check
|
|
159
|
+
///////////////////////////////////////////////////////////////////////////
|
|
160
|
+
|
|
161
|
+
/** Return the incompatibility reason, or null if the operator is allowed. */
|
|
162
|
+
function getIncompat(opBase: number, typeInfo: TypeInfo): { code: string; message: string } | null {
|
|
163
|
+
const { typeCode, isDynamic, staticSize } = typeInfo;
|
|
164
|
+
if (isValueOp(opBase)) {
|
|
165
|
+
if (isDynamic || staticSize !== 32) {
|
|
166
|
+
return { code: "VALUE_OP_ON_DYNAMIC", message: "Value operator used on dynamic type" };
|
|
167
|
+
}
|
|
168
|
+
if (classifyTypeCode(typeCode).typeClass !== "elementary") {
|
|
169
|
+
return { code: "VALUE_OP_ON_COMPOSITE", message: "Value operator used on composite type" };
|
|
170
|
+
}
|
|
171
|
+
if (isComparisonOp(opBase) && !isNumericType(typeCode)) {
|
|
172
|
+
return { code: "NUMERIC_OP_ON_NON_NUMERIC", message: "Comparison operator used on non-numeric type" };
|
|
173
|
+
}
|
|
174
|
+
if (isBitmaskOp(opBase) && !isBitmaskCompatible(typeCode)) {
|
|
175
|
+
return { code: "BITMASK_ON_INVALID", message: "Bitmask operator used on incompatible type" };
|
|
176
|
+
}
|
|
177
|
+
// IN on bool is degenerate: the two-value domain reduces every set to an equality, a tautology, or dead members.
|
|
178
|
+
if (opBase === Op.IN && typeCode === TypeCode.BOOL) {
|
|
179
|
+
return { code: "IN_ON_BOOL", message: "IN operator used on boolean type" };
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
if (isLengthOp(opBase)) {
|
|
184
|
+
if (!isLengthValidType(typeCode)) {
|
|
185
|
+
return { code: "LENGTH_ON_STATIC", message: "Length operator used on non-dynamic type" };
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return { code: "UNKNOWN_OPERATOR", message: "Unknown operator code" };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* True if operator `opCode` is allowed on a target with the given type info.
|
|
194
|
+
* Tolerates the NOT flag on `opCode`.
|
|
195
|
+
*/
|
|
196
|
+
export function isOpAllowed(opCode: number, typeInfo: TypeInfo): boolean {
|
|
197
|
+
return getIncompat(opCode & ~Op.NOT, typeInfo) === null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
///////////////////////////////////////////////////////////////////////////
|
|
201
|
+
// Payload reading
|
|
202
|
+
///////////////////////////////////////////////////////////////////////////
|
|
203
|
+
|
|
204
|
+
/** Read a single uint256 from operator hex (bytes after the opcode byte). */
|
|
205
|
+
function readValue(opHex: Hex): bigint {
|
|
206
|
+
return BigInt(`0x${opHex.slice(4)}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** First payload word deviating from the canonical encoding for the type, with its canonical form, or null. */
|
|
210
|
+
function findNonCanonicalWord(opHex: Hex, typeCode: number): { word: bigint; canonical: bigint } | null {
|
|
211
|
+
const body = opHex.slice(4);
|
|
212
|
+
for (let i = 0; i + 64 <= body.length; i += 64) {
|
|
213
|
+
const word = BigInt(`0x${body.slice(i, i + 64)}`);
|
|
214
|
+
const canonical = canonicalize(word, typeCode);
|
|
215
|
+
if (canonical !== word) return { word, canonical };
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Read a pair of uint256 values from a BETWEEN operator. */
|
|
221
|
+
function readPair(opHex: Hex): { low: bigint; high: bigint } {
|
|
222
|
+
const body = opHex.slice(4);
|
|
223
|
+
return {
|
|
224
|
+
low: BigInt(`0x${body.slice(0, 64)}`),
|
|
225
|
+
high: BigInt(`0x${body.slice(64, 128)}`),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Unpack an IN operator's payload into an array of bigint values. */
|
|
230
|
+
function unpackSet(opHex: Hex): bigint[] {
|
|
231
|
+
const body = opHex.slice(4);
|
|
232
|
+
const count = body.length / 64;
|
|
233
|
+
const values: bigint[] = [];
|
|
234
|
+
for (let i = 0; i < count; i++) {
|
|
235
|
+
values.push(BigInt(`0x${body.slice(i * 64, (i + 1) * 64)}`));
|
|
236
|
+
}
|
|
237
|
+
return values;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** True if every element is strictly greater than the previous. */
|
|
241
|
+
function isStrictlyAscending(values: bigint[]): boolean {
|
|
242
|
+
for (let i = 1; i < values.length; i++) {
|
|
243
|
+
if (values[i]! <= values[i - 1]!) return false;
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
///////////////////////////////////////////////////////////////////////////
|
|
249
|
+
// Opcode normalization
|
|
250
|
+
///////////////////////////////////////////////////////////////////////////
|
|
251
|
+
|
|
252
|
+
/** Convert LENGTH_* opcode to its core comparison equivalent. */
|
|
253
|
+
function normalizeLengthOp(base: number): number {
|
|
254
|
+
if (base === Op.LENGTH_EQ) return Op.EQ;
|
|
255
|
+
if (base === Op.LENGTH_GT) return Op.GT;
|
|
256
|
+
if (base === Op.LENGTH_LT) return Op.LT;
|
|
257
|
+
if (base === Op.LENGTH_GTE) return Op.GTE;
|
|
258
|
+
if (base === Op.LENGTH_LTE) return Op.LTE;
|
|
259
|
+
return base;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Convert a negated bound opcode to its positive equivalent. */
|
|
263
|
+
function negateBoundOp(base: number): number {
|
|
264
|
+
if (base === Op.GT) return Op.LTE;
|
|
265
|
+
if (base === Op.GTE) return Op.LT;
|
|
266
|
+
if (base === Op.LT) return Op.GTE;
|
|
267
|
+
if (base === Op.LTE) return Op.GT;
|
|
268
|
+
return base;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
///////////////////////////////////////////////////////////////////////////
|
|
272
|
+
// Context initialization
|
|
273
|
+
///////////////////////////////////////////////////////////////////////////
|
|
274
|
+
|
|
275
|
+
/** Fresh bound domain with no constraints recorded yet. */
|
|
276
|
+
function emptyBoundDomain(boundIsSigned: boolean, min: bigint, max: bigint): BoundDomain {
|
|
277
|
+
return {
|
|
278
|
+
isSigned: boundIsSigned,
|
|
279
|
+
min,
|
|
280
|
+
max,
|
|
281
|
+
hasEq: false,
|
|
282
|
+
eq: 0n,
|
|
283
|
+
hasLower: false,
|
|
284
|
+
lower: 0n,
|
|
285
|
+
lowerInclusive: false,
|
|
286
|
+
hasUpper: false,
|
|
287
|
+
upper: 0n,
|
|
288
|
+
upperInclusive: false,
|
|
289
|
+
holes: [],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Create a fresh constraint context with domain limits from the type. */
|
|
294
|
+
function initContext(scope: number, path: Hex, steps: number[], typeInfo: TypeInfo): ConstraintContext {
|
|
295
|
+
const { min, max } = getDomainLimits(typeInfo.typeCode);
|
|
296
|
+
return {
|
|
297
|
+
scope,
|
|
298
|
+
path,
|
|
299
|
+
steps,
|
|
300
|
+
typeInfo,
|
|
301
|
+
numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
|
|
302
|
+
bitmask: { mustBeOne: 0n, mustBeZero: 0n },
|
|
303
|
+
length: emptyBoundDomain(false, 0n, LENGTH_MAX),
|
|
304
|
+
set: {
|
|
305
|
+
hasIn: false,
|
|
306
|
+
inValues: [],
|
|
307
|
+
notInValues: [],
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
///////////////////////////////////////////////////////////////////////////
|
|
313
|
+
// Bound domain update
|
|
314
|
+
///////////////////////////////////////////////////////////////////////////
|
|
315
|
+
|
|
316
|
+
/** Narrow the numeric bound domain with an operator and emit issues on contradiction or redundancy. */
|
|
317
|
+
function updateBound(
|
|
318
|
+
domain: BoundDomain,
|
|
319
|
+
base: number,
|
|
320
|
+
isNegated: boolean,
|
|
321
|
+
value: bigint,
|
|
322
|
+
isLength: boolean,
|
|
323
|
+
groupIndex: number,
|
|
324
|
+
constraintIndex: number,
|
|
325
|
+
issues: Issue[],
|
|
326
|
+
): void {
|
|
327
|
+
if (isNegated) {
|
|
328
|
+
if (base === Op.EQ) {
|
|
329
|
+
if (domain.hasEq && domain.eq === value) {
|
|
330
|
+
issues.push(ValidationIssue.eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
331
|
+
}
|
|
332
|
+
let alreadyHole = false;
|
|
333
|
+
for (let j = 0; j < domain.holes.length; j++) {
|
|
334
|
+
if (domain.holes[j] === value) {
|
|
335
|
+
alreadyHole = true;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (!alreadyHole) {
|
|
340
|
+
domain.holes.push(value);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
updateBound(domain, negateBoundOp(base), false, value, isLength, groupIndex, constraintIndex, issues);
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Vacuity checks.
|
|
349
|
+
if (base === Op.GTE && value === domain.min) {
|
|
350
|
+
issues.push(ValidationIssue.vacuousGte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
351
|
+
} else if (base === Op.LTE && value === domain.max) {
|
|
352
|
+
issues.push(ValidationIssue.vacuousLte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Physical bounds and impossibility.
|
|
356
|
+
if (
|
|
357
|
+
signedCompare(value, domain.min, domain.isSigned) < 0n ||
|
|
358
|
+
signedCompare(value, domain.max, domain.isSigned) > 0n
|
|
359
|
+
) {
|
|
360
|
+
issues.push(ValidationIssue.outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
361
|
+
} else if (base === Op.GT && value === domain.max) {
|
|
362
|
+
issues.push(ValidationIssue.impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
363
|
+
} else if (base === Op.LT && value === domain.min) {
|
|
364
|
+
issues.push(ValidationIssue.impossibleLt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let changedEq = false;
|
|
368
|
+
let changedLower = false;
|
|
369
|
+
let changedUpper = false;
|
|
370
|
+
|
|
371
|
+
// Equality handling.
|
|
372
|
+
if (base === Op.EQ) {
|
|
373
|
+
if (!domain.hasEq || domain.eq !== value) changedEq = true;
|
|
374
|
+
if (domain.hasEq) {
|
|
375
|
+
if (domain.eq !== value) {
|
|
376
|
+
issues.push(
|
|
377
|
+
ValidationIssue.conflictingEquality(
|
|
378
|
+
isLength,
|
|
379
|
+
groupIndex,
|
|
380
|
+
constraintIndex,
|
|
381
|
+
bigintToHex(domain.eq),
|
|
382
|
+
bigintToHex(value),
|
|
383
|
+
),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (let j = 0; j < domain.holes.length; j++) {
|
|
388
|
+
if (domain.holes[j] === value) {
|
|
389
|
+
issues.push(ValidationIssue.eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
domain.hasEq = true;
|
|
393
|
+
domain.eq = value;
|
|
394
|
+
} else if (base === Op.GT || base === Op.GTE) {
|
|
395
|
+
const inclusive = base === Op.GTE;
|
|
396
|
+
if (domain.hasLower) {
|
|
397
|
+
let redundant = false;
|
|
398
|
+
let strictlyBetter = false;
|
|
399
|
+
|
|
400
|
+
if (signedCompare(value, domain.lower, domain.isSigned) < 0n) {
|
|
401
|
+
redundant = true;
|
|
402
|
+
} else if (value === domain.lower) {
|
|
403
|
+
if (domain.lowerInclusive) {
|
|
404
|
+
if (!inclusive) strictlyBetter = true;
|
|
405
|
+
else redundant = true;
|
|
406
|
+
} else {
|
|
407
|
+
if (!inclusive) redundant = true;
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
strictlyBetter = true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (redundant) {
|
|
414
|
+
issues.push(ValidationIssue.dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (strictlyBetter) {
|
|
418
|
+
issues.push(ValidationIssue.dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.lower)));
|
|
419
|
+
domain.lower = value;
|
|
420
|
+
domain.lowerInclusive = inclusive;
|
|
421
|
+
changedLower = true;
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
domain.hasLower = true;
|
|
425
|
+
domain.lower = value;
|
|
426
|
+
domain.lowerInclusive = inclusive;
|
|
427
|
+
changedLower = true;
|
|
428
|
+
}
|
|
429
|
+
} else if (base === Op.LT || base === Op.LTE) {
|
|
430
|
+
const inclusive = base === Op.LTE;
|
|
431
|
+
if (domain.hasUpper) {
|
|
432
|
+
let redundant = false;
|
|
433
|
+
let strictlyBetter = false;
|
|
434
|
+
|
|
435
|
+
if (signedCompare(value, domain.upper, domain.isSigned) > 0n) {
|
|
436
|
+
redundant = true;
|
|
437
|
+
} else if (value === domain.upper) {
|
|
438
|
+
if (domain.upperInclusive) {
|
|
439
|
+
if (!inclusive) strictlyBetter = true;
|
|
440
|
+
else redundant = true;
|
|
441
|
+
} else {
|
|
442
|
+
if (!inclusive) redundant = true;
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
strictlyBetter = true;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (redundant) {
|
|
449
|
+
issues.push(ValidationIssue.dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (strictlyBetter) {
|
|
453
|
+
issues.push(ValidationIssue.dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.upper)));
|
|
454
|
+
domain.upper = value;
|
|
455
|
+
domain.upperInclusive = inclusive;
|
|
456
|
+
changedUpper = true;
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
domain.hasUpper = true;
|
|
460
|
+
domain.upper = value;
|
|
461
|
+
domain.upperInclusive = inclusive;
|
|
462
|
+
changedUpper = true;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Cross-checks: equality vs lower bound.
|
|
467
|
+
if (changedEq || changedLower) {
|
|
468
|
+
if (domain.hasEq && domain.hasLower) {
|
|
469
|
+
const contradiction = domain.lowerInclusive
|
|
470
|
+
? signedCompare(domain.eq, domain.lower, domain.isSigned) < 0n
|
|
471
|
+
: signedCompare(domain.eq, domain.lower, domain.isSigned) <= 0n;
|
|
472
|
+
if (contradiction) {
|
|
473
|
+
issues.push(
|
|
474
|
+
ValidationIssue.boundsExcludeEquality(
|
|
475
|
+
isLength,
|
|
476
|
+
groupIndex,
|
|
477
|
+
constraintIndex,
|
|
478
|
+
bigintToHex(domain.eq),
|
|
479
|
+
bigintToHex(domain.lower),
|
|
480
|
+
),
|
|
481
|
+
);
|
|
482
|
+
} else {
|
|
483
|
+
issues.push(
|
|
484
|
+
ValidationIssue.redundantBound(
|
|
485
|
+
isLength,
|
|
486
|
+
groupIndex,
|
|
487
|
+
constraintIndex,
|
|
488
|
+
bigintToHex(domain.lower),
|
|
489
|
+
bigintToHex(domain.eq),
|
|
490
|
+
),
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Cross-checks: equality vs upper bound.
|
|
497
|
+
if (changedEq || changedUpper) {
|
|
498
|
+
if (domain.hasEq && domain.hasUpper) {
|
|
499
|
+
const contradiction = domain.upperInclusive
|
|
500
|
+
? signedCompare(domain.eq, domain.upper, domain.isSigned) > 0n
|
|
501
|
+
: signedCompare(domain.eq, domain.upper, domain.isSigned) >= 0n;
|
|
502
|
+
if (contradiction) {
|
|
503
|
+
issues.push(
|
|
504
|
+
ValidationIssue.boundsExcludeEquality(
|
|
505
|
+
isLength,
|
|
506
|
+
groupIndex,
|
|
507
|
+
constraintIndex,
|
|
508
|
+
bigintToHex(domain.eq),
|
|
509
|
+
bigintToHex(domain.upper),
|
|
510
|
+
),
|
|
511
|
+
);
|
|
512
|
+
} else {
|
|
513
|
+
issues.push(
|
|
514
|
+
ValidationIssue.redundantBound(
|
|
515
|
+
isLength,
|
|
516
|
+
groupIndex,
|
|
517
|
+
constraintIndex,
|
|
518
|
+
bigintToHex(domain.upper),
|
|
519
|
+
bigintToHex(domain.eq),
|
|
520
|
+
),
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Cross-check: lower vs upper bound.
|
|
527
|
+
if (changedLower || changedUpper) {
|
|
528
|
+
if (domain.hasLower && domain.hasUpper) {
|
|
529
|
+
const impossible =
|
|
530
|
+
signedCompare(domain.lower, domain.upper, domain.isSigned) > 0n ||
|
|
531
|
+
(domain.lower === domain.upper && (!domain.lowerInclusive || !domain.upperInclusive));
|
|
532
|
+
if (impossible) {
|
|
533
|
+
issues.push(
|
|
534
|
+
ValidationIssue.impossibleRange(
|
|
535
|
+
isLength,
|
|
536
|
+
groupIndex,
|
|
537
|
+
constraintIndex,
|
|
538
|
+
bigintToHex(domain.lower),
|
|
539
|
+
bigintToHex(domain.upper),
|
|
540
|
+
),
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
///////////////////////////////////////////////////////////////////////////
|
|
548
|
+
// Bitmask domain update
|
|
549
|
+
///////////////////////////////////////////////////////////////////////////
|
|
550
|
+
|
|
551
|
+
/** Update the bitmask domain and emit issues on contradiction or redundancy. */
|
|
552
|
+
function updateBitmask(
|
|
553
|
+
ctx: ConstraintContext,
|
|
554
|
+
base: number,
|
|
555
|
+
isNegated: boolean,
|
|
556
|
+
mask: bigint,
|
|
557
|
+
groupIndex: number,
|
|
558
|
+
constraintIndex: number,
|
|
559
|
+
issues: Issue[],
|
|
560
|
+
): void {
|
|
561
|
+
if (isNegated) return;
|
|
562
|
+
|
|
563
|
+
if (base === Op.BITMASK_ALL) {
|
|
564
|
+
if ((ctx.bitmask.mustBeZero & mask) !== 0n) {
|
|
565
|
+
issues.push(
|
|
566
|
+
ValidationIssue.bitmaskContradiction(
|
|
567
|
+
groupIndex,
|
|
568
|
+
constraintIndex,
|
|
569
|
+
bigintToHex(mask),
|
|
570
|
+
bigintToHex(ctx.bitmask.mustBeZero),
|
|
571
|
+
),
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if ((ctx.bitmask.mustBeOne & mask) === mask) {
|
|
575
|
+
issues.push(
|
|
576
|
+
ValidationIssue.redundantBitmask(
|
|
577
|
+
groupIndex,
|
|
578
|
+
constraintIndex,
|
|
579
|
+
bigintToHex(mask),
|
|
580
|
+
bigintToHex(ctx.bitmask.mustBeOne),
|
|
581
|
+
),
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
ctx.bitmask.mustBeOne |= mask;
|
|
585
|
+
} else if (base === Op.BITMASK_NONE) {
|
|
586
|
+
if ((ctx.bitmask.mustBeOne & mask) !== 0n) {
|
|
587
|
+
issues.push(
|
|
588
|
+
ValidationIssue.bitmaskContradiction(
|
|
589
|
+
groupIndex,
|
|
590
|
+
constraintIndex,
|
|
591
|
+
bigintToHex(mask),
|
|
592
|
+
bigintToHex(ctx.bitmask.mustBeOne),
|
|
593
|
+
),
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
if ((ctx.bitmask.mustBeZero & mask) === mask) {
|
|
597
|
+
issues.push(
|
|
598
|
+
ValidationIssue.redundantBitmask(
|
|
599
|
+
groupIndex,
|
|
600
|
+
constraintIndex,
|
|
601
|
+
bigintToHex(mask),
|
|
602
|
+
bigintToHex(ctx.bitmask.mustBeZero),
|
|
603
|
+
),
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
ctx.bitmask.mustBeZero |= mask;
|
|
607
|
+
} else if (base === Op.BITMASK_ANY) {
|
|
608
|
+
if (mask !== 0n && (ctx.bitmask.mustBeZero & mask) === mask) {
|
|
609
|
+
issues.push(
|
|
610
|
+
ValidationIssue.bitmaskAnyImpossible(
|
|
611
|
+
groupIndex,
|
|
612
|
+
constraintIndex,
|
|
613
|
+
bigintToHex(mask),
|
|
614
|
+
bigintToHex(ctx.bitmask.mustBeZero),
|
|
615
|
+
),
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
if ((ctx.bitmask.mustBeOne & mask) !== 0n) {
|
|
619
|
+
issues.push(
|
|
620
|
+
ValidationIssue.redundantBitmask(
|
|
621
|
+
groupIndex,
|
|
622
|
+
constraintIndex,
|
|
623
|
+
bigintToHex(mask),
|
|
624
|
+
bigintToHex(ctx.bitmask.mustBeOne),
|
|
625
|
+
),
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
///////////////////////////////////////////////////////////////////////////
|
|
632
|
+
// Set domain update
|
|
633
|
+
///////////////////////////////////////////////////////////////////////////
|
|
634
|
+
|
|
635
|
+
/** Emit an issue if the IN set has been fully excluded by NOT_IN or NEQ holes. */
|
|
636
|
+
function checkSetEmpty(ctx: ConstraintContext, groupIndex: number, constraintIndex: number, issues: Issue[]): void {
|
|
637
|
+
if (!ctx.set.hasIn) return;
|
|
638
|
+
|
|
639
|
+
let possibleCount = 0;
|
|
640
|
+
const inCount = ctx.set.inValues.length;
|
|
641
|
+
for (let i = 0; i < inCount; i++) {
|
|
642
|
+
const value = ctx.set.inValues[i]!;
|
|
643
|
+
let forbidden = false;
|
|
644
|
+
for (let k = 0; k < ctx.numeric.holes.length; k++) {
|
|
645
|
+
if (ctx.numeric.holes[k] === value) {
|
|
646
|
+
forbidden = true;
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (!forbidden) {
|
|
651
|
+
for (let k = 0; k < ctx.set.notInValues.length; k++) {
|
|
652
|
+
if (ctx.set.notInValues[k] === value) {
|
|
653
|
+
forbidden = true;
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (!forbidden) possibleCount++;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (possibleCount === 0) {
|
|
662
|
+
issues.push(ValidationIssue.setFullyExcluded(groupIndex, constraintIndex));
|
|
663
|
+
} else if (possibleCount < inCount) {
|
|
664
|
+
issues.push(
|
|
665
|
+
ValidationIssue.setPartiallyExcluded(groupIndex, constraintIndex, bigintToHex(BigInt(inCount - possibleCount))),
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Update the set domain with IN or NOT_IN values and emit issues on contradiction or redundancy. */
|
|
671
|
+
function updateSet(
|
|
672
|
+
ctx: ConstraintContext,
|
|
673
|
+
isNegated: boolean,
|
|
674
|
+
values: bigint[],
|
|
675
|
+
groupIndex: number,
|
|
676
|
+
constraintIndex: number,
|
|
677
|
+
issues: Issue[],
|
|
678
|
+
): void {
|
|
679
|
+
if (isNegated) {
|
|
680
|
+
for (const value of values) {
|
|
681
|
+
if (ctx.numeric.hasEq && ctx.numeric.eq === value) {
|
|
682
|
+
issues.push(ValidationIssue.setExcludesEquality(groupIndex, constraintIndex, bigintToHex(value)));
|
|
683
|
+
}
|
|
684
|
+
if (ctx.set.hasIn) {
|
|
685
|
+
let inSet = false;
|
|
686
|
+
for (const existing of ctx.set.inValues) {
|
|
687
|
+
if (existing === value) {
|
|
688
|
+
inSet = true;
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (inSet) {
|
|
693
|
+
issues.push(ValidationIssue.setReduction(groupIndex, constraintIndex, bigintToHex(value)));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
ctx.set.notInValues.push(value);
|
|
697
|
+
}
|
|
698
|
+
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
699
|
+
} else {
|
|
700
|
+
// Physical bounds: a member outside the type's domain can never be matched.
|
|
701
|
+
for (const value of values) {
|
|
702
|
+
if (
|
|
703
|
+
signedCompare(value, ctx.numeric.min, ctx.numeric.isSigned) < 0n ||
|
|
704
|
+
signedCompare(value, ctx.numeric.max, ctx.numeric.isSigned) > 0n
|
|
705
|
+
) {
|
|
706
|
+
issues.push(ValidationIssue.outOfPhysicalBounds(false, groupIndex, constraintIndex, bigintToHex(value)));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (ctx.numeric.hasEq) {
|
|
711
|
+
let found = false;
|
|
712
|
+
for (const candidate of values) {
|
|
713
|
+
if (candidate === ctx.numeric.eq) {
|
|
714
|
+
found = true;
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (!found) {
|
|
719
|
+
issues.push(ValidationIssue.setExcludesEquality(groupIndex, constraintIndex, bigintToHex(ctx.numeric.eq)));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (ctx.set.hasIn) {
|
|
724
|
+
const intersection: bigint[] = [];
|
|
725
|
+
for (const existing of ctx.set.inValues) {
|
|
726
|
+
for (const candidate of values) {
|
|
727
|
+
if (existing === candidate) {
|
|
728
|
+
intersection.push(existing);
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (intersection.length === 0) {
|
|
735
|
+
issues.push(ValidationIssue.emptySetIntersection(groupIndex, constraintIndex));
|
|
736
|
+
} else if (intersection.length < values.length || intersection.length < ctx.set.inValues.length) {
|
|
737
|
+
issues.push(
|
|
738
|
+
ValidationIssue.setRedundancy(groupIndex, constraintIndex, bigintToHex(BigInt(intersection.length))),
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
ctx.set.inValues = intersection;
|
|
742
|
+
} else {
|
|
743
|
+
ctx.set.hasIn = true;
|
|
744
|
+
ctx.set.inValues = values;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
///////////////////////////////////////////////////////////////////////////
|
|
752
|
+
// Duplicate detection
|
|
753
|
+
///////////////////////////////////////////////////////////////////////////
|
|
754
|
+
|
|
755
|
+
/** Emit a warning if the constraint contains any identical operators. */
|
|
756
|
+
function checkDuplicates(issues: Issue[], operators: Hex[], groupIndex: number, constraintIndex: number): void {
|
|
757
|
+
const seen = new Set<string>();
|
|
758
|
+
for (const op of operators) {
|
|
759
|
+
const key = op.toLowerCase();
|
|
760
|
+
if (seen.has(key)) {
|
|
761
|
+
issues.push(ValidationIssue.duplicateConstraint(groupIndex, constraintIndex));
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
seen.add(key);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
///////////////////////////////////////////////////////////////////////////
|
|
769
|
+
// Fusible range detection
|
|
770
|
+
///////////////////////////////////////////////////////////////////////////
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Report a lone unnegated gte/lte pair (value or length domain) as fusible into the
|
|
774
|
+
* corresponding single range operator. Fires only for a satisfiable pair on a compatible
|
|
775
|
+
* type; contradictions and dominated bounds are already reported by the domain checks.
|
|
776
|
+
*/
|
|
777
|
+
function checkFusibleRange(
|
|
778
|
+
ctx: ConstraintContext,
|
|
779
|
+
operators: Hex[],
|
|
780
|
+
groupIndex: number,
|
|
781
|
+
constraintIndex: number,
|
|
782
|
+
issues: Issue[],
|
|
783
|
+
): void {
|
|
784
|
+
const numericPair = findLonePair(operators, Op.GTE, Op.LTE);
|
|
785
|
+
if (
|
|
786
|
+
numericPair &&
|
|
787
|
+
getIncompat(Op.GTE, ctx.typeInfo) === null &&
|
|
788
|
+
signedCompare(numericPair.low, numericPair.high, ctx.numeric.isSigned) <= 0n
|
|
789
|
+
) {
|
|
790
|
+
issues.push(
|
|
791
|
+
ValidationIssue.fusibleRange(
|
|
792
|
+
false,
|
|
793
|
+
groupIndex,
|
|
794
|
+
constraintIndex,
|
|
795
|
+
bigintToHex(numericPair.low),
|
|
796
|
+
bigintToHex(numericPair.high),
|
|
797
|
+
),
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const lengthPair = findLonePair(operators, Op.LENGTH_GTE, Op.LENGTH_LTE);
|
|
802
|
+
if (lengthPair && getIncompat(Op.LENGTH_GTE, ctx.typeInfo) === null && lengthPair.low <= lengthPair.high) {
|
|
803
|
+
issues.push(
|
|
804
|
+
ValidationIssue.fusibleRange(
|
|
805
|
+
true,
|
|
806
|
+
groupIndex,
|
|
807
|
+
constraintIndex,
|
|
808
|
+
bigintToHex(lengthPair.low),
|
|
809
|
+
bigintToHex(lengthPair.high),
|
|
810
|
+
),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** Return the operand pair when the operators contain exactly one of each unnegated bound opcode. */
|
|
816
|
+
function findLonePair(operators: Hex[], lowerOp: number, upperOp: number): { low: bigint; high: bigint } | null {
|
|
817
|
+
let lowerCount = 0;
|
|
818
|
+
let upperCount = 0;
|
|
819
|
+
let low = 0n;
|
|
820
|
+
let high = 0n;
|
|
821
|
+
|
|
822
|
+
for (const opHex of operators) {
|
|
823
|
+
const opCode = parseInt(opHex.slice(2, 4), 16);
|
|
824
|
+
const dataLength = (opHex.length - 4) / 2;
|
|
825
|
+
if (dataLength > 0xffff || !isValidOperatorData(opCode & ~Op.NOT, dataLength)) continue;
|
|
826
|
+
if (opCode === lowerOp) {
|
|
827
|
+
lowerCount++;
|
|
828
|
+
low = readValue(opHex);
|
|
829
|
+
} else if (opCode === upperOp) {
|
|
830
|
+
upperCount++;
|
|
831
|
+
high = readValue(opHex);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return lowerCount === 1 && upperCount === 1 ? { low, high } : null;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
///////////////////////////////////////////////////////////////////////////
|
|
839
|
+
// Constraint validation
|
|
840
|
+
///////////////////////////////////////////////////////////////////////////
|
|
841
|
+
|
|
842
|
+
/** Validate all operators on a single constraint against its domain context. */
|
|
843
|
+
function validateConstraint(
|
|
844
|
+
ctx: ConstraintContext,
|
|
845
|
+
constraint: Constraint,
|
|
846
|
+
groupIndex: number,
|
|
847
|
+
constraintIndex: number,
|
|
848
|
+
issues: Issue[],
|
|
849
|
+
): void {
|
|
850
|
+
const operators = constraint.operators;
|
|
851
|
+
const underAny = constraint.scope === Scope.CALLDATA && ctx.steps.includes(Quantifier.ANY);
|
|
852
|
+
|
|
853
|
+
for (const opHex of operators) {
|
|
854
|
+
const opCode = parseInt(opHex.slice(2, 4), 16);
|
|
855
|
+
const base = opCode & ~Op.NOT;
|
|
856
|
+
|
|
857
|
+
// An unassigned opcode or mismatched payload size has no defined semantics to analyze.
|
|
858
|
+
const dataLength = (opHex.length - 4) / 2;
|
|
859
|
+
if (dataLength > 0xffff || !isValidOperatorData(base, dataLength)) {
|
|
860
|
+
issues.push(
|
|
861
|
+
ValidationIssue.fromOpRule(
|
|
862
|
+
"UNKNOWN_OPERATOR",
|
|
863
|
+
"Unknown operator code",
|
|
864
|
+
groupIndex,
|
|
865
|
+
constraintIndex,
|
|
866
|
+
bigintToHex(BigInt(opCode)),
|
|
867
|
+
),
|
|
868
|
+
);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const isNegated = (opCode & Op.NOT) !== 0;
|
|
873
|
+
|
|
874
|
+
// A negated operator under any() is satisfied by a single decoy element.
|
|
875
|
+
if (underAny && isNegated) {
|
|
876
|
+
issues.push(ValidationIssue.negationUnderAny(groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const incompat = getIncompat(base, ctx.typeInfo);
|
|
880
|
+
if (incompat) {
|
|
881
|
+
issues.push(
|
|
882
|
+
ValidationIssue.fromOpRule(
|
|
883
|
+
incompat.code,
|
|
884
|
+
incompat.message,
|
|
885
|
+
groupIndex,
|
|
886
|
+
constraintIndex,
|
|
887
|
+
bigintToHex(BigInt(opCode)),
|
|
888
|
+
),
|
|
889
|
+
);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (isValueOp(base)) {
|
|
894
|
+
// A non-canonical operand can never equal a canonicalized runtime value (PC-1, spec
|
|
895
|
+
// section 4.5), so the rule is unsatisfiable or vacuous; skip analyzing the garbage
|
|
896
|
+
// word. Scoped to the left-aligned types: numeric, address, and bool operands are
|
|
897
|
+
// already covered by the physical domain limits.
|
|
898
|
+
const typeCode = ctx.typeInfo.typeCode;
|
|
899
|
+
if (isLeftAligned(typeCode)) {
|
|
900
|
+
const nonCanonical = findNonCanonicalWord(opHex, typeCode);
|
|
901
|
+
if (nonCanonical !== null) {
|
|
902
|
+
issues.push(
|
|
903
|
+
ValidationIssue.nonCanonicalOperand(
|
|
904
|
+
groupIndex,
|
|
905
|
+
constraintIndex,
|
|
906
|
+
bigintToHex(nonCanonical.word),
|
|
907
|
+
bigintToHex(nonCanonical.canonical),
|
|
908
|
+
),
|
|
909
|
+
);
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (base >= Op.EQ && base <= Op.BETWEEN) {
|
|
915
|
+
if (base === Op.BETWEEN) {
|
|
916
|
+
// A negated range is a disjunction (value < low OR value > high); distributing the
|
|
917
|
+
// negation over the decomposed bounds would conjoin the complement halves into a
|
|
918
|
+
// spurious contradiction. Leave it un-analyzed, like a neq hole. Inverted bounds
|
|
919
|
+
// still surface: negating an empty range excludes nothing.
|
|
920
|
+
const { low, high } = readPair(opHex);
|
|
921
|
+
if (!isNegated) {
|
|
922
|
+
updateBound(ctx.numeric, Op.GTE, false, low, false, groupIndex, constraintIndex, issues);
|
|
923
|
+
updateBound(ctx.numeric, Op.LTE, false, high, false, groupIndex, constraintIndex, issues);
|
|
924
|
+
} else if (signedCompare(low, high, ctx.numeric.isSigned) > 0n) {
|
|
925
|
+
issues.push(
|
|
926
|
+
ValidationIssue.vacuousNegatedRange(
|
|
927
|
+
false,
|
|
928
|
+
groupIndex,
|
|
929
|
+
constraintIndex,
|
|
930
|
+
bigintToHex(low),
|
|
931
|
+
bigintToHex(high),
|
|
932
|
+
),
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
} else {
|
|
936
|
+
const value = readValue(opHex);
|
|
937
|
+
const holesBefore = ctx.numeric.holes.length;
|
|
938
|
+
updateBound(ctx.numeric, base, isNegated, value, false, groupIndex, constraintIndex, issues);
|
|
939
|
+
if (ctx.numeric.holes.length > holesBefore) {
|
|
940
|
+
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
} else if (isBitmaskOp(base)) {
|
|
944
|
+
const mask = readValue(opHex);
|
|
945
|
+
updateBitmask(ctx, base, isNegated, mask, groupIndex, constraintIndex, issues);
|
|
946
|
+
} else if (base === Op.IN) {
|
|
947
|
+
const values = unpackSet(opHex);
|
|
948
|
+
if (!isStrictlyAscending(values)) {
|
|
949
|
+
issues.push(ValidationIssue.unsortedInSet(groupIndex, constraintIndex));
|
|
950
|
+
} else {
|
|
951
|
+
updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
} else if (isLengthOp(base)) {
|
|
955
|
+
if (base === Op.LENGTH_BETWEEN) {
|
|
956
|
+
// Same disjunction reasoning as the negated value-range above.
|
|
957
|
+
const { low, high } = readPair(opHex);
|
|
958
|
+
if (!isNegated) {
|
|
959
|
+
updateBound(ctx.length, Op.GTE, false, low, true, groupIndex, constraintIndex, issues);
|
|
960
|
+
updateBound(ctx.length, Op.LTE, false, high, true, groupIndex, constraintIndex, issues);
|
|
961
|
+
} else if (low > high) {
|
|
962
|
+
issues.push(
|
|
963
|
+
ValidationIssue.vacuousNegatedRange(true, groupIndex, constraintIndex, bigintToHex(low), bigintToHex(high)),
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
const value = readValue(opHex);
|
|
968
|
+
updateBound(ctx.length, normalizeLengthOp(base), isNegated, value, true, groupIndex, constraintIndex, issues);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
checkDuplicates(issues, operators, groupIndex, constraintIndex);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
///////////////////////////////////////////////////////////////////////////
|
|
977
|
+
// Group validation
|
|
978
|
+
///////////////////////////////////////////////////////////////////////////
|
|
979
|
+
|
|
980
|
+
/** Validate all constraints in a group, building per-path contexts for cross-constraint analysis. */
|
|
981
|
+
function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: number, issues: Issue[]): void {
|
|
982
|
+
const constraints = data.groups[groupIndex]!;
|
|
983
|
+
const contexts: ConstraintContext[] = [];
|
|
984
|
+
|
|
985
|
+
for (let constraintIndex = 0; constraintIndex < constraints.length; constraintIndex++) {
|
|
986
|
+
const constraint = constraints[constraintIndex]!;
|
|
987
|
+
const normalizedPath: Hex = `0x${constraint.path.slice(2).toLowerCase()}`;
|
|
988
|
+
|
|
989
|
+
// Find existing context for this (scope, path) pair.
|
|
990
|
+
let ctx: ConstraintContext | undefined;
|
|
991
|
+
for (const existing of contexts) {
|
|
992
|
+
if (existing.scope === constraint.scope && existing.path === normalizedPath) {
|
|
993
|
+
ctx = existing;
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (!ctx) {
|
|
999
|
+
const steps = parsePathSteps(constraint.path);
|
|
1000
|
+
let typeInfo: TypeInfo;
|
|
1001
|
+
if (constraint.scope === Scope.CALLDATA) {
|
|
1002
|
+
// Compatibility warnings against the limits the spec fixes (PWF-17, PV-7).
|
|
1003
|
+
if (steps.length > Limits.MAX_PATH_DEPTH) {
|
|
1004
|
+
issues.push(
|
|
1005
|
+
ValidationIssue.pathDepthExceeded(
|
|
1006
|
+
groupIndex,
|
|
1007
|
+
constraintIndex,
|
|
1008
|
+
bigintToHex(BigInt(steps.length)),
|
|
1009
|
+
bigintToHex(BigInt(Limits.MAX_PATH_DEPTH)),
|
|
1010
|
+
),
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
let walk: ReturnType<typeof Descriptor.walkPath>;
|
|
1014
|
+
try {
|
|
1015
|
+
walk = Descriptor.walkPath(descBytes, steps);
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
// Navigability faults become issues; anything else propagates.
|
|
1018
|
+
if (error instanceof CallciumError && NAVIGABILITY_CODES.has(error.code)) {
|
|
1019
|
+
issues.push(ValidationIssue.unnavigablePath(groupIndex, constraintIndex));
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
throw error;
|
|
1023
|
+
}
|
|
1024
|
+
// One array at a time carries a quantifier, so the element chain stays singular.
|
|
1025
|
+
if (steps.filter(isQuantifier).length > 1) {
|
|
1026
|
+
issues.push(ValidationIssue.nestedQuantifier(groupIndex, constraintIndex));
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
if (walk.quantifiedStaticLength > Limits.MAX_QUANTIFIED_ARRAY_LENGTH) {
|
|
1030
|
+
issues.push(
|
|
1031
|
+
ValidationIssue.quantifierOverStaticLimit(
|
|
1032
|
+
groupIndex,
|
|
1033
|
+
constraintIndex,
|
|
1034
|
+
bigintToHex(BigInt(walk.quantifiedStaticLength)),
|
|
1035
|
+
bigintToHex(BigInt(Limits.MAX_QUANTIFIED_ARRAY_LENGTH)),
|
|
1036
|
+
),
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
typeInfo = walk.typeInfo;
|
|
1040
|
+
} else {
|
|
1041
|
+
const ctxId = steps[0]!;
|
|
1042
|
+
if (ctxId > MAX_CONTEXT_PROPERTY_ID) {
|
|
1043
|
+
// An unassigned ID declares no type, so the analysis proceeds over the widest
|
|
1044
|
+
// unsigned domain and the warning carries the ID.
|
|
1045
|
+
issues.push(
|
|
1046
|
+
ValidationIssue.unknownContextProperty(
|
|
1047
|
+
groupIndex,
|
|
1048
|
+
constraintIndex,
|
|
1049
|
+
bigintToHex(BigInt(ctxId)),
|
|
1050
|
+
bigintToHex(BigInt(MAX_CONTEXT_PROPERTY_ID)),
|
|
1051
|
+
),
|
|
1052
|
+
);
|
|
1053
|
+
typeInfo = { typeCode: TypeCode.UINT_MAX, isDynamic: false, staticSize: 32 };
|
|
1054
|
+
} else {
|
|
1055
|
+
const typeCode = lookupContextProperty(ctxId).typeCode;
|
|
1056
|
+
typeInfo = { typeCode, isDynamic: false, staticSize: 32 };
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
ctx = initContext(constraint.scope, normalizedPath, steps, typeInfo);
|
|
1060
|
+
contexts.push(ctx);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// A carried hint must equal the compilation of its own path; constraints without an encoding
|
|
1064
|
+
// carry none, and the encoder compiles theirs from the same descriptor. Reaching here means
|
|
1065
|
+
// the path navigates the descriptor and quantifies at most once, so only the depth PWF-17
|
|
1066
|
+
// admits remains to be established.
|
|
1067
|
+
if (
|
|
1068
|
+
constraint.scope === Scope.CALLDATA &&
|
|
1069
|
+
constraint.hint !== undefined &&
|
|
1070
|
+
ctx.steps.length <= Limits.MAX_PATH_DEPTH
|
|
1071
|
+
) {
|
|
1072
|
+
const compiled = bytesToHex(Descriptor.compileHint(descBytes, ctx.steps));
|
|
1073
|
+
if (compiled !== constraint.hint.toLowerCase()) {
|
|
1074
|
+
issues.push(ValidationIssue.hintMismatch(groupIndex, constraintIndex));
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues);
|
|
1079
|
+
|
|
1080
|
+
checkFusibleRange(ctx, constraint.operators, groupIndex, constraintIndex, issues);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
///////////////////////////////////////////////////////////////////////////
|
|
1085
|
+
// Public API
|
|
1086
|
+
///////////////////////////////////////////////////////////////////////////
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Validate a policy for type mismatches, contradictions, redundancies, and vacuities.
|
|
1090
|
+
* @param data - The canonical policy data to validate.
|
|
1091
|
+
* @returns All validation issues found, ordered by group and constraint index.
|
|
1092
|
+
* @throws {CallciumError} If the descriptor is not well-formed; semantic validation is
|
|
1093
|
+
* defined only over a well-formed descriptor.
|
|
1094
|
+
*/
|
|
1095
|
+
function validate(data: PolicyData): Issue[] {
|
|
1096
|
+
const descBytes = hexToBytes(data.descriptor);
|
|
1097
|
+
decodeDescriptor(descBytes);
|
|
1098
|
+
|
|
1099
|
+
const issues: Issue[] = [];
|
|
1100
|
+
for (let groupIndex = 0; groupIndex < data.groups.length; groupIndex++) {
|
|
1101
|
+
if (data.groups[groupIndex]!.length === 0) {
|
|
1102
|
+
issues.push(ValidationIssue.emptyGroup(groupIndex));
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
validateGroup(data, descBytes, groupIndex, issues);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return issues;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/** Semantic validation for policies. */
|
|
1112
|
+
export const PolicyValidator = { validate };
|