@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-validator.ts
CHANGED
|
@@ -1,1169 +1,680 @@
|
|
|
1
|
-
import { bigintToHex, bytesToHex, hexToBytes } from "./bytes";
|
|
1
|
+
import { bigintToHex, bytesToHex, hexToBytes, readU16 } from "./bytes";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
comparisonOf,
|
|
4
|
+
isComparisonCode,
|
|
5
5
|
MAX_CONTEXT_PROPERTY_ID,
|
|
6
6
|
Op,
|
|
7
|
-
|
|
8
|
-
PolicyFormat,
|
|
7
|
+
PolicyFormat as PF,
|
|
9
8
|
Scope,
|
|
9
|
+
Source,
|
|
10
|
+
sourceOf,
|
|
10
11
|
TypeCode,
|
|
11
12
|
} from "./constants";
|
|
12
|
-
import { Descriptor
|
|
13
|
-
import {
|
|
14
|
-
import { CallciumError, type CallciumErrorCode } from "./errors";
|
|
13
|
+
import { Descriptor } from "./descriptor";
|
|
14
|
+
import { Hint } from "./hint";
|
|
15
15
|
import {
|
|
16
|
-
|
|
16
|
+
acceptsContextSource,
|
|
17
|
+
contextPropertyType,
|
|
18
|
+
hasCalldataLength,
|
|
17
19
|
isLeftAligned,
|
|
18
20
|
isSigned,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
isElementary,
|
|
23
|
-
isValidOperatorData,
|
|
24
|
-
toOperatorBytes,
|
|
21
|
+
operandTypeOf,
|
|
22
|
+
operandWidth,
|
|
23
|
+
toBigInt,
|
|
25
24
|
} from "./operators";
|
|
26
|
-
import {
|
|
25
|
+
import { parsePathSteps } from "./path";
|
|
26
|
+
import { PolicyCoder, validatePolicy } from "./policy-coder";
|
|
27
27
|
import * as ValidationIssue from "./validation-issue";
|
|
28
28
|
|
|
29
|
-
import type {
|
|
29
|
+
import type { BodyRule, Hex, Issue, IssueCode, LoopRule, Rule, RuleLocation } from "./types";
|
|
30
30
|
|
|
31
31
|
///////////////////////////////////////////////////////////////////////////
|
|
32
|
-
//
|
|
32
|
+
// Compatibility
|
|
33
33
|
///////////////////////////////////////////////////////////////////////////
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
39
|
-
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
40
|
-
"NOT_COMPOSITE",
|
|
41
|
-
]);
|
|
42
|
-
|
|
43
|
-
type BoundDomain = {
|
|
44
|
-
isSigned: boolean;
|
|
45
|
-
min: bigint;
|
|
46
|
-
max: bigint;
|
|
47
|
-
hasEq: boolean;
|
|
48
|
-
eq: bigint;
|
|
49
|
-
hasLower: boolean;
|
|
50
|
-
lower: bigint;
|
|
51
|
-
lowerInclusive: boolean;
|
|
52
|
-
hasUpper: boolean;
|
|
53
|
-
upper: bigint;
|
|
54
|
-
upperInclusive: boolean;
|
|
55
|
-
holes: bigint[];
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
type BitmaskDomain = {
|
|
59
|
-
mustBeOne: bigint;
|
|
60
|
-
mustBeZero: bigint;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
type SetDomain = {
|
|
64
|
-
hasIn: boolean;
|
|
65
|
-
inValues: bigint[];
|
|
66
|
-
notInValues: bigint[];
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
type ConstraintContext = {
|
|
70
|
-
scope: number;
|
|
71
|
-
path: Hex;
|
|
72
|
-
/** Path steps of every constraint sharing this context. */
|
|
73
|
-
steps: number[];
|
|
74
|
-
typeInfo: TypeInfo;
|
|
75
|
-
numeric: BoundDomain;
|
|
76
|
-
bitmask: BitmaskDomain;
|
|
77
|
-
length: BoundDomain;
|
|
78
|
-
set: SetDomain;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
///////////////////////////////////////////////////////////////////////////
|
|
82
|
-
// Constants
|
|
83
|
-
///////////////////////////////////////////////////////////////////////////
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Maximum length value for dynamic types.
|
|
87
|
-
*/
|
|
88
|
-
const LENGTH_MAX = 0xffffffffn;
|
|
89
|
-
|
|
90
|
-
// Mask for reinterpreting uint256 as int256 (two's complement).
|
|
91
|
-
const UINT256_MAX = (1n << 256n) - 1n;
|
|
92
|
-
const INT256_SIGN_BIT = 1n << 255n;
|
|
93
|
-
|
|
94
|
-
///////////////////////////////////////////////////////////////////////////
|
|
95
|
-
// Signed-aware comparisons
|
|
96
|
-
///////////////////////////////////////////////////////////////////////////
|
|
97
|
-
|
|
98
|
-
/** Signed-aware difference of two raw uint256 values: negative if a<b, zero if equal, positive if a>b. */
|
|
99
|
-
function signedCompare(a: bigint, b: bigint, signed: boolean): bigint {
|
|
100
|
-
if (!signed) return a - b;
|
|
101
|
-
const toSigned = (word: bigint) => (word >= INT256_SIGN_BIT ? word - (1n << 256n) : word);
|
|
102
|
-
return toSigned(a) - toSigned(b);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** True when a value lies outside the domain's physical bounds. */
|
|
106
|
-
function isOutsideDomain(domain: BoundDomain, value: bigint): boolean {
|
|
107
|
-
return (
|
|
108
|
-
signedCompare(value, domain.min, domain.isSigned) < 0n || signedCompare(value, domain.max, domain.isSigned) > 0n
|
|
109
|
-
);
|
|
35
|
+
/** Return true for a `uintN` type code. */
|
|
36
|
+
function isUnsigned(typeCode: number): boolean {
|
|
37
|
+
return typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX;
|
|
110
38
|
}
|
|
111
39
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
/** True for uint and int type codes. */
|
|
117
|
-
function isNumericType(typeCode: number): boolean {
|
|
118
|
-
return (
|
|
119
|
-
(typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) ||
|
|
120
|
-
(typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX)
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** True for uint and bytes32 type codes. */
|
|
125
|
-
function isBitmaskCompatible(typeCode: number): boolean {
|
|
126
|
-
return (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) || typeCode === TypeCode.FIXED_BYTES_MIN + 31;
|
|
40
|
+
/** Return true for a `uintN` or `intN` type code. */
|
|
41
|
+
function isNumeric(typeCode: number): boolean {
|
|
42
|
+
return isUnsigned(typeCode) || (typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX);
|
|
127
43
|
}
|
|
128
44
|
|
|
129
|
-
/**
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
45
|
+
/** Return the code and message a constant comparison raises against its target type, or undefined when it applies. */
|
|
46
|
+
function incompatibility(comparison: number, typeCode: number): { code: IssueCode; message: string } | undefined {
|
|
47
|
+
const isOrdering =
|
|
48
|
+
comparison === Op.GT ||
|
|
49
|
+
comparison === Op.LT ||
|
|
50
|
+
comparison === Op.GTE ||
|
|
51
|
+
comparison === Op.LTE ||
|
|
52
|
+
comparison === Op.BETWEEN;
|
|
53
|
+
if (isOrdering && !isNumeric(typeCode)) {
|
|
54
|
+
return { code: "NUMERIC_OP_ON_NON_NUMERIC", message: "Comparison operator used on non-numeric type" };
|
|
134
55
|
}
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
if (bits === 256n) {
|
|
138
|
-
// int256: min = uint256(type(int256).min), max = uint256(type(int256).max).
|
|
139
|
-
return { min: INT256_SIGN_BIT, max: INT256_SIGN_BIT - 1n };
|
|
140
|
-
}
|
|
141
|
-
const half = 1n << (bits - 1n);
|
|
142
|
-
// Stored as raw uint256: min is two's complement of -(2^(bits-1)).
|
|
143
|
-
const max = half - 1n;
|
|
144
|
-
const min = (UINT256_MAX - half + 1n) & UINT256_MAX;
|
|
145
|
-
return { min, max };
|
|
56
|
+
if (comparison === Op.IN && typeCode === TypeCode.BOOL) {
|
|
57
|
+
return { code: "IN_ON_BOOL", message: "IN operator used on boolean type" };
|
|
146
58
|
}
|
|
147
|
-
|
|
148
|
-
if (typeCode === TypeCode.
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
///////////////////////////////////////////////////////////////////////////
|
|
153
|
-
// Operator classification
|
|
154
|
-
///////////////////////////////////////////////////////////////////////////
|
|
155
|
-
|
|
156
|
-
/** True if an operator code is in the value-operator range (unassigned codes included). */
|
|
157
|
-
function isValueOp(opBase: number): boolean {
|
|
158
|
-
return opBase <= Op.BITMASK_NONE;
|
|
59
|
+
const isMask = comparison === Op.BITMASK_ALL || comparison === Op.BITMASK_ANY || comparison === Op.BITMASK_NONE;
|
|
60
|
+
if (isMask && (typeCode === TypeCode.BOOL || typeCode === TypeCode.FUNCTION)) {
|
|
61
|
+
return { code: "BITMASK_ON_INVALID", message: "Bitmask operator used on incompatible type" };
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
159
64
|
}
|
|
160
65
|
|
|
161
|
-
/**
|
|
162
|
-
function
|
|
163
|
-
|
|
66
|
+
/** Return true when a context operand of `propertyType` pairs with a target of `typeCode` under `comparison`. */
|
|
67
|
+
function pairsWithContext(comparison: number, typeCode: number, propertyType: number): boolean {
|
|
68
|
+
if (isUnsigned(typeCode)) return propertyType === TypeCode.UINT_MAX;
|
|
69
|
+
return comparison === Op.EQ && typeCode === TypeCode.ADDRESS && propertyType === TypeCode.ADDRESS;
|
|
164
70
|
}
|
|
165
71
|
|
|
166
|
-
/**
|
|
167
|
-
|
|
168
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Determine whether the compatibility matrix admits an operator on a target type. A context operand is admitted on a
|
|
74
|
+
* target its comparison can pair with a context property. These targets are `uintN` under `EQ`, `GT`, `LT`, `GTE` and
|
|
75
|
+
* `LTE`, and `address` under `EQ`.
|
|
76
|
+
* @param opCode - The operator code, with or without the NOT flag.
|
|
77
|
+
* @param typeCode - The target's type code.
|
|
78
|
+
* @returns True when the operator applies to the target.
|
|
79
|
+
*/
|
|
80
|
+
export function isOpAllowed(opCode: number, typeCode: number): boolean {
|
|
81
|
+
const comparison = comparisonOf(opCode);
|
|
82
|
+
if (!isComparisonCode(comparison)) return false;
|
|
83
|
+
if (sourceOf(opCode) === Source.CONTEXT) {
|
|
84
|
+
if (!acceptsContextSource(comparison)) return false;
|
|
85
|
+
return isUnsigned(typeCode) || (comparison === Op.EQ && typeCode === TypeCode.ADDRESS);
|
|
86
|
+
}
|
|
87
|
+
if (sourceOf(opCode) !== Source.CONSTANT) return false;
|
|
88
|
+
return operandTypeOf(comparison, typeCode) !== undefined && incompatibility(comparison, typeCode) === undefined;
|
|
169
89
|
}
|
|
170
90
|
|
|
171
91
|
///////////////////////////////////////////////////////////////////////////
|
|
172
|
-
//
|
|
92
|
+
// Rule checks
|
|
173
93
|
///////////////////////////////////////////////////////////////////////////
|
|
174
94
|
|
|
175
|
-
/**
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (isBitmaskOp(opBase) && !isBitmaskCompatible(typeCode)) {
|
|
189
|
-
return { code: "BITMASK_ON_INVALID", message: "Bitmask operator used on incompatible type" };
|
|
190
|
-
}
|
|
191
|
-
// IN on bool is degenerate: two values reduce every set to an equality, a tautology, or dead members.
|
|
192
|
-
if (opBase === Op.IN && typeCode === TypeCode.BOOL) {
|
|
193
|
-
return { code: "IN_ON_BOOL", message: "IN operator used on boolean type" };
|
|
194
|
-
}
|
|
195
|
-
// Context properties are addresses or uint256, so only those target classes can match, and an address is an
|
|
196
|
-
// identity rather than a magnitude, so it pairs under equality only.
|
|
197
|
-
if (
|
|
198
|
-
isContextOp(opBase) &&
|
|
199
|
-
!(typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) &&
|
|
200
|
-
!(opBase === Op.EQ_CTX && typeCode === TypeCode.ADDRESS)
|
|
201
|
-
) {
|
|
202
|
-
return { code: "CONTEXT_TYPE_MISMATCH", message: "Context reference operator used on incompatible type" };
|
|
203
|
-
}
|
|
204
|
-
return null;
|
|
95
|
+
/**
|
|
96
|
+
* Push the PV-5 and PV-6 issues of a test's operator against a target of `typeCode`.
|
|
97
|
+
* Return true when the operator raises none.
|
|
98
|
+
*/
|
|
99
|
+
function checkOperator(test: BodyRule, typeCode: number, location: RuleLocation, issues: Issue[]): boolean {
|
|
100
|
+
const operator = hexToBytes(test.operator);
|
|
101
|
+
const opCode = operator[0]!;
|
|
102
|
+
const comparison = comparisonOf(opCode);
|
|
103
|
+
if (sourceOf(opCode) === Source.CONTEXT) {
|
|
104
|
+
const propertyId = Number(toBigInt(operator, 1));
|
|
105
|
+
if (pairsWithContext(comparison, typeCode, contextPropertyType(propertyId))) return true;
|
|
106
|
+
issues.push(ValidationIssue.contextTypeMismatch(location, propertyId, typeCode));
|
|
107
|
+
return false;
|
|
205
108
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
109
|
+
|
|
110
|
+
const incompatible = incompatibility(comparison, typeCode);
|
|
111
|
+
if (incompatible !== undefined) {
|
|
112
|
+
issues.push(ValidationIssue.fromOpRule(incompatible.code, incompatible.message, location, opCode));
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (typeCode !== TypeCode.BOOL) return true;
|
|
116
|
+
for (let offset = 1; offset < operator.length; offset += 32) {
|
|
117
|
+
const value = toBigInt(operator, offset);
|
|
118
|
+
if (value > 1n) {
|
|
119
|
+
issues.push(ValidationIssue.outOfPhysicalBounds(location, bigintToHex(value)));
|
|
120
|
+
return false;
|
|
209
121
|
}
|
|
210
|
-
return null;
|
|
211
122
|
}
|
|
212
|
-
return
|
|
123
|
+
return true;
|
|
213
124
|
}
|
|
214
125
|
|
|
215
126
|
/**
|
|
216
|
-
*
|
|
217
|
-
*
|
|
127
|
+
* Push the issues of a test, from the arguments or, given `element`, from the loop element whose node lies there.
|
|
128
|
+
* Returns the test, or undefined when an issue leaves it without a target to analyze or its hint differs from its
|
|
129
|
+
* path's.
|
|
218
130
|
*/
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
131
|
+
function checkTest(
|
|
132
|
+
desc: Uint8Array,
|
|
133
|
+
test: BodyRule,
|
|
134
|
+
element: number | undefined,
|
|
135
|
+
location: RuleLocation,
|
|
136
|
+
issues: Issue[],
|
|
137
|
+
): BodyRule | undefined {
|
|
138
|
+
let typeCode: number;
|
|
139
|
+
let hintMatches = true;
|
|
140
|
+
if (test.scope === Scope.CONTEXT) {
|
|
141
|
+
typeCode = contextPropertyType(readU16(hexToBytes(test.path), 0));
|
|
142
|
+
} else {
|
|
143
|
+
const compiled = Hint.compileTest(desc, parsePathSteps(test.path), element);
|
|
144
|
+
if ("fault" in compiled) {
|
|
145
|
+
issues.push(ValidationIssue.unnavigablePath(location, compiled.stepIndex, compiled.bound));
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
hintMatches = bytesToHex(compiled.hint) === test.hint;
|
|
149
|
+
if (!hintMatches) issues.push(ValidationIssue.hintMismatch(location));
|
|
150
|
+
typeCode = desc[compiled.node]!;
|
|
151
|
+
}
|
|
152
|
+
// A test whose hint differs from its path's has constants sized to the hint's type, so it leaves the analysis.
|
|
153
|
+
return checkOperator(test, typeCode, location, issues) && hintMatches ? test : undefined;
|
|
222
154
|
}
|
|
223
155
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
/** Read a single uint256 from operator hex (bytes after the opcode byte). */
|
|
229
|
-
function readValue(opHex: Hex): bigint {
|
|
230
|
-
return BigInt(`0x${opHex.slice(4)}`);
|
|
156
|
+
/** Return the declared length a loop's frame hint carries, or zero for a dynamic array. */
|
|
157
|
+
function frameCount(hint: Uint8Array): number {
|
|
158
|
+
const hopCount = hint[0]!;
|
|
159
|
+
return readU16(hint, PF.HINT_HEADER_SIZE + hopCount * PF.HINT_HOP_SIZE + PF.HINT_FRAME_COUNT_OFFSET);
|
|
231
160
|
}
|
|
232
161
|
|
|
233
|
-
/**
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Push the issues of a loop and its body. Returns the loop without the body tests left without a target. The result is
|
|
164
|
+
* undefined when the loop's array path does not compile or no body test is left.
|
|
165
|
+
*/
|
|
166
|
+
function checkLoop(desc: Uint8Array, loop: LoopRule, location: RuleLocation, issues: Issue[]): LoopRule | undefined {
|
|
167
|
+
const compiled = Hint.compileLoop(desc, parsePathSteps(loop.path));
|
|
168
|
+
if ("fault" in compiled) {
|
|
169
|
+
issues.push(
|
|
170
|
+
compiled.fault === "NOT_ARRAY"
|
|
171
|
+
? ValidationIssue.quantifierOnNonArray(location, compiled.bound)
|
|
172
|
+
: ValidationIssue.unnavigablePath(location, compiled.stepIndex, compiled.bound),
|
|
173
|
+
);
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
if (bytesToHex(compiled.hint) !== loop.hint) issues.push(ValidationIssue.hintMismatch(location));
|
|
177
|
+
const count = frameCount(compiled.hint);
|
|
178
|
+
if (count > PF.MAX_QUANTIFIED_ARRAY_LENGTH) {
|
|
179
|
+
issues.push(ValidationIssue.quantifierOverStaticLimit(location, count, PF.MAX_QUANTIFIED_ARRAY_LENGTH));
|
|
240
180
|
}
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
181
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
};
|
|
182
|
+
const body: BodyRule[] = [];
|
|
183
|
+
loop.body.forEach((test, bodyRuleIndex) => {
|
|
184
|
+
const testLocation = { ...location, bodyRuleIndex, scope: test.scope, path: test.path, operator: test.operator };
|
|
185
|
+
const kept = checkTest(desc, test, compiled.node, testLocation, issues);
|
|
186
|
+
if (kept !== undefined) body.push(kept);
|
|
187
|
+
});
|
|
188
|
+
return body.length === 0 ? undefined : { ...loop, body };
|
|
251
189
|
}
|
|
252
190
|
|
|
253
|
-
/**
|
|
254
|
-
function
|
|
255
|
-
|
|
256
|
-
const count = body.length / 64;
|
|
257
|
-
const values: bigint[] = [];
|
|
258
|
-
for (let i = 0; i < count; i++) {
|
|
259
|
-
values.push(BigInt(`0x${body.slice(i * 64, (i + 1) * 64)}`));
|
|
260
|
-
}
|
|
261
|
-
return values;
|
|
191
|
+
/** The identity of a loop within its group across normalization: its quantifier and its array path. */
|
|
192
|
+
function loopKey(loop: LoopRule): string {
|
|
193
|
+
return `${loop.quantifier}:${loop.path}`;
|
|
262
194
|
}
|
|
263
195
|
|
|
264
|
-
/**
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Push a PV-8 issue for each loop of a group's normal form whose body contains no calldata test. Skip a loop if the
|
|
198
|
+
* group lost a test from a loop with the same quantifier and array path.
|
|
199
|
+
*/
|
|
200
|
+
function checkBodies(rules: Rule[], groupIndex: number, lost: ReadonlySet<string>, issues: Issue[]): void {
|
|
201
|
+
rules.forEach((rule, ruleIndex) => {
|
|
202
|
+
if (rule.scope !== Scope.LOOP || lost.has(loopKey(rule))) return;
|
|
203
|
+
if (rule.body.some((test) => test.scope === Scope.CALLDATA)) return;
|
|
204
|
+
issues.push(ValidationIssue.contextOnlyBody({ groupIndex, ruleIndex, scope: rule.scope, path: rule.path }));
|
|
205
|
+
});
|
|
270
206
|
}
|
|
271
207
|
|
|
272
208
|
///////////////////////////////////////////////////////////////////////////
|
|
273
|
-
//
|
|
209
|
+
// Subject analysis
|
|
274
210
|
///////////////////////////////////////////////////////////////////////////
|
|
275
211
|
|
|
276
|
-
/**
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
212
|
+
/** The values of a subject as keys: `width`-bit patterns ordered unsigned, a signed one with its top bit flipped. */
|
|
213
|
+
type Domain = {
|
|
214
|
+
width: number;
|
|
215
|
+
/** The greatest key. */
|
|
216
|
+
max: bigint;
|
|
217
|
+
/** The top bit of a signed type's pattern, zero otherwise. */
|
|
218
|
+
flip: bigint;
|
|
219
|
+
/** The bits below a left-aligned type's pattern in its word, zero otherwise. */
|
|
220
|
+
shift: bigint;
|
|
221
|
+
};
|
|
285
222
|
|
|
286
|
-
/**
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
223
|
+
/** The relations of a subject to a context property's value: below it, equal to it, above it. */
|
|
224
|
+
const LESS = 1;
|
|
225
|
+
const EQUAL = 2;
|
|
226
|
+
const GREATER = 4;
|
|
227
|
+
const ANY_RELATION = LESS | EQUAL | GREATER;
|
|
228
|
+
|
|
229
|
+
/** The sides of a subject's range a rule bounds. */
|
|
230
|
+
const LOWER_SIDE = 1;
|
|
231
|
+
const UPPER_SIDE = 2;
|
|
232
|
+
|
|
233
|
+
/** A rule's condition on its subject's key. */
|
|
234
|
+
type Literal =
|
|
235
|
+
/** Keys from `lower` to `upper`, none when `lower` exceeds `upper`. */
|
|
236
|
+
| { kind: "range"; negated: boolean; lower: bigint; upper: bigint }
|
|
237
|
+
/** The member keys. */
|
|
238
|
+
| { kind: "set"; negated: boolean; members: bigint[] }
|
|
239
|
+
/** Keys with every bit of `ones` set and every bit of `zeros` clear. */
|
|
240
|
+
| { kind: "bits"; negated: boolean; ones: bigint; zeros: bigint }
|
|
241
|
+
/** Keys in one of `relations` to the value of a context property. */
|
|
242
|
+
| { kind: "relation"; negated: boolean; property: number; relations: number };
|
|
243
|
+
|
|
244
|
+
/** A rule on a subject: where it lies, its literal, the context properties it reads and the sides it bounds. */
|
|
245
|
+
type SubjectRule = { location: RuleLocation; literal: Literal; reads: number; sides: number };
|
|
246
|
+
|
|
247
|
+
/** The first `count` literals, the one at `flipped` negated, with their range and fixed bits folded. */
|
|
248
|
+
type Conjunction = {
|
|
249
|
+
literals: readonly Literal[];
|
|
250
|
+
count: number;
|
|
251
|
+
flipped: number;
|
|
252
|
+
lower: bigint;
|
|
253
|
+
upper: bigint;
|
|
254
|
+
ones: bigint;
|
|
255
|
+
zeros: bigint;
|
|
256
|
+
};
|
|
294
257
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
///////////////////////////////////////////////////////////////////////////
|
|
258
|
+
/** Index of no literal. */
|
|
259
|
+
const NO_LITERAL = -1;
|
|
298
260
|
|
|
299
|
-
/**
|
|
300
|
-
function
|
|
261
|
+
/** Return the domain of a subject of `typeCode`. A length compares as 256 bits. */
|
|
262
|
+
function domainOf(typeCode: number): Domain {
|
|
263
|
+
const width = typeCode === TypeCode.BOOL ? 1 : 8 * (operandWidth(typeCode) ?? 32);
|
|
301
264
|
return {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
eq: 0n,
|
|
307
|
-
hasLower: false,
|
|
308
|
-
lower: 0n,
|
|
309
|
-
lowerInclusive: false,
|
|
310
|
-
hasUpper: false,
|
|
311
|
-
upper: 0n,
|
|
312
|
-
upperInclusive: false,
|
|
313
|
-
holes: [],
|
|
265
|
+
width,
|
|
266
|
+
max: (1n << BigInt(width)) - 1n,
|
|
267
|
+
flip: isSigned(typeCode) ? 1n << BigInt(width - 1) : 0n,
|
|
268
|
+
shift: isLeftAligned(typeCode) ? BigInt(256 - width) : 0n,
|
|
314
269
|
};
|
|
315
270
|
}
|
|
316
271
|
|
|
317
|
-
/**
|
|
318
|
-
function
|
|
319
|
-
|
|
320
|
-
return {
|
|
321
|
-
scope,
|
|
322
|
-
path,
|
|
323
|
-
steps,
|
|
324
|
-
typeInfo,
|
|
325
|
-
numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
|
|
326
|
-
// Bits above the target's width are zero in every canonical value.
|
|
327
|
-
bitmask: { mustBeOne: 0n, mustBeZero: isBitmaskCompatible(typeInfo.typeCode) ? UINT256_MAX ^ max : 0n },
|
|
328
|
-
length: emptyBoundDomain(false, 0n, LENGTH_MAX),
|
|
329
|
-
set: {
|
|
330
|
-
hasIn: false,
|
|
331
|
-
inValues: [],
|
|
332
|
-
notInValues: [],
|
|
333
|
-
},
|
|
334
|
-
};
|
|
272
|
+
/** Return the pattern a loaded word contains. */
|
|
273
|
+
function patternOf(domain: Domain, word: bigint): bigint {
|
|
274
|
+
return (word >> domain.shift) & domain.max;
|
|
335
275
|
}
|
|
336
276
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
function updateBound(
|
|
343
|
-
domain: BoundDomain,
|
|
344
|
-
base: number,
|
|
345
|
-
isNegated: boolean,
|
|
346
|
-
value: bigint,
|
|
347
|
-
isLength: boolean,
|
|
348
|
-
groupIndex: number,
|
|
349
|
-
constraintIndex: number,
|
|
350
|
-
issues: Issue[],
|
|
351
|
-
): void {
|
|
352
|
-
if (isNegated) {
|
|
353
|
-
if (base === Op.EQ) {
|
|
354
|
-
// A hole outside the type's domain excludes nothing.
|
|
355
|
-
if (isOutsideDomain(domain, value)) {
|
|
356
|
-
issues.push(ValidationIssue.outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
357
|
-
}
|
|
358
|
-
if (domain.hasEq && domain.eq === value) {
|
|
359
|
-
issues.push(ValidationIssue.eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
360
|
-
}
|
|
361
|
-
let alreadyHole = false;
|
|
362
|
-
for (let j = 0; j < domain.holes.length; j++) {
|
|
363
|
-
if (domain.holes[j] === value) {
|
|
364
|
-
alreadyHole = true;
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
if (!alreadyHole) {
|
|
369
|
-
domain.holes.push(value);
|
|
370
|
-
}
|
|
371
|
-
} else {
|
|
372
|
-
updateBound(domain, negateBoundOp(base), false, value, isLength, groupIndex, constraintIndex, issues);
|
|
373
|
-
}
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Vacuity checks.
|
|
378
|
-
if (base === Op.GTE && value === domain.min) {
|
|
379
|
-
issues.push(ValidationIssue.vacuousGte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
380
|
-
} else if (base === Op.LTE && value === domain.max) {
|
|
381
|
-
issues.push(ValidationIssue.vacuousLte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Physical bounds and impossibility.
|
|
385
|
-
if (isOutsideDomain(domain, value)) {
|
|
386
|
-
issues.push(ValidationIssue.outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
387
|
-
} else if (base === Op.GT && value === domain.max) {
|
|
388
|
-
issues.push(ValidationIssue.impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
389
|
-
} else if (base === Op.LT && value === domain.min) {
|
|
390
|
-
issues.push(ValidationIssue.impossibleLt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
let changedEq = false;
|
|
394
|
-
let changedLower = false;
|
|
395
|
-
let changedUpper = false;
|
|
396
|
-
|
|
397
|
-
// Equality handling.
|
|
398
|
-
if (base === Op.EQ) {
|
|
399
|
-
if (!domain.hasEq || domain.eq !== value) changedEq = true;
|
|
400
|
-
if (domain.hasEq) {
|
|
401
|
-
if (domain.eq !== value) {
|
|
402
|
-
issues.push(
|
|
403
|
-
ValidationIssue.conflictingEquality(
|
|
404
|
-
isLength,
|
|
405
|
-
groupIndex,
|
|
406
|
-
constraintIndex,
|
|
407
|
-
bigintToHex(domain.eq),
|
|
408
|
-
bigintToHex(value),
|
|
409
|
-
),
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
for (let j = 0; j < domain.holes.length; j++) {
|
|
414
|
-
if (domain.holes[j] === value) {
|
|
415
|
-
issues.push(ValidationIssue.eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
domain.hasEq = true;
|
|
419
|
-
domain.eq = value;
|
|
420
|
-
} else if (base === Op.GT || base === Op.GTE) {
|
|
421
|
-
const inclusive = base === Op.GTE;
|
|
422
|
-
if (domain.hasLower) {
|
|
423
|
-
let redundant = false;
|
|
424
|
-
let strictlyBetter = false;
|
|
277
|
+
/** Return the word a key of a numeric or length subject loads from. */
|
|
278
|
+
function wordOf(domain: Domain, key: bigint): bigint {
|
|
279
|
+
if (domain.flip === 0n) return key;
|
|
280
|
+
return BigInt.asUintN(256, BigInt.asIntN(domain.width, key ^ domain.flip));
|
|
281
|
+
}
|
|
425
282
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
} else {
|
|
436
|
-
strictlyBetter = true;
|
|
437
|
-
}
|
|
283
|
+
/** Return the relations to a context property's value a comparison admits. */
|
|
284
|
+
function relationsOf(comparison: number): number {
|
|
285
|
+
if (comparison === Op.EQ) return EQUAL;
|
|
286
|
+
if (comparison === Op.GT) return GREATER;
|
|
287
|
+
if (comparison === Op.LT) return LESS;
|
|
288
|
+
if (comparison === Op.GTE) return GREATER | EQUAL;
|
|
289
|
+
return LESS | EQUAL;
|
|
290
|
+
}
|
|
438
291
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
292
|
+
/** Return the range literal of the keys from `lower` to `upper`. */
|
|
293
|
+
function range(lower: bigint, upper: bigint): Literal {
|
|
294
|
+
return { kind: "range", negated: false, lower, upper };
|
|
295
|
+
}
|
|
442
296
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Return the literal of an operator on a subject, and the sides it bounds. `subjectProperty` is the context property a
|
|
299
|
+
* context test compares, which a context operand naming it equals.
|
|
300
|
+
*/
|
|
301
|
+
function literalOf(
|
|
302
|
+
domain: Domain,
|
|
303
|
+
operator: Hex,
|
|
304
|
+
subjectProperty: number | undefined,
|
|
305
|
+
): { literal: Literal; sides: number } {
|
|
306
|
+
const bytes = hexToBytes(operator);
|
|
307
|
+
const opCode = bytes[0]!;
|
|
308
|
+
const negated = (opCode & Op.NOT) !== 0;
|
|
309
|
+
const comparison = comparisonOf(opCode);
|
|
310
|
+
const key = (index: number) => patternOf(domain, toBigInt(bytes, 1 + 32 * index)) ^ domain.flip;
|
|
311
|
+
let literal: Literal;
|
|
312
|
+
let sides = 0;
|
|
313
|
+
|
|
314
|
+
if (sourceOf(opCode) === Source.CONTEXT) {
|
|
315
|
+
const property = Number(toBigInt(bytes, 1));
|
|
316
|
+
const relations = relationsOf(comparison);
|
|
317
|
+
if (property === subjectProperty) {
|
|
318
|
+
literal = (relations & EQUAL) !== 0 ? range(0n, domain.max) : range(1n, 0n);
|
|
449
319
|
} else {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
320
|
+
literal = { kind: "relation", negated: false, property, relations };
|
|
321
|
+
}
|
|
322
|
+
} else if (comparison === Op.EQ || comparison === Op.LENGTH_EQ) {
|
|
323
|
+
literal = { kind: "set", negated: false, members: [key(0)] };
|
|
324
|
+
} else if (comparison === Op.IN) {
|
|
325
|
+
const members: bigint[] = [];
|
|
326
|
+
for (let index = 0; 1 + 32 * index < bytes.length; index++) members.push(key(index));
|
|
327
|
+
literal = { kind: "set", negated: false, members };
|
|
328
|
+
} else if (comparison === Op.GT || comparison === Op.LENGTH_GT) {
|
|
329
|
+
literal = range(key(0) + 1n, domain.max);
|
|
330
|
+
sides = LOWER_SIDE;
|
|
331
|
+
} else if (comparison === Op.LT || comparison === Op.LENGTH_LT) {
|
|
332
|
+
literal = range(0n, key(0) - 1n);
|
|
333
|
+
sides = UPPER_SIDE;
|
|
334
|
+
} else if (comparison === Op.GTE || comparison === Op.LENGTH_GTE) {
|
|
335
|
+
literal = range(key(0), domain.max);
|
|
336
|
+
sides = LOWER_SIDE;
|
|
337
|
+
} else if (comparison === Op.LTE || comparison === Op.LENGTH_LTE) {
|
|
338
|
+
literal = range(0n, key(0));
|
|
339
|
+
sides = UPPER_SIDE;
|
|
340
|
+
} else if (comparison === Op.BETWEEN || comparison === Op.LENGTH_BETWEEN) {
|
|
341
|
+
literal = range(key(0), key(1));
|
|
342
|
+
sides = LOWER_SIDE | UPPER_SIDE;
|
|
343
|
+
} else {
|
|
344
|
+
// A mask's top bit asks the opposite of a signed key's top bit.
|
|
345
|
+
const mask = patternOf(domain, toBigInt(bytes, 1));
|
|
346
|
+
const set = mask & (domain.max ^ domain.flip);
|
|
347
|
+
const flipped = mask & domain.flip;
|
|
348
|
+
literal =
|
|
349
|
+
comparison === Op.BITMASK_ALL
|
|
350
|
+
? { kind: "bits", negated: false, ones: set, zeros: flipped }
|
|
351
|
+
: { kind: "bits", negated: comparison === Op.BITMASK_ANY, ones: flipped, zeros: set };
|
|
352
|
+
}
|
|
353
|
+
return { literal: { ...literal, negated: literal.negated !== negated }, sides: negated ? 0 : sides };
|
|
354
|
+
}
|
|
473
355
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
356
|
+
/** Return true when the literal at `index` is negated in a conjunction. */
|
|
357
|
+
function isNegated(conjunction: Conjunction, index: number): boolean {
|
|
358
|
+
return conjunction.literals[index]!.negated !== (index === conjunction.flipped);
|
|
359
|
+
}
|
|
477
360
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Return true when some key and some context meet the first `count` literals, the literal at `flipped` negated. A
|
|
363
|
+
* context property each literal reads is present.
|
|
364
|
+
*/
|
|
365
|
+
function satisfiable(domain: Domain, literals: readonly Literal[], count: number, flipped: number): boolean {
|
|
366
|
+
const conjunction: Conjunction = { literals, count, flipped, lower: 0n, upper: domain.max, ones: 0n, zeros: 0n };
|
|
367
|
+
const relations = Array.from({ length: MAX_CONTEXT_PROPERTY_ID + 1 }, () => ANY_RELATION);
|
|
368
|
+
let firstSet: Extract<Literal, { kind: "set" }> | undefined;
|
|
369
|
+
for (let index = 0; index < count; index++) {
|
|
370
|
+
const literal = literals[index]!;
|
|
371
|
+
const negated = isNegated(conjunction, index);
|
|
372
|
+
if (literal.kind === "relation") {
|
|
373
|
+
relations[literal.property]! &= negated ? ANY_RELATION ^ literal.relations : literal.relations;
|
|
374
|
+
} else if (negated) {
|
|
375
|
+
continue;
|
|
376
|
+
} else if (literal.kind === "range") {
|
|
377
|
+
if (literal.lower > conjunction.lower) conjunction.lower = literal.lower;
|
|
378
|
+
if (literal.upper < conjunction.upper) conjunction.upper = literal.upper;
|
|
379
|
+
} else if (literal.kind === "bits") {
|
|
380
|
+
conjunction.ones |= literal.ones;
|
|
381
|
+
conjunction.zeros |= literal.zeros;
|
|
484
382
|
} else {
|
|
485
|
-
|
|
486
|
-
domain.upper = value;
|
|
487
|
-
domain.upperInclusive = inclusive;
|
|
488
|
-
changedUpper = true;
|
|
383
|
+
firstSet ??= literal;
|
|
489
384
|
}
|
|
490
385
|
}
|
|
491
386
|
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (
|
|
499
|
-
issues.push(
|
|
500
|
-
ValidationIssue.boundsExcludeEquality(
|
|
501
|
-
isLength,
|
|
502
|
-
groupIndex,
|
|
503
|
-
constraintIndex,
|
|
504
|
-
bigintToHex(domain.eq),
|
|
505
|
-
bigintToHex(domain.lower),
|
|
506
|
-
),
|
|
507
|
-
);
|
|
508
|
-
} else {
|
|
509
|
-
issues.push(
|
|
510
|
-
ValidationIssue.redundantBound(
|
|
511
|
-
isLength,
|
|
512
|
-
groupIndex,
|
|
513
|
-
constraintIndex,
|
|
514
|
-
bigintToHex(domain.lower),
|
|
515
|
-
bigintToHex(domain.eq),
|
|
516
|
-
),
|
|
517
|
-
);
|
|
518
|
-
}
|
|
387
|
+
// A context property's value lies anywhere in its type, so the relations left to it bound the subject's key.
|
|
388
|
+
for (let property = 0; property <= MAX_CONTEXT_PROPERTY_ID; property++) {
|
|
389
|
+
const held = relations[property]!;
|
|
390
|
+
if (held === 0) return false;
|
|
391
|
+
if (held === LESS) {
|
|
392
|
+
const propertyMax = domainOf(contextPropertyType(property)).max;
|
|
393
|
+
if (propertyMax - 1n < conjunction.upper) conjunction.upper = propertyMax - 1n;
|
|
519
394
|
}
|
|
395
|
+
if (held === GREATER && conjunction.lower < 1n) conjunction.lower = 1n;
|
|
520
396
|
}
|
|
397
|
+
if ((conjunction.ones & conjunction.zeros) !== 0n || conjunction.lower > conjunction.upper) return false;
|
|
521
398
|
|
|
522
|
-
|
|
523
|
-
if (changedEq || changedUpper) {
|
|
524
|
-
if (domain.hasEq && domain.hasUpper) {
|
|
525
|
-
const contradiction = domain.upperInclusive
|
|
526
|
-
? signedCompare(domain.eq, domain.upper, domain.isSigned) > 0n
|
|
527
|
-
: signedCompare(domain.eq, domain.upper, domain.isSigned) >= 0n;
|
|
528
|
-
if (contradiction) {
|
|
529
|
-
issues.push(
|
|
530
|
-
ValidationIssue.boundsExcludeEquality(
|
|
531
|
-
isLength,
|
|
532
|
-
groupIndex,
|
|
533
|
-
constraintIndex,
|
|
534
|
-
bigintToHex(domain.eq),
|
|
535
|
-
bigintToHex(domain.upper),
|
|
536
|
-
),
|
|
537
|
-
);
|
|
538
|
-
} else {
|
|
539
|
-
issues.push(
|
|
540
|
-
ValidationIssue.redundantBound(
|
|
541
|
-
isLength,
|
|
542
|
-
groupIndex,
|
|
543
|
-
constraintIndex,
|
|
544
|
-
bigintToHex(domain.upper),
|
|
545
|
-
bigintToHex(domain.eq),
|
|
546
|
-
),
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
399
|
+
if (firstSet !== undefined) return firstSet.members.some((key) => admits(conjunction, key));
|
|
551
400
|
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
issues.push(
|
|
560
|
-
ValidationIssue.impossibleRange(
|
|
561
|
-
isLength,
|
|
562
|
-
groupIndex,
|
|
563
|
-
constraintIndex,
|
|
564
|
-
bigintToHex(domain.lower),
|
|
565
|
-
bigintToHex(domain.upper),
|
|
566
|
-
),
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
401
|
+
// Each step passes one hole or one excluded key, so the walk takes at most one step per literal member.
|
|
402
|
+
let key = nextKey(conjunction, domain.max, conjunction.lower);
|
|
403
|
+
while (key !== undefined && key <= conjunction.upper) {
|
|
404
|
+
const end = excludedTo(conjunction, key);
|
|
405
|
+
if (end === undefined) return true;
|
|
406
|
+
if (end >= conjunction.upper) return false;
|
|
407
|
+
key = nextKey(conjunction, domain.max, end + 1n);
|
|
570
408
|
}
|
|
409
|
+
return false;
|
|
571
410
|
}
|
|
572
411
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
ctx: ConstraintContext,
|
|
580
|
-
base: number,
|
|
581
|
-
isNegated: boolean,
|
|
582
|
-
mask: bigint,
|
|
583
|
-
groupIndex: number,
|
|
584
|
-
constraintIndex: number,
|
|
585
|
-
issues: Issue[],
|
|
586
|
-
): void {
|
|
587
|
-
if (isNegated) return;
|
|
588
|
-
|
|
589
|
-
if (base === Op.BITMASK_ALL) {
|
|
590
|
-
if ((ctx.bitmask.mustBeZero & mask) !== 0n) {
|
|
591
|
-
issues.push(
|
|
592
|
-
ValidationIssue.bitmaskContradiction(
|
|
593
|
-
groupIndex,
|
|
594
|
-
constraintIndex,
|
|
595
|
-
bigintToHex(mask),
|
|
596
|
-
bigintToHex(ctx.bitmask.mustBeZero),
|
|
597
|
-
),
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
if ((ctx.bitmask.mustBeOne & mask) === mask) {
|
|
601
|
-
issues.push(
|
|
602
|
-
ValidationIssue.redundantBitmask(
|
|
603
|
-
groupIndex,
|
|
604
|
-
constraintIndex,
|
|
605
|
-
bigintToHex(mask),
|
|
606
|
-
bigintToHex(ctx.bitmask.mustBeOne),
|
|
607
|
-
),
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
ctx.bitmask.mustBeOne |= mask;
|
|
611
|
-
} else if (base === Op.BITMASK_NONE) {
|
|
612
|
-
if ((ctx.bitmask.mustBeOne & mask) !== 0n) {
|
|
613
|
-
issues.push(
|
|
614
|
-
ValidationIssue.bitmaskContradiction(
|
|
615
|
-
groupIndex,
|
|
616
|
-
constraintIndex,
|
|
617
|
-
bigintToHex(mask),
|
|
618
|
-
bigintToHex(ctx.bitmask.mustBeOne),
|
|
619
|
-
),
|
|
620
|
-
);
|
|
621
|
-
}
|
|
622
|
-
if ((ctx.bitmask.mustBeZero & mask) === mask) {
|
|
623
|
-
issues.push(
|
|
624
|
-
ValidationIssue.redundantBitmask(
|
|
625
|
-
groupIndex,
|
|
626
|
-
constraintIndex,
|
|
627
|
-
bigintToHex(mask),
|
|
628
|
-
bigintToHex(ctx.bitmask.mustBeZero),
|
|
629
|
-
),
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
ctx.bitmask.mustBeZero |= mask;
|
|
633
|
-
} else if (base === Op.BITMASK_ANY) {
|
|
634
|
-
if (mask !== 0n && (ctx.bitmask.mustBeZero & mask) === mask) {
|
|
635
|
-
issues.push(
|
|
636
|
-
ValidationIssue.bitmaskAnyImpossible(
|
|
637
|
-
groupIndex,
|
|
638
|
-
constraintIndex,
|
|
639
|
-
bigintToHex(mask),
|
|
640
|
-
bigintToHex(ctx.bitmask.mustBeZero),
|
|
641
|
-
),
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
if ((ctx.bitmask.mustBeOne & mask) !== 0n) {
|
|
645
|
-
issues.push(
|
|
646
|
-
ValidationIssue.redundantBitmask(
|
|
647
|
-
groupIndex,
|
|
648
|
-
constraintIndex,
|
|
649
|
-
bigintToHex(mask),
|
|
650
|
-
bigintToHex(ctx.bitmask.mustBeOne),
|
|
651
|
-
),
|
|
652
|
-
);
|
|
653
|
-
}
|
|
412
|
+
/** Return true when `key` meets every literal of a conjunction. */
|
|
413
|
+
function admits(conjunction: Conjunction, key: bigint): boolean {
|
|
414
|
+
if (key < conjunction.lower || key > conjunction.upper || !meetsBits(conjunction, key)) return false;
|
|
415
|
+
for (let index = 0; index < conjunction.count; index++) {
|
|
416
|
+
const literal = conjunction.literals[index]!;
|
|
417
|
+
if (literal.kind === "set" && literal.members.includes(key) === isNegated(conjunction, index)) return false;
|
|
654
418
|
}
|
|
419
|
+
return excludedTo(conjunction, key) === undefined;
|
|
655
420
|
}
|
|
656
421
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
for (let i = 0; i < inCount; i++) {
|
|
668
|
-
const value = ctx.set.inValues[i]!;
|
|
669
|
-
let forbidden = false;
|
|
670
|
-
for (let k = 0; k < ctx.numeric.holes.length; k++) {
|
|
671
|
-
if (ctx.numeric.holes[k] === value) {
|
|
672
|
-
forbidden = true;
|
|
673
|
-
break;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
if (!forbidden) {
|
|
677
|
-
for (let k = 0; k < ctx.set.notInValues.length; k++) {
|
|
678
|
-
if (ctx.set.notInValues[k] === value) {
|
|
679
|
-
forbidden = true;
|
|
680
|
-
break;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
if (!forbidden) possibleCount++;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if (possibleCount === 0) {
|
|
688
|
-
issues.push(ValidationIssue.setFullyExcluded(groupIndex, constraintIndex));
|
|
689
|
-
} else if (possibleCount < inCount) {
|
|
690
|
-
issues.push(
|
|
691
|
-
ValidationIssue.setPartiallyExcluded(groupIndex, constraintIndex, bigintToHex(BigInt(inCount - possibleCount))),
|
|
692
|
-
);
|
|
422
|
+
/**
|
|
423
|
+
* Return the greatest key the negated range or set of a conjunction that contains `key` excludes from `key` on, or
|
|
424
|
+
* undefined when none contains it.
|
|
425
|
+
*/
|
|
426
|
+
function excludedTo(conjunction: Conjunction, key: bigint): bigint | undefined {
|
|
427
|
+
for (let index = 0; index < conjunction.count; index++) {
|
|
428
|
+
const literal = conjunction.literals[index]!;
|
|
429
|
+
if (!isNegated(conjunction, index)) continue;
|
|
430
|
+
if (literal.kind === "range" && literal.lower <= key && key <= literal.upper) return literal.upper;
|
|
431
|
+
if (literal.kind === "set" && literal.members.includes(key)) return key;
|
|
693
432
|
}
|
|
433
|
+
return undefined;
|
|
694
434
|
}
|
|
695
435
|
|
|
696
|
-
/**
|
|
697
|
-
function
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
issues: Issue[],
|
|
704
|
-
): void {
|
|
705
|
-
// Physical bounds: a member outside the type's domain never occurs, so it matches and excludes nothing.
|
|
706
|
-
for (const value of values) {
|
|
707
|
-
if (isOutsideDomain(ctx.numeric, value)) {
|
|
708
|
-
issues.push(ValidationIssue.outOfPhysicalBounds(false, groupIndex, constraintIndex, bigintToHex(value)));
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
if (isNegated) {
|
|
713
|
-
for (const value of values) {
|
|
714
|
-
if (ctx.numeric.hasEq && ctx.numeric.eq === value) {
|
|
715
|
-
issues.push(ValidationIssue.setExcludesEquality(groupIndex, constraintIndex, bigintToHex(value)));
|
|
716
|
-
}
|
|
717
|
-
if (ctx.set.hasIn) {
|
|
718
|
-
let inSet = false;
|
|
719
|
-
for (const existing of ctx.set.inValues) {
|
|
720
|
-
if (existing === value) {
|
|
721
|
-
inSet = true;
|
|
722
|
-
break;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
if (inSet) {
|
|
726
|
-
issues.push(ValidationIssue.setReduction(groupIndex, constraintIndex, bigintToHex(value)));
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
ctx.set.notInValues.push(value);
|
|
730
|
-
}
|
|
731
|
-
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
732
|
-
} else {
|
|
733
|
-
if (ctx.numeric.hasEq) {
|
|
734
|
-
let found = false;
|
|
735
|
-
for (const candidate of values) {
|
|
736
|
-
if (candidate === ctx.numeric.eq) {
|
|
737
|
-
found = true;
|
|
738
|
-
break;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
if (!found) {
|
|
742
|
-
issues.push(ValidationIssue.setExcludesEquality(groupIndex, constraintIndex, bigintToHex(ctx.numeric.eq)));
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
if (ctx.set.hasIn) {
|
|
747
|
-
const intersection: bigint[] = [];
|
|
748
|
-
for (const existing of ctx.set.inValues) {
|
|
749
|
-
for (const candidate of values) {
|
|
750
|
-
if (existing === candidate) {
|
|
751
|
-
intersection.push(existing);
|
|
752
|
-
break;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
if (intersection.length === 0) {
|
|
758
|
-
issues.push(ValidationIssue.emptySetIntersection(groupIndex, constraintIndex));
|
|
759
|
-
} else if (intersection.length < values.length || intersection.length < ctx.set.inValues.length) {
|
|
760
|
-
issues.push(
|
|
761
|
-
ValidationIssue.setRedundancy(groupIndex, constraintIndex, bigintToHex(BigInt(intersection.length))),
|
|
762
|
-
);
|
|
763
|
-
}
|
|
764
|
-
ctx.set.inValues = intersection;
|
|
765
|
-
} else {
|
|
766
|
-
ctx.set.hasIn = true;
|
|
767
|
-
ctx.set.inValues = values;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
436
|
+
/** Return true when `key` meets the fixed bits and every negated bits literal of a conjunction. */
|
|
437
|
+
function meetsBits(conjunction: Conjunction, key: bigint): boolean {
|
|
438
|
+
if ((key & conjunction.ones) !== conjunction.ones || (key & conjunction.zeros) !== 0n) return false;
|
|
439
|
+
for (let index = 0; index < conjunction.count; index++) {
|
|
440
|
+
const literal = conjunction.literals[index]!;
|
|
441
|
+
if (literal.kind !== "bits" || !isNegated(conjunction, index)) continue;
|
|
442
|
+
if ((key & literal.ones) === literal.ones && (key & literal.zeros) === 0n) return false;
|
|
771
443
|
}
|
|
444
|
+
return true;
|
|
772
445
|
}
|
|
773
446
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
function
|
|
780
|
-
|
|
781
|
-
for (
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
447
|
+
/**
|
|
448
|
+
* Return the least key from `from` up that meets the fixed bits and every negated bits literal. If `from` meets these
|
|
449
|
+
* conditions, the key is `from`. Otherwise the key sets the lowest clear bit of `from` that can be set and keeps the
|
|
450
|
+
* bits of `from` above that bit. The key completes the bits below that bit, least first.
|
|
451
|
+
*/
|
|
452
|
+
function nextKey(conjunction: Conjunction, max: bigint, from: bigint): bigint | undefined {
|
|
453
|
+
if (meetsBits(conjunction, from)) return from;
|
|
454
|
+
for (let bit = 1n; bit <= max; bit <<= 1n) {
|
|
455
|
+
if ((from & bit) !== 0n) continue;
|
|
456
|
+
let fixedBits = max ^ (bit - 1n);
|
|
457
|
+
let key = (from & fixedBits) | bit;
|
|
458
|
+
if (!completable(conjunction, max, fixedBits, key)) continue;
|
|
459
|
+
for (let lowBit = bit >> 1n; lowBit !== 0n; lowBit >>= 1n) {
|
|
460
|
+
fixedBits |= lowBit;
|
|
461
|
+
if (!completable(conjunction, max, fixedBits, key)) key |= lowBit;
|
|
462
|
+
}
|
|
463
|
+
return key;
|
|
788
464
|
}
|
|
465
|
+
return undefined;
|
|
789
466
|
}
|
|
790
467
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Return true when some key whose bits in `fixedBits` are those of `key` meets the fixed bits and every negated bits
|
|
470
|
+
* literal.
|
|
471
|
+
*/
|
|
472
|
+
function completable(conjunction: Conjunction, max: bigint, fixedBits: bigint, key: bigint): boolean {
|
|
473
|
+
if ((key & conjunction.zeros) !== 0n || (conjunction.ones & fixedBits & ~key) !== 0n) return false;
|
|
474
|
+
const known = fixedBits | conjunction.ones | conjunction.zeros;
|
|
475
|
+
return solvable(conjunction, max, known, key | conjunction.ones);
|
|
476
|
+
}
|
|
794
477
|
|
|
795
478
|
/**
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
479
|
+
* Return true when some assignment of the bits outside `known` meets every negated bits literal, with the known bits as
|
|
480
|
+
* in `value`. Propagates the literals left with one free bit. Then branches on the free bits of the first literal left
|
|
481
|
+
* open. Each branch meets that literal, so the recursion runs at most one level per negated bits literal.
|
|
799
482
|
*/
|
|
800
|
-
function
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
bigintToHex(numericPair.high),
|
|
820
|
-
),
|
|
821
|
-
);
|
|
483
|
+
function solvable(conjunction: Conjunction, max: bigint, known: bigint, value: bigint): boolean {
|
|
484
|
+
let openOnes = 0n;
|
|
485
|
+
let openZeros = 0n;
|
|
486
|
+
let index = 0;
|
|
487
|
+
while (index < conjunction.count) {
|
|
488
|
+
const literal = conjunction.literals[index++]!;
|
|
489
|
+
if (literal.kind !== "bits" || !isNegated(conjunction, index - 1)) continue;
|
|
490
|
+
if ((literal.ones & known & ~value) !== 0n || (literal.zeros & known & value) !== 0n) continue;
|
|
491
|
+
const ones = literal.ones & max & ~known;
|
|
492
|
+
const zeros = literal.zeros & max & ~known;
|
|
493
|
+
const free = ones | zeros;
|
|
494
|
+
if (free === 0n) return false;
|
|
495
|
+
if ((free & (free - 1n)) === 0n) {
|
|
496
|
+
known |= free;
|
|
497
|
+
value = ones === 0n ? value | free : value & ~free;
|
|
498
|
+
[openOnes, openZeros, index] = [0n, 0n, 0];
|
|
499
|
+
} else if ((openOnes | openZeros) === 0n) {
|
|
500
|
+
[openOnes, openZeros] = [ones, zeros];
|
|
501
|
+
}
|
|
822
502
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
),
|
|
834
|
-
);
|
|
503
|
+
if ((openOnes | openZeros) === 0n) return true;
|
|
504
|
+
|
|
505
|
+
// Each branch clears one free bit the literal wants set, or sets one it wants clear; a failed branch's bit then takes
|
|
506
|
+
// the other value.
|
|
507
|
+
for (let free = openOnes | openZeros; free !== 0n; free &= free - 1n) {
|
|
508
|
+
const bit = free & -free;
|
|
509
|
+
const meeting = (openOnes & bit) !== 0n ? value & ~bit : value | bit;
|
|
510
|
+
if (solvable(conjunction, max, known | bit, meeting)) return true;
|
|
511
|
+
known |= bit;
|
|
512
|
+
value = meeting ^ bit;
|
|
835
513
|
}
|
|
514
|
+
return false;
|
|
836
515
|
}
|
|
837
516
|
|
|
838
|
-
/**
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
function
|
|
845
|
-
|
|
846
|
-
let
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
for (const opHex of operators) {
|
|
851
|
-
const opCode = parseInt(opHex.slice(2, 4), 16);
|
|
852
|
-
const dataLength = (opHex.length - 4) / 2;
|
|
853
|
-
if (!hasValidPayload(opCode & ~Op.NOT, dataLength)) continue;
|
|
854
|
-
if (opCode === lowerOp) {
|
|
855
|
-
lowerCount++;
|
|
856
|
-
low = readValue(opHex);
|
|
857
|
-
} else if (opCode === upperOp) {
|
|
858
|
-
upperCount++;
|
|
859
|
-
high = readValue(opHex);
|
|
860
|
-
}
|
|
517
|
+
/**
|
|
518
|
+
* Push the PV-9 issues of the rules on one subject, in normal-form order. If the rules stop being satisfiable
|
|
519
|
+
* together, push one issue for the rule at which they stop, and nothing else. Otherwise, push an issue for each rule
|
|
520
|
+
* that holds for every value and context, and for each other rule the others imply. Then, among the rules not yet
|
|
521
|
+
* named, push an issue for each constant bound looser than the tightest on its side.
|
|
522
|
+
*/
|
|
523
|
+
function checkSubject(domain: Domain, isLength: boolean, rules: readonly SubjectRule[], issues: Issue[]): void {
|
|
524
|
+
const literals = rules.map((rule) => rule.literal);
|
|
525
|
+
for (let count = 1; count <= literals.length; count++) {
|
|
526
|
+
if (satisfiable(domain, literals, count, NO_LITERAL)) continue;
|
|
527
|
+
issues.push(ValidationIssue.contradictoryRule(rules[count - 1]!.location));
|
|
528
|
+
return;
|
|
861
529
|
}
|
|
862
530
|
|
|
863
|
-
|
|
531
|
+
const named = rules.map((rule, index) => {
|
|
532
|
+
if (rule.reads === 0 && !satisfiable(domain, [rule.literal], 1, 0)) {
|
|
533
|
+
issues.push(ValidationIssue.vacuousRule(rule.location));
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
let othersRead = 0;
|
|
537
|
+
rules.forEach((other, otherIndex) => {
|
|
538
|
+
if (otherIndex !== index) othersRead |= other.reads;
|
|
539
|
+
});
|
|
540
|
+
if ((rule.reads & ~othersRead) !== 0 || satisfiable(domain, literals, literals.length, index)) return false;
|
|
541
|
+
issues.push(ValidationIssue.redundantRule(rule.location));
|
|
542
|
+
return true;
|
|
543
|
+
});
|
|
544
|
+
checkBounds(domain, isLength, rules, named, issues);
|
|
864
545
|
}
|
|
865
546
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
constraintIndex: number,
|
|
547
|
+
/**
|
|
548
|
+
* Push an issue for each constant bound of a rule not in `named` looser than the tightest on its side among those
|
|
549
|
+
* rules.
|
|
550
|
+
*/
|
|
551
|
+
function checkBounds(
|
|
552
|
+
domain: Domain,
|
|
553
|
+
isLength: boolean,
|
|
554
|
+
rules: readonly SubjectRule[],
|
|
555
|
+
named: readonly boolean[],
|
|
876
556
|
issues: Issue[],
|
|
877
557
|
): void {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
if (
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
constraintIndex,
|
|
896
|
-
bigintToHex(BigInt(opCode)),
|
|
897
|
-
),
|
|
898
|
-
);
|
|
899
|
-
continue;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const isNegated = (opCode & Op.NOT) !== 0;
|
|
903
|
-
|
|
904
|
-
// A negated operator under any() is satisfied by a single decoy element.
|
|
905
|
-
if (underAny && isNegated) {
|
|
906
|
-
issues.push(ValidationIssue.negationUnderAny(groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
const incompatibility = checkCompatibility(base, ctx.typeInfo);
|
|
910
|
-
if (incompatibility) {
|
|
911
|
-
issues.push(
|
|
912
|
-
ValidationIssue.fromOpRule(
|
|
913
|
-
incompatibility.code,
|
|
914
|
-
incompatibility.message,
|
|
915
|
-
groupIndex,
|
|
916
|
-
constraintIndex,
|
|
917
|
-
bigintToHex(BigInt(opCode)),
|
|
918
|
-
),
|
|
919
|
-
);
|
|
920
|
-
continue;
|
|
558
|
+
let tightestLower = 0n;
|
|
559
|
+
let tightestUpper = domain.max;
|
|
560
|
+
rules.forEach(({ literal, sides }, index) => {
|
|
561
|
+
if (named[index] || literal.kind !== "range") return;
|
|
562
|
+
if ((sides & LOWER_SIDE) !== 0 && literal.lower > tightestLower) tightestLower = literal.lower;
|
|
563
|
+
if ((sides & UPPER_SIDE) !== 0 && literal.upper < tightestUpper) tightestUpper = literal.upper;
|
|
564
|
+
});
|
|
565
|
+
rules.forEach(({ location, literal, sides }, index) => {
|
|
566
|
+
if (named[index] || literal.kind !== "range") return;
|
|
567
|
+
let bound: bigint;
|
|
568
|
+
let tightest: bigint;
|
|
569
|
+
if ((sides & LOWER_SIDE) !== 0 && literal.lower < tightestLower) {
|
|
570
|
+
[bound, tightest] = [literal.lower, tightestLower];
|
|
571
|
+
} else if ((sides & UPPER_SIDE) !== 0 && literal.upper > tightestUpper) {
|
|
572
|
+
[bound, tightest] = [literal.upper, tightestUpper];
|
|
573
|
+
} else {
|
|
574
|
+
return;
|
|
921
575
|
}
|
|
576
|
+
const [boundWord, tightestWord] = [bigintToHex(wordOf(domain, bound)), bigintToHex(wordOf(domain, tightest))];
|
|
577
|
+
issues.push(ValidationIssue.dominatedBound(isLength, location, boundWord, tightestWord));
|
|
578
|
+
});
|
|
579
|
+
}
|
|
922
580
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
const propertyIsAddress = property.typeCode === TypeCode.ADDRESS;
|
|
940
|
-
const targetIsAddress = ctx.typeInfo.typeCode === TypeCode.ADDRESS;
|
|
941
|
-
if (propertyIsAddress !== targetIsAddress) {
|
|
942
|
-
issues.push(
|
|
943
|
-
ValidationIssue.contextTypeMismatch(
|
|
944
|
-
groupIndex,
|
|
945
|
-
constraintIndex,
|
|
946
|
-
bigintToHex(ctxOperand),
|
|
947
|
-
bigintToHex(BigInt(ctx.typeInfo.typeCode)),
|
|
948
|
-
),
|
|
949
|
-
);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
} else if (isValueOp(base)) {
|
|
953
|
-
// An operand that breaks PC-1 never matches, since enforcement only admits canonical values, so the rule is
|
|
954
|
-
// unsatisfiable or vacuous; skip it. Numeric, address and bool operands are already covered by the domain limits.
|
|
955
|
-
const typeCode = ctx.typeInfo.typeCode;
|
|
956
|
-
if (isLeftAligned(typeCode)) {
|
|
957
|
-
const nonCanonical = findNonCanonicalWord(opHex, typeCode);
|
|
958
|
-
if (nonCanonical !== null) {
|
|
959
|
-
issues.push(
|
|
960
|
-
ValidationIssue.nonCanonicalOperand(
|
|
961
|
-
groupIndex,
|
|
962
|
-
constraintIndex,
|
|
963
|
-
bigintToHex(nonCanonical.word),
|
|
964
|
-
bigintToHex(nonCanonical.canonical),
|
|
965
|
-
),
|
|
966
|
-
);
|
|
967
|
-
continue;
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
if (base >= Op.EQ && base <= Op.BETWEEN) {
|
|
972
|
-
if (base === Op.BETWEEN) {
|
|
973
|
-
// A negated range is a disjunction (value < low OR value > high); distributing the negation over the
|
|
974
|
-
// decomposed bounds would conjoin the complement halves into a spurious contradiction. Leave it un-analyzed,
|
|
975
|
-
// like a neq hole. Inverted bounds still surface: negating an empty range excludes nothing.
|
|
976
|
-
const { low, high } = readPair(opHex);
|
|
977
|
-
if (!isNegated) {
|
|
978
|
-
updateBound(ctx.numeric, Op.GTE, false, low, false, groupIndex, constraintIndex, issues);
|
|
979
|
-
updateBound(ctx.numeric, Op.LTE, false, high, false, groupIndex, constraintIndex, issues);
|
|
980
|
-
} else if (signedCompare(low, high, ctx.numeric.isSigned) > 0n) {
|
|
981
|
-
issues.push(
|
|
982
|
-
ValidationIssue.vacuousNegatedRange(
|
|
983
|
-
false,
|
|
984
|
-
groupIndex,
|
|
985
|
-
constraintIndex,
|
|
986
|
-
bigintToHex(low),
|
|
987
|
-
bigintToHex(high),
|
|
988
|
-
),
|
|
989
|
-
);
|
|
990
|
-
}
|
|
991
|
-
} else {
|
|
992
|
-
const value = readValue(opHex);
|
|
993
|
-
const holesBefore = ctx.numeric.holes.length;
|
|
994
|
-
updateBound(ctx.numeric, base, isNegated, value, false, groupIndex, constraintIndex, issues);
|
|
995
|
-
if (ctx.numeric.holes.length > holesBefore) {
|
|
996
|
-
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
} else if (isBitmaskOp(base)) {
|
|
1000
|
-
const mask = readValue(opHex);
|
|
1001
|
-
updateBitmask(ctx, base, isNegated, mask, groupIndex, constraintIndex, issues);
|
|
1002
|
-
} else if (base === Op.IN) {
|
|
1003
|
-
const values = unpackSet(opHex);
|
|
1004
|
-
if (!isStrictlyAscending(values)) {
|
|
1005
|
-
issues.push(ValidationIssue.unsortedInSet(groupIndex, constraintIndex));
|
|
1006
|
-
} else {
|
|
1007
|
-
updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
} else if (isLengthOp(base)) {
|
|
1011
|
-
if (base === Op.LENGTH_BETWEEN) {
|
|
1012
|
-
// Same disjunction reasoning as the negated value-range above.
|
|
1013
|
-
const { low, high } = readPair(opHex);
|
|
1014
|
-
if (!isNegated) {
|
|
1015
|
-
updateBound(ctx.length, Op.GTE, false, low, true, groupIndex, constraintIndex, issues);
|
|
1016
|
-
updateBound(ctx.length, Op.LTE, false, high, true, groupIndex, constraintIndex, issues);
|
|
1017
|
-
} else if (low > high) {
|
|
1018
|
-
issues.push(
|
|
1019
|
-
ValidationIssue.vacuousNegatedRange(true, groupIndex, constraintIndex, bigintToHex(low), bigintToHex(high)),
|
|
1020
|
-
);
|
|
1021
|
-
}
|
|
1022
|
-
} else {
|
|
1023
|
-
const value = readValue(opHex);
|
|
1024
|
-
updateBound(ctx.length, normalizeLengthOp(base), isNegated, value, true, groupIndex, constraintIndex, issues);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
581
|
+
/** Push the PV-9 issues of the tests of a group or a loop body, subject by subject in order of first appearance. */
|
|
582
|
+
function checkSubjects(tests: readonly { test: BodyRule; location: RuleLocation }[], issues: Issue[]): void {
|
|
583
|
+
const subjects = new Map<string, { test: BodyRule; location: RuleLocation }[]>();
|
|
584
|
+
for (const entry of tests) {
|
|
585
|
+
const subject = `${entry.test.scope}:${entry.test.path}`;
|
|
586
|
+
const entries = subjects.get(subject);
|
|
587
|
+
if (entries === undefined) subjects.set(subject, [entry]);
|
|
588
|
+
else entries.push(entry);
|
|
589
|
+
}
|
|
590
|
+
for (const entries of subjects.values()) {
|
|
591
|
+
const { scope, path, hint } = entries[0]!.test;
|
|
592
|
+
const subjectProperty = scope === Scope.CONTEXT ? readU16(hexToBytes(path), 0) : undefined;
|
|
593
|
+
const typeCode = subjectProperty === undefined ? hexToBytes(hint!).at(-1)! : contextPropertyType(subjectProperty);
|
|
594
|
+
const domain = domainOf(typeCode);
|
|
595
|
+
const rules = entries.map(({ test, location }) => subjectRule(domain, test, location, subjectProperty));
|
|
596
|
+
checkSubject(domain, hasCalldataLength(typeCode), rules, issues);
|
|
1027
597
|
}
|
|
1028
|
-
|
|
1029
|
-
checkDuplicates(issues, operators, groupIndex, constraintIndex);
|
|
1030
598
|
}
|
|
1031
599
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
// Find existing context for this (scope, path) pair.
|
|
1046
|
-
let ctx: ConstraintContext | undefined;
|
|
1047
|
-
for (const existing of contexts) {
|
|
1048
|
-
if (existing.scope === constraint.scope && existing.path === normalizedPath) {
|
|
1049
|
-
ctx = existing;
|
|
1050
|
-
break;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
if (!ctx) {
|
|
1055
|
-
const steps = parsePathSteps(constraint.path);
|
|
1056
|
-
if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
1057
|
-
let typeInfo: TypeInfo;
|
|
1058
|
-
if (constraint.scope === Scope.CALLDATA) {
|
|
1059
|
-
// Compatibility issues against the limits the spec fixes (PWF-17, PV-7).
|
|
1060
|
-
if (steps.length > PolicyFormat.MAX_PATH_DEPTH) {
|
|
1061
|
-
issues.push(
|
|
1062
|
-
ValidationIssue.pathDepthExceeded(
|
|
1063
|
-
groupIndex,
|
|
1064
|
-
constraintIndex,
|
|
1065
|
-
bigintToHex(BigInt(steps.length)),
|
|
1066
|
-
bigintToHex(BigInt(PolicyFormat.MAX_PATH_DEPTH)),
|
|
1067
|
-
),
|
|
1068
|
-
);
|
|
1069
|
-
}
|
|
1070
|
-
let walk: ReturnType<typeof Descriptor.walkPath>;
|
|
1071
|
-
try {
|
|
1072
|
-
walk = Descriptor.walkPath(descBytes, steps);
|
|
1073
|
-
} catch (error) {
|
|
1074
|
-
// Navigability faults become issues; anything else propagates.
|
|
1075
|
-
if (error instanceof CallciumError && navigabilityCodes.has(error.code)) {
|
|
1076
|
-
issues.push(ValidationIssue.unnavigablePath(groupIndex, constraintIndex));
|
|
1077
|
-
continue;
|
|
1078
|
-
}
|
|
1079
|
-
throw error;
|
|
1080
|
-
}
|
|
1081
|
-
// One array at a time carries a quantifier, so the element chain stays singular.
|
|
1082
|
-
if (steps.filter(isQuantifier).length > 1) {
|
|
1083
|
-
issues.push(ValidationIssue.nestedQuantifier(groupIndex, constraintIndex));
|
|
1084
|
-
continue;
|
|
1085
|
-
}
|
|
1086
|
-
if (walk.quantifiedStaticLength > PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH) {
|
|
1087
|
-
issues.push(
|
|
1088
|
-
ValidationIssue.quantifierOverStaticLimit(
|
|
1089
|
-
groupIndex,
|
|
1090
|
-
constraintIndex,
|
|
1091
|
-
bigintToHex(BigInt(walk.quantifiedStaticLength)),
|
|
1092
|
-
bigintToHex(BigInt(PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH)),
|
|
1093
|
-
),
|
|
1094
|
-
);
|
|
1095
|
-
}
|
|
1096
|
-
typeInfo = walk.typeInfo;
|
|
1097
|
-
} else {
|
|
1098
|
-
const ctxId = steps[0]!;
|
|
1099
|
-
const property = findContextProperty(ctxId);
|
|
1100
|
-
if (!property) {
|
|
1101
|
-
// An unassigned ID declares no type, so the analysis proceeds over the widest unsigned domain and the warning
|
|
1102
|
-
// carries the ID.
|
|
1103
|
-
issues.push(
|
|
1104
|
-
ValidationIssue.unknownContextProperty(
|
|
1105
|
-
groupIndex,
|
|
1106
|
-
constraintIndex,
|
|
1107
|
-
bigintToHex(BigInt(ctxId)),
|
|
1108
|
-
bigintToHex(BigInt(MAX_CONTEXT_PROPERTY_ID)),
|
|
1109
|
-
),
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
1112
|
-
typeInfo = { typeCode: property?.typeCode ?? TypeCode.UINT_MAX, isDynamic: false, staticSize: 32 };
|
|
1113
|
-
}
|
|
1114
|
-
ctx = initContext(constraint.scope, normalizedPath, steps, typeInfo);
|
|
1115
|
-
contexts.push(ctx);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
// A carried hint must equal the compilation of its own path; constraints without an encoding carry none, an empty
|
|
1119
|
-
// one carries nothing to compare, and the encoder compiles theirs from the same descriptor. Reaching here means the
|
|
1120
|
-
// path navigates the descriptor and quantifies at most once, so only the PWF-17 depth check remains.
|
|
1121
|
-
if (
|
|
1122
|
-
constraint.scope === Scope.CALLDATA &&
|
|
1123
|
-
constraint.hint !== undefined &&
|
|
1124
|
-
constraint.hint !== "0x" &&
|
|
1125
|
-
ctx.steps.length <= PolicyFormat.MAX_PATH_DEPTH
|
|
1126
|
-
) {
|
|
1127
|
-
const compiled = bytesToHex(Descriptor.compileHint(descBytes, ctx.steps));
|
|
1128
|
-
if (compiled !== constraint.hint.toLowerCase()) {
|
|
1129
|
-
issues.push(ValidationIssue.hintMismatch(groupIndex, constraintIndex));
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues);
|
|
600
|
+
/** Return a test as a rule on its subject, whose context property is `subjectProperty` for a context test. */
|
|
601
|
+
function subjectRule(
|
|
602
|
+
domain: Domain,
|
|
603
|
+
test: BodyRule,
|
|
604
|
+
location: RuleLocation,
|
|
605
|
+
subjectProperty: number | undefined,
|
|
606
|
+
): SubjectRule {
|
|
607
|
+
const { literal, sides } = literalOf(domain, test.operator, subjectProperty);
|
|
608
|
+
let reads = subjectProperty === undefined ? 0 : 1 << subjectProperty;
|
|
609
|
+
if (literal.kind === "relation") reads |= 1 << literal.property;
|
|
610
|
+
return { location, literal, reads, sides };
|
|
611
|
+
}
|
|
1134
612
|
|
|
1135
|
-
|
|
613
|
+
/** Push the PV-9 issues of a group's normal form: its tests and context tests, then each loop body apart. */
|
|
614
|
+
function checkGroupSubjects(rules: Rule[], groupIndex: number, issues: Issue[]): void {
|
|
615
|
+
const tests: { test: BodyRule; location: RuleLocation }[] = [];
|
|
616
|
+
for (const [ruleIndex, rule] of rules.entries()) {
|
|
617
|
+
if (rule.scope === Scope.LOOP) continue;
|
|
618
|
+
tests.push({
|
|
619
|
+
test: rule,
|
|
620
|
+
location: { groupIndex, ruleIndex, scope: rule.scope, path: rule.path, operator: rule.operator },
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
checkSubjects(tests, issues);
|
|
624
|
+
for (const [ruleIndex, rule] of rules.entries()) {
|
|
625
|
+
if (rule.scope !== Scope.LOOP) continue;
|
|
626
|
+
const body = rule.body.map((test, bodyRuleIndex) => ({
|
|
627
|
+
test,
|
|
628
|
+
location: { groupIndex, ruleIndex, bodyRuleIndex, scope: test.scope, path: test.path, operator: test.operator },
|
|
629
|
+
}));
|
|
630
|
+
checkSubjects(body, issues);
|
|
1136
631
|
}
|
|
1137
632
|
}
|
|
1138
633
|
|
|
1139
634
|
///////////////////////////////////////////////////////////////////////////
|
|
1140
|
-
//
|
|
635
|
+
// Validation
|
|
1141
636
|
///////////////////////////////////////////////////////////////////////////
|
|
1142
637
|
|
|
1143
638
|
/**
|
|
1144
|
-
* Validate a policy
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
639
|
+
* Validate a policy blob (PV-1 to PV-9). Storage validation runs first, since the descriptor check reads the descriptor
|
|
640
|
+
* before anything is decoded. Issues of PV-1 to PV-7 name a rule where the blob stores it. Issues of PV-8 and PV-9 name
|
|
641
|
+
* the group where the blob stores it and the rule's position in the normal form of that group. Only rules that raise no
|
|
642
|
+
* PV-2, PV-3, PV-5 or PV-6 issue and no PV-4 issue on a test enter the normal form. PV-8 skips a loop that lost a body
|
|
643
|
+
* test to such an issue.
|
|
644
|
+
* @param blob - A policy blob that passes storage validation.
|
|
645
|
+
* @returns Every issue found, or one issue alone when the descriptor is not valid.
|
|
646
|
+
* @throws {CallciumError} When the blob fails storage validation, or a group's normal form exceeds a format limit.
|
|
1151
647
|
*/
|
|
1152
|
-
function validate(
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
648
|
+
function validate(blob: Hex): Issue[] {
|
|
649
|
+
validatePolicy(blob);
|
|
650
|
+
const bytes = hexToBytes(blob);
|
|
651
|
+
const desc = bytes.subarray(PF.DESC_OFFSET, PF.DESC_OFFSET + readU16(bytes, PF.DESC_LENGTH_OFFSET));
|
|
652
|
+
const invalid = Descriptor.tryValidateMeta(desc);
|
|
653
|
+
if (invalid !== undefined) return [ValidationIssue.invalidDescriptor(invalid.offset)];
|
|
654
|
+
|
|
655
|
+
const data = PolicyCoder.decode(blob);
|
|
1156
656
|
const issues: Issue[] = [];
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
657
|
+
data.groups.forEach((rules, groupIndex) => {
|
|
658
|
+
const kept: Rule[] = [];
|
|
659
|
+
const lost = new Set<string>();
|
|
660
|
+
rules.forEach((rule, ruleIndex) => {
|
|
661
|
+
if (rule.scope === Scope.LOOP) {
|
|
662
|
+
const loop = checkLoop(desc, rule, { groupIndex, ruleIndex, scope: rule.scope, path: rule.path }, issues);
|
|
663
|
+
if (loop === undefined || loop.body.length < rule.body.length) lost.add(loopKey(rule));
|
|
664
|
+
if (loop !== undefined) kept.push(loop);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const location = { groupIndex, ruleIndex, scope: rule.scope, path: rule.path, operator: rule.operator };
|
|
668
|
+
const test = checkTest(desc, rule, undefined, location, issues);
|
|
669
|
+
if (test !== undefined) kept.push(test);
|
|
670
|
+
});
|
|
671
|
+
if (kept.length === 0) return;
|
|
672
|
+
const normal = PolicyCoder.decode(PolicyCoder.encode({ ...data, groups: [kept] })).groups[0]!;
|
|
673
|
+
checkBodies(normal, groupIndex, lost, issues);
|
|
674
|
+
checkGroupSubjects(normal, groupIndex, issues);
|
|
675
|
+
});
|
|
1165
676
|
return issues;
|
|
1166
677
|
}
|
|
1167
678
|
|
|
1168
|
-
/**
|
|
679
|
+
/** Build-time validation of policy blobs. */
|
|
1169
680
|
export const PolicyValidator = { validate };
|