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