@callcium/sdk 0.3.0 → 0.5.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/dist/index.d.ts +64 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +705 -356
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -116,7 +116,8 @@ const DescriptorFormat = {
|
|
|
116
116
|
MAX_NODE_LENGTH: 4095,
|
|
117
117
|
MAX_STATIC_ARRAY_LENGTH: 4095,
|
|
118
118
|
MAX_TUPLE_FIELDS: 4089,
|
|
119
|
-
MAX_PARAMS: 255
|
|
119
|
+
MAX_PARAMS: 255,
|
|
120
|
+
MAX_NESTING_DEPTH: 64
|
|
120
121
|
};
|
|
121
122
|
/** Binary layout constants for the Callcium policy format. */
|
|
122
123
|
const PolicyFormat = {
|
|
@@ -171,42 +172,71 @@ function lookupScope(code) {
|
|
|
171
172
|
if (!info) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${code}`);
|
|
172
173
|
return info;
|
|
173
174
|
}
|
|
175
|
+
/** ABI type code ranges and sentinel values for the descriptor format. */
|
|
176
|
+
const TypeCode = {
|
|
177
|
+
UINT_MIN: 0,
|
|
178
|
+
UINT_MAX: 31,
|
|
179
|
+
INT_MIN: 32,
|
|
180
|
+
INT_MAX: 63,
|
|
181
|
+
ADDRESS: 64,
|
|
182
|
+
BOOL: 65,
|
|
183
|
+
FUNCTION: 66,
|
|
184
|
+
FIXED_BYTES_MIN: 80,
|
|
185
|
+
FIXED_BYTES_MAX: 111,
|
|
186
|
+
BYTES: 112,
|
|
187
|
+
STRING: 113,
|
|
188
|
+
STATIC_ARRAY: 128,
|
|
189
|
+
DYNAMIC_ARRAY: 129,
|
|
190
|
+
TUPLE: 144
|
|
191
|
+
};
|
|
174
192
|
const CTX_PROP_TABLE = [
|
|
175
193
|
{
|
|
176
194
|
key: "MSG_SENDER",
|
|
177
195
|
code: 0,
|
|
178
196
|
label: "msg.sender",
|
|
179
|
-
typeCode:
|
|
197
|
+
typeCode: TypeCode.ADDRESS
|
|
180
198
|
},
|
|
181
199
|
{
|
|
182
200
|
key: "MSG_VALUE",
|
|
183
201
|
code: 1,
|
|
184
202
|
label: "msg.value",
|
|
185
|
-
typeCode:
|
|
203
|
+
typeCode: TypeCode.UINT_MAX
|
|
186
204
|
},
|
|
187
205
|
{
|
|
188
206
|
key: "BLOCK_TIMESTAMP",
|
|
189
207
|
code: 2,
|
|
190
208
|
label: "block.timestamp",
|
|
191
|
-
typeCode:
|
|
209
|
+
typeCode: TypeCode.UINT_MAX
|
|
192
210
|
},
|
|
193
211
|
{
|
|
194
212
|
key: "BLOCK_NUMBER",
|
|
195
213
|
code: 3,
|
|
196
214
|
label: "block.number",
|
|
197
|
-
typeCode:
|
|
215
|
+
typeCode: TypeCode.UINT_MAX
|
|
198
216
|
},
|
|
199
217
|
{
|
|
200
218
|
key: "CHAIN_ID",
|
|
201
219
|
code: 4,
|
|
202
220
|
label: "block.chainid",
|
|
203
|
-
typeCode:
|
|
221
|
+
typeCode: TypeCode.UINT_MAX
|
|
204
222
|
},
|
|
205
223
|
{
|
|
206
224
|
key: "TX_ORIGIN",
|
|
207
225
|
code: 5,
|
|
208
226
|
label: "tx.origin",
|
|
209
|
-
typeCode:
|
|
227
|
+
typeCode: TypeCode.ADDRESS
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: "BASE_FEE",
|
|
231
|
+
code: 6,
|
|
232
|
+
label: "block.basefee",
|
|
233
|
+
typeCode: TypeCode.UINT_MAX
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
key: "GAS_PRICE",
|
|
237
|
+
code: 7,
|
|
238
|
+
label: "tx.gasprice",
|
|
239
|
+
typeCode: TypeCode.UINT_MAX
|
|
210
240
|
}
|
|
211
241
|
];
|
|
212
242
|
/** Well-known context property IDs for context-scope rules. */
|
|
@@ -220,7 +250,9 @@ const CTX_KEY_MAP = {
|
|
|
220
250
|
BLOCK_TIMESTAMP: "blockTimestamp",
|
|
221
251
|
BLOCK_NUMBER: "blockNumber",
|
|
222
252
|
CHAIN_ID: "chainId",
|
|
223
|
-
TX_ORIGIN: "txOrigin"
|
|
253
|
+
TX_ORIGIN: "txOrigin",
|
|
254
|
+
BASE_FEE: "baseFee",
|
|
255
|
+
GAS_PRICE: "gasPrice"
|
|
224
256
|
};
|
|
225
257
|
const ctxPropByCode = new Map(CTX_PROP_TABLE.map((e) => [e.code, {
|
|
226
258
|
label: e.label,
|
|
@@ -411,23 +443,6 @@ function lookupQuantifier(code) {
|
|
|
411
443
|
if (!info) throw new CallciumError("INVALID_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
|
|
412
444
|
return info;
|
|
413
445
|
}
|
|
414
|
-
/** ABI type code ranges and sentinel values for the descriptor format. */
|
|
415
|
-
const TypeCode = {
|
|
416
|
-
UINT_MIN: 0,
|
|
417
|
-
UINT_MAX: 31,
|
|
418
|
-
INT_MIN: 32,
|
|
419
|
-
INT_MAX: 63,
|
|
420
|
-
ADDRESS: 64,
|
|
421
|
-
BOOL: 65,
|
|
422
|
-
FUNCTION: 66,
|
|
423
|
-
FIXED_BYTES_MIN: 80,
|
|
424
|
-
FIXED_BYTES_MAX: 111,
|
|
425
|
-
BYTES: 112,
|
|
426
|
-
STRING: 113,
|
|
427
|
-
STATIC_ARRAY: 128,
|
|
428
|
-
DYNAMIC_ARRAY: 129,
|
|
429
|
-
TUPLE: 144
|
|
430
|
-
};
|
|
431
446
|
/** Throw an UNKNOWN_TYPE_CODE error. */
|
|
432
447
|
function unknownTypeCode(code) {
|
|
433
448
|
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code 0x${code.toString(16).padStart(2, "0")}`);
|
|
@@ -562,11 +577,24 @@ function inspect$1(desc, offset) {
|
|
|
562
577
|
* @throws {CallciumError} On empty steps, out-of-bounds param, or descent into elementary type.
|
|
563
578
|
*/
|
|
564
579
|
function typeAt(desc, steps) {
|
|
580
|
+
return walkPath(desc, steps).typeInfo;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Resolve the type at the path and the declared length of the static array a
|
|
584
|
+
* quantifier step descends into (zero when the path holds no quantifier step
|
|
585
|
+
* over a static array).
|
|
586
|
+
* @param desc - Raw descriptor bytes.
|
|
587
|
+
* @param steps - Path steps, length >= 1.
|
|
588
|
+
* @returns TypeInfo at the resolved path and the quantified static array length.
|
|
589
|
+
* @throws {CallciumError} On empty steps, out-of-bounds param, or descent into elementary type.
|
|
590
|
+
*/
|
|
591
|
+
function walkPath(desc, steps) {
|
|
565
592
|
if (steps.length === 0) throw new CallciumError("INVALID_PATH", "Path must have at least one step.");
|
|
566
593
|
const paramIndex = steps[0];
|
|
567
594
|
const count = desc[1];
|
|
568
595
|
if (paramIndex >= count) throw new CallciumError("INVALID_PATH", `Param index ${paramIndex} out of range (paramCount=${count}).`);
|
|
569
596
|
let cursor = paramOffset(desc, paramIndex);
|
|
597
|
+
let quantifiedStaticLength = 0;
|
|
570
598
|
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
|
|
571
599
|
const typeCode = desc[cursor];
|
|
572
600
|
const step = steps[stepIndex];
|
|
@@ -574,10 +602,15 @@ function typeAt(desc, steps) {
|
|
|
574
602
|
const fields = tupleFieldCount(desc, cursor);
|
|
575
603
|
if (step >= fields) throw new CallciumError("INVALID_PATH", `Tuple field index ${step} out of range (tuple has ${fields} fields).`);
|
|
576
604
|
cursor = tupleFieldOffset(desc, cursor, step);
|
|
577
|
-
} else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY)
|
|
578
|
-
|
|
605
|
+
} else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) {
|
|
606
|
+
if (typeCode === TypeCode.STATIC_ARRAY && isQuantifier(step)) quantifiedStaticLength = staticArrayLength(desc, cursor);
|
|
607
|
+
cursor = arrayElementOffset(cursor);
|
|
608
|
+
} else throw new CallciumError("INVALID_PATH", `Cannot descend into elementary type at offset ${cursor}.`);
|
|
579
609
|
}
|
|
580
|
-
return
|
|
610
|
+
return {
|
|
611
|
+
typeInfo: inspect$1(desc, cursor),
|
|
612
|
+
quantifiedStaticLength
|
|
613
|
+
};
|
|
581
614
|
}
|
|
582
615
|
/**
|
|
583
616
|
* Return tuple field count at a tuple node offset.
|
|
@@ -613,7 +646,8 @@ const Descriptor = {
|
|
|
613
646
|
tupleFieldCount,
|
|
614
647
|
staticArrayLength,
|
|
615
648
|
nodeLength,
|
|
616
|
-
arrayElementOffset
|
|
649
|
+
arrayElementOffset,
|
|
650
|
+
walkPath
|
|
617
651
|
};
|
|
618
652
|
//#endregion
|
|
619
653
|
//#region src/type-desc.ts
|
|
@@ -939,7 +973,7 @@ function toTypes(desc) {
|
|
|
939
973
|
return types.join(",");
|
|
940
974
|
}
|
|
941
975
|
/** Recursively parse a single descriptor node starting at offset. */
|
|
942
|
-
function parseNode(data, offset) {
|
|
976
|
+
function parseNode(data, offset, depth) {
|
|
943
977
|
if (offset >= data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
|
|
944
978
|
const code = data[offset];
|
|
945
979
|
const info = classifyTypeCode(code);
|
|
@@ -957,6 +991,7 @@ function parseNode(data, offset) {
|
|
|
957
991
|
},
|
|
958
992
|
next: metaOffset
|
|
959
993
|
};
|
|
994
|
+
if (depth > DescriptorFormat.MAX_NESTING_DEPTH) throw new CallciumError("NESTING_TOO_DEEP", `Composite nesting exceeds maximum depth ${DescriptorFormat.MAX_NESTING_DEPTH}`, offset);
|
|
960
995
|
const metaEnd = metaOffset + DescriptorFormat.COMPOSITE_META_SIZE;
|
|
961
996
|
if (metaEnd > data.length) throw new CallciumError("UNEXPECTED_END", "Incomplete composite metadata", offset);
|
|
962
997
|
const meta = readU24(data, metaOffset);
|
|
@@ -977,7 +1012,7 @@ function parseNode(data, offset) {
|
|
|
977
1012
|
const fields = [];
|
|
978
1013
|
let cursor = offset + DescriptorFormat.TUPLE_HEADER_SIZE;
|
|
979
1014
|
for (let i = 0; i < fieldCount; i++) {
|
|
980
|
-
const result = parseNode(data, cursor);
|
|
1015
|
+
const result = parseNode(data, cursor, depth + 1);
|
|
981
1016
|
fields.push(result.node);
|
|
982
1017
|
cursor = result.next;
|
|
983
1018
|
}
|
|
@@ -997,7 +1032,7 @@ function parseNode(data, offset) {
|
|
|
997
1032
|
};
|
|
998
1033
|
}
|
|
999
1034
|
if (info.typeClass === "staticArray") {
|
|
1000
|
-
const elemResult = parseNode(data, Descriptor.arrayElementOffset(offset));
|
|
1035
|
+
const elemResult = parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
|
|
1001
1036
|
const lengthOffset = elemResult.next;
|
|
1002
1037
|
if (lengthOffset + DescriptorFormat.ARRAY_LENGTH_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Missing static array length suffix", offset);
|
|
1003
1038
|
const length = readU16(data, lengthOffset);
|
|
@@ -1025,7 +1060,7 @@ function parseNode(data, offset) {
|
|
|
1025
1060
|
typeCode: code,
|
|
1026
1061
|
isDynamic: true,
|
|
1027
1062
|
staticSize: 0,
|
|
1028
|
-
element: parseNode(data, Descriptor.arrayElementOffset(offset)).node,
|
|
1063
|
+
element: parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1).node,
|
|
1029
1064
|
span: {
|
|
1030
1065
|
start: offset,
|
|
1031
1066
|
end: nodeEnd
|
|
@@ -1050,7 +1085,7 @@ function decodeDescriptor(data) {
|
|
|
1050
1085
|
let cursor = DescriptorFormat.HEADER_SIZE;
|
|
1051
1086
|
while (cursor < data.length) {
|
|
1052
1087
|
if (params.length >= DescriptorFormat.MAX_PARAMS) throw new CallciumError("PARAM_COUNT_MISMATCH", `Descriptor exceeds the maximum of ${DescriptorFormat.MAX_PARAMS} top-level params`);
|
|
1053
|
-
const { node, next } = parseNode(data, cursor);
|
|
1088
|
+
const { node, next } = parseNode(data, cursor, 1);
|
|
1054
1089
|
tree.push(node);
|
|
1055
1090
|
params.push({
|
|
1056
1091
|
index: params.length,
|
|
@@ -1161,10 +1196,15 @@ function decodePolicy(blob) {
|
|
|
1161
1196
|
const depthOffset = ruleOffset + PolicyFormat.RULE_DEPTH_OFFSET;
|
|
1162
1197
|
const depthValue = data[depthOffset];
|
|
1163
1198
|
if (depthValue === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
|
|
1199
|
+
if (depthValue > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depthValue} exceeds maximum ${Limits.MAX_PATH_DEPTH}`, ruleOffset);
|
|
1164
1200
|
if (scopeValue === Scope.CONTEXT && depthValue !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step", ruleOffset);
|
|
1165
1201
|
const pathStart = ruleOffset + PolicyFormat.RULE_PATH_OFFSET;
|
|
1166
1202
|
const pathLength = depthValue * PolicyFormat.PATH_STEP_SIZE;
|
|
1167
1203
|
const pathHex = toHex(data, pathStart, pathStart + pathLength);
|
|
1204
|
+
if (scopeValue === Scope.CONTEXT) {
|
|
1205
|
+
const contextPropertyId = readU16(data, pathStart);
|
|
1206
|
+
if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("INVALID_CONTEXT_PROPERTY", `Context rule references undefined context property 0x${contextPropertyId.toString(16).padStart(4, "0")}`, ruleOffset);
|
|
1207
|
+
}
|
|
1168
1208
|
const opCodeOffset = pathStart + pathLength;
|
|
1169
1209
|
const opCodeValue = data[opCodeOffset];
|
|
1170
1210
|
const dataLengthOffset = opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE;
|
|
@@ -1174,6 +1214,10 @@ function decodePolicy(blob) {
|
|
|
1174
1214
|
if (ruleSizeValue !== expectedRuleSize) throw new CallciumError("RULE_SIZE_MISMATCH", `Declared rule size ${ruleSizeValue} does not match computed size ${expectedRuleSize}`, ruleOffset);
|
|
1175
1215
|
const opBase = opCodeValue & ~Op.NOT;
|
|
1176
1216
|
if (opBase === 0 || !isValidOperatorData(opBase, dataLengthValue)) throw new CallciumError("INVALID_OPERATOR", "Unrecognized or malformed operator", ruleOffset);
|
|
1217
|
+
if (opBase === Op.IN) {
|
|
1218
|
+
const wordCount = dataLengthValue / 32;
|
|
1219
|
+
for (let word = 1; word < wordCount; word++) if (compareBytes(data.subarray(dataStart + (word - 1) * 32, dataStart + word * 32), data.subarray(dataStart + word * 32, dataStart + (word + 1) * 32)) >= 0) throw new CallciumError("UNSORTED_IN_SET", "IN operands must be strictly ascending", ruleOffset);
|
|
1220
|
+
}
|
|
1177
1221
|
rules.push({
|
|
1178
1222
|
ruleSize: field(ruleSizeValue, ruleOffset, ruleOffset + PolicyFormat.RULE_SIZE_SIZE),
|
|
1179
1223
|
scope: field(scopeValue, scopeOffset, scopeOffset + 1),
|
|
@@ -1264,7 +1308,7 @@ function encodeRule(rule) {
|
|
|
1264
1308
|
if ((rule.path.length & 1) !== 0) throw new CallciumError("INVALID_PATH", "Path byte length must be even.");
|
|
1265
1309
|
if (rule.operator.length < 1) throw new CallciumError("INVALID_OPERATOR", "Operator must have at least one byte (opcode).");
|
|
1266
1310
|
const depth = rule.path.length / 2;
|
|
1267
|
-
if (depth > Limits.MAX_PATH_DEPTH) throw new CallciumError("
|
|
1311
|
+
if (depth > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${Limits.MAX_PATH_DEPTH}.`);
|
|
1268
1312
|
if (rule.scope === Scope.CONTEXT && depth !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step.");
|
|
1269
1313
|
const opCode = rule.operator[0];
|
|
1270
1314
|
const data = rule.operator.subarray(1);
|
|
@@ -1413,6 +1457,13 @@ function isSigned(typeCode) {
|
|
|
1413
1457
|
return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
|
|
1414
1458
|
}
|
|
1415
1459
|
/**
|
|
1460
|
+
* Return true when the type code represents a left-aligned type (fixed bytes or function),
|
|
1461
|
+
* whose value occupies the high bytes of the word with zero padding below.
|
|
1462
|
+
*/
|
|
1463
|
+
function isLeftAligned(typeCode) {
|
|
1464
|
+
return typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX || typeCode === TypeCode.FUNCTION;
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1416
1467
|
* Canonicalize a raw 256-bit calldata word to its ABI value for the declared type (Policy Spec §7.8).
|
|
1417
1468
|
* A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
|
|
1418
1469
|
* unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
|
|
@@ -1446,6 +1497,15 @@ function isLengthOp(opCode) {
|
|
|
1446
1497
|
const base = opCode & ~Op.NOT;
|
|
1447
1498
|
return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
|
|
1448
1499
|
}
|
|
1500
|
+
/** Map a LENGTH_* opcode base to its core value-comparison twin. */
|
|
1501
|
+
function lengthToValueOp(base) {
|
|
1502
|
+
if (base === Op.LENGTH_EQ) return Op.EQ;
|
|
1503
|
+
if (base === Op.LENGTH_GT) return Op.GT;
|
|
1504
|
+
if (base === Op.LENGTH_LT) return Op.LT;
|
|
1505
|
+
if (base === Op.LENGTH_GTE) return Op.GTE;
|
|
1506
|
+
if (base === Op.LENGTH_LTE) return Op.LTE;
|
|
1507
|
+
return Op.BETWEEN;
|
|
1508
|
+
}
|
|
1449
1509
|
/** Return true when LENGTH_* operators are valid for the given type code (bytes, string, or dynamic array). */
|
|
1450
1510
|
function isLengthValidType(typeCode) {
|
|
1451
1511
|
return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
|
|
@@ -1485,7 +1545,12 @@ function isIn(value, data) {
|
|
|
1485
1545
|
*/
|
|
1486
1546
|
function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
1487
1547
|
const negate = (opCode & Op.NOT) !== 0;
|
|
1488
|
-
|
|
1548
|
+
let base = opCode & ~Op.NOT;
|
|
1549
|
+
if (isLengthOp(base)) {
|
|
1550
|
+
value = BigInt(valueLength);
|
|
1551
|
+
typeCode = TypeCode.UINT_MAX;
|
|
1552
|
+
base = lengthToValueOp(base);
|
|
1553
|
+
}
|
|
1489
1554
|
let result;
|
|
1490
1555
|
switch (base) {
|
|
1491
1556
|
case Op.EQ:
|
|
@@ -1517,32 +1582,14 @@ function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
|
1517
1582
|
result = (value & mask) === mask;
|
|
1518
1583
|
break;
|
|
1519
1584
|
}
|
|
1520
|
-
case Op.BITMASK_ANY:
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
case Op.BITMASK_NONE:
|
|
1524
|
-
result = (value & toBigInt(operandData, 0)) === 0n;
|
|
1525
|
-
break;
|
|
1526
|
-
case Op.LENGTH_EQ:
|
|
1527
|
-
result = BigInt(valueLength) === toBigInt(operandData, 0);
|
|
1528
|
-
break;
|
|
1529
|
-
case Op.LENGTH_GT:
|
|
1530
|
-
result = BigInt(valueLength) > toBigInt(operandData, 0);
|
|
1531
|
-
break;
|
|
1532
|
-
case Op.LENGTH_LT:
|
|
1533
|
-
result = BigInt(valueLength) < toBigInt(operandData, 0);
|
|
1534
|
-
break;
|
|
1535
|
-
case Op.LENGTH_GTE:
|
|
1536
|
-
result = BigInt(valueLength) >= toBigInt(operandData, 0);
|
|
1537
|
-
break;
|
|
1538
|
-
case Op.LENGTH_LTE:
|
|
1539
|
-
result = BigInt(valueLength) <= toBigInt(operandData, 0);
|
|
1585
|
+
case Op.BITMASK_ANY: {
|
|
1586
|
+
const mask = toBigInt(operandData, 0);
|
|
1587
|
+
result = (value & mask) !== 0n;
|
|
1540
1588
|
break;
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
const
|
|
1544
|
-
|
|
1545
|
-
result = length >= min && length <= max;
|
|
1589
|
+
}
|
|
1590
|
+
case Op.BITMASK_NONE: {
|
|
1591
|
+
const mask = toBigInt(operandData, 0);
|
|
1592
|
+
result = (value & mask) === 0n;
|
|
1546
1593
|
break;
|
|
1547
1594
|
}
|
|
1548
1595
|
default: throw new CallciumError("INVALID_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
|
|
@@ -1552,173 +1599,394 @@ function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
|
1552
1599
|
//#endregion
|
|
1553
1600
|
//#region src/validation-issue.ts
|
|
1554
1601
|
const ZERO = `0x${"0".repeat(64)}`;
|
|
1555
|
-
/**
|
|
1556
|
-
function
|
|
1602
|
+
/** Create an issue from operator compatibility results. */
|
|
1603
|
+
function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
|
|
1557
1604
|
return {
|
|
1558
|
-
severity,
|
|
1559
|
-
category,
|
|
1605
|
+
severity: "error",
|
|
1606
|
+
category: "typeMismatch",
|
|
1560
1607
|
groupIndex,
|
|
1561
1608
|
constraintIndex,
|
|
1562
1609
|
code,
|
|
1563
|
-
value1,
|
|
1564
|
-
value2,
|
|
1610
|
+
value1: opCode,
|
|
1611
|
+
value2: ZERO,
|
|
1565
1612
|
message
|
|
1566
1613
|
};
|
|
1567
1614
|
}
|
|
1568
|
-
/**
|
|
1569
|
-
function
|
|
1570
|
-
return
|
|
1615
|
+
/** Issue for an operand that is not canonically encoded for the declared type. */
|
|
1616
|
+
function nonCanonicalOperand(groupIndex, constraintIndex, operand, canonical) {
|
|
1617
|
+
return {
|
|
1618
|
+
severity: "error",
|
|
1619
|
+
category: "typeMismatch",
|
|
1620
|
+
groupIndex,
|
|
1621
|
+
constraintIndex,
|
|
1622
|
+
code: "NON_CANONICAL_OPERAND",
|
|
1623
|
+
value1: operand,
|
|
1624
|
+
value2: canonical,
|
|
1625
|
+
message: "Operand is not the canonical encoding for the declared type"
|
|
1626
|
+
};
|
|
1571
1627
|
}
|
|
1572
|
-
/** eq(v)
|
|
1573
|
-
function eqNeqContradiction(groupIndex, constraintIndex, value) {
|
|
1574
|
-
return
|
|
1628
|
+
/** eq(v)/neq(v) or lengthEq(v)/lengthNeq(v) on the same path. */
|
|
1629
|
+
function eqNeqContradiction(isLength, groupIndex, constraintIndex, value) {
|
|
1630
|
+
return {
|
|
1631
|
+
severity: "error",
|
|
1632
|
+
category: "contradiction",
|
|
1633
|
+
groupIndex,
|
|
1634
|
+
constraintIndex,
|
|
1635
|
+
code: isLength ? "LENGTH_EQ_NEQ_CONTRADICTION" : "EQ_NEQ_CONTRADICTION",
|
|
1636
|
+
value1: value,
|
|
1637
|
+
value2: ZERO,
|
|
1638
|
+
message: isLength ? "lengthEq(v) and lengthNeq(v) on same path" : "eq(v) and neq(v) on same path"
|
|
1639
|
+
};
|
|
1575
1640
|
}
|
|
1576
|
-
/** Multiple eq() operators with different values. */
|
|
1577
|
-
function conflictingEquality(groupIndex, constraintIndex, existing, newValue) {
|
|
1578
|
-
return
|
|
1641
|
+
/** Multiple eq()/lengthEq() operators with different values. */
|
|
1642
|
+
function conflictingEquality(isLength, groupIndex, constraintIndex, existing, newValue) {
|
|
1643
|
+
return {
|
|
1644
|
+
severity: "error",
|
|
1645
|
+
category: "contradiction",
|
|
1646
|
+
groupIndex,
|
|
1647
|
+
constraintIndex,
|
|
1648
|
+
code: isLength ? "CONFLICTING_LENGTH" : "CONFLICTING_EQUALITY",
|
|
1649
|
+
value1: existing,
|
|
1650
|
+
value2: newValue,
|
|
1651
|
+
message: isLength ? "Multiple lengthEq() operators with different values" : "Multiple eq() operators with different values"
|
|
1652
|
+
};
|
|
1579
1653
|
}
|
|
1580
|
-
/** Value is outside the physical range of
|
|
1581
|
-
function outOfPhysicalBounds(groupIndex, constraintIndex, value) {
|
|
1582
|
-
return
|
|
1654
|
+
/** Value/length is outside the physical range of its type. */
|
|
1655
|
+
function outOfPhysicalBounds(isLength, groupIndex, constraintIndex, value) {
|
|
1656
|
+
return {
|
|
1657
|
+
severity: "error",
|
|
1658
|
+
category: "contradiction",
|
|
1659
|
+
groupIndex,
|
|
1660
|
+
constraintIndex,
|
|
1661
|
+
code: isLength ? "OUT_OF_PHYSICAL_LENGTH_BOUNDS" : "OUT_OF_PHYSICAL_BOUNDS",
|
|
1662
|
+
value1: value,
|
|
1663
|
+
value2: ZERO,
|
|
1664
|
+
message: isLength ? "Length value is outside the physical range" : "Value is outside the physical range of the type"
|
|
1665
|
+
};
|
|
1583
1666
|
}
|
|
1584
|
-
/** gt() on
|
|
1585
|
-
function impossibleGt(groupIndex, constraintIndex, value) {
|
|
1586
|
-
return
|
|
1667
|
+
/** gt()/lengthGt() on the maximum is impossible. */
|
|
1668
|
+
function impossibleGt(isLength, groupIndex, constraintIndex, value) {
|
|
1669
|
+
return {
|
|
1670
|
+
severity: "error",
|
|
1671
|
+
category: "contradiction",
|
|
1672
|
+
groupIndex,
|
|
1673
|
+
constraintIndex,
|
|
1674
|
+
code: isLength ? "IMPOSSIBLE_LENGTH_GT" : "IMPOSSIBLE_GT",
|
|
1675
|
+
value1: value,
|
|
1676
|
+
value2: ZERO,
|
|
1677
|
+
message: isLength ? "lengthGt() on maximum length is impossible" : "gt() on type maximum is impossible"
|
|
1678
|
+
};
|
|
1587
1679
|
}
|
|
1588
|
-
/** lt() on
|
|
1589
|
-
function impossibleLt(groupIndex, constraintIndex, value) {
|
|
1590
|
-
return
|
|
1680
|
+
/** lt()/lengthLt() on the minimum is impossible. */
|
|
1681
|
+
function impossibleLt(isLength, groupIndex, constraintIndex, value) {
|
|
1682
|
+
return {
|
|
1683
|
+
severity: "error",
|
|
1684
|
+
category: "contradiction",
|
|
1685
|
+
groupIndex,
|
|
1686
|
+
constraintIndex,
|
|
1687
|
+
code: isLength ? "IMPOSSIBLE_LENGTH_LT" : "IMPOSSIBLE_LT",
|
|
1688
|
+
value1: value,
|
|
1689
|
+
value2: ZERO,
|
|
1690
|
+
message: isLength ? "lengthLt() on minimum length is impossible" : "lt() on type minimum is impossible"
|
|
1691
|
+
};
|
|
1591
1692
|
}
|
|
1592
|
-
/** eq() value is excluded by bound. */
|
|
1593
|
-
function boundsExcludeEquality(groupIndex, constraintIndex, eq, bound) {
|
|
1594
|
-
return
|
|
1693
|
+
/** eq()/lengthEq() value is excluded by a bound. */
|
|
1694
|
+
function boundsExcludeEquality(isLength, groupIndex, constraintIndex, eq, bound) {
|
|
1695
|
+
return {
|
|
1696
|
+
severity: "error",
|
|
1697
|
+
category: "contradiction",
|
|
1698
|
+
groupIndex,
|
|
1699
|
+
constraintIndex,
|
|
1700
|
+
code: isLength ? "BOUNDS_EXCLUDE_LENGTH" : "BOUNDS_EXCLUDE_EQUALITY",
|
|
1701
|
+
value1: eq,
|
|
1702
|
+
value2: bound,
|
|
1703
|
+
message: isLength ? "lengthEq() value is excluded by bound" : "eq() value is excluded by bound"
|
|
1704
|
+
};
|
|
1595
1705
|
}
|
|
1596
|
-
/** Lower bound exceeds upper bound. */
|
|
1597
|
-
function impossibleRange(groupIndex, constraintIndex, lower, upper) {
|
|
1598
|
-
return
|
|
1706
|
+
/** Lower bound/length exceeds the upper bound. */
|
|
1707
|
+
function impossibleRange(isLength, groupIndex, constraintIndex, lower, upper) {
|
|
1708
|
+
return {
|
|
1709
|
+
severity: "error",
|
|
1710
|
+
category: "contradiction",
|
|
1711
|
+
groupIndex,
|
|
1712
|
+
constraintIndex,
|
|
1713
|
+
code: isLength ? "IMPOSSIBLE_LENGTH_RANGE" : "IMPOSSIBLE_RANGE",
|
|
1714
|
+
value1: lower,
|
|
1715
|
+
value2: upper,
|
|
1716
|
+
message: isLength ? "Lower length bound exceeds upper bound" : "Lower bound exceeds upper bound"
|
|
1717
|
+
};
|
|
1599
1718
|
}
|
|
1600
1719
|
/** Set operation excludes existing eq() value. */
|
|
1601
1720
|
function setExcludesEquality(groupIndex, constraintIndex, value) {
|
|
1602
|
-
return
|
|
1721
|
+
return {
|
|
1722
|
+
severity: "error",
|
|
1723
|
+
category: "contradiction",
|
|
1724
|
+
groupIndex,
|
|
1725
|
+
constraintIndex,
|
|
1726
|
+
code: "SET_EXCLUDES_EQUALITY",
|
|
1727
|
+
value1: value,
|
|
1728
|
+
value2: ZERO,
|
|
1729
|
+
message: "Set operation excludes existing eq() value"
|
|
1730
|
+
};
|
|
1603
1731
|
}
|
|
1604
1732
|
/** Multiple isIn() sets have no intersection. */
|
|
1605
1733
|
function emptySetIntersection(groupIndex, constraintIndex) {
|
|
1606
|
-
return
|
|
1734
|
+
return {
|
|
1735
|
+
severity: "error",
|
|
1736
|
+
category: "contradiction",
|
|
1737
|
+
groupIndex,
|
|
1738
|
+
constraintIndex,
|
|
1739
|
+
code: "EMPTY_SET_INTERSECTION",
|
|
1740
|
+
value1: ZERO,
|
|
1741
|
+
value2: ZERO,
|
|
1742
|
+
message: "Multiple isIn() sets have no intersection"
|
|
1743
|
+
};
|
|
1607
1744
|
}
|
|
1608
1745
|
/** All values in isIn() set are excluded by neq/notIn. */
|
|
1609
1746
|
function setFullyExcluded(groupIndex, constraintIndex) {
|
|
1610
|
-
return
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
}
|
|
1620
|
-
/** lengthEq() value is excluded by bound. */
|
|
1621
|
-
function boundsExcludeLength(groupIndex, constraintIndex, eq, bound) {
|
|
1622
|
-
return makeIssue("error", "contradiction", groupIndex, constraintIndex, "BOUNDS_EXCLUDE_LENGTH", eq, bound, "lengthEq() value is excluded by bound");
|
|
1623
|
-
}
|
|
1624
|
-
/** Lower length bound exceeds upper bound. */
|
|
1625
|
-
function impossibleLengthRange(groupIndex, constraintIndex, lower, upper) {
|
|
1626
|
-
return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LENGTH_RANGE", lower, upper, "Lower length bound exceeds upper bound");
|
|
1627
|
-
}
|
|
1628
|
-
/** Length value is outside the physical range. */
|
|
1629
|
-
function outOfPhysicalLengthBounds(groupIndex, constraintIndex, value) {
|
|
1630
|
-
return makeIssue("error", "contradiction", groupIndex, constraintIndex, "OUT_OF_PHYSICAL_LENGTH_BOUNDS", value, ZERO, "Length value is outside the physical range");
|
|
1631
|
-
}
|
|
1632
|
-
/** lengthGt() on maximum length is impossible. */
|
|
1633
|
-
function impossibleLengthGt(groupIndex, constraintIndex, value) {
|
|
1634
|
-
return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LENGTH_GT", value, ZERO, "lengthGt() on maximum length is impossible");
|
|
1635
|
-
}
|
|
1636
|
-
/** lengthLt() on minimum length is impossible. */
|
|
1637
|
-
function impossibleLengthLt(groupIndex, constraintIndex, value) {
|
|
1638
|
-
return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LENGTH_LT", value, ZERO, "lengthLt() on minimum length is impossible");
|
|
1747
|
+
return {
|
|
1748
|
+
severity: "error",
|
|
1749
|
+
category: "contradiction",
|
|
1750
|
+
groupIndex,
|
|
1751
|
+
constraintIndex,
|
|
1752
|
+
code: "SET_FULLY_EXCLUDED",
|
|
1753
|
+
value1: ZERO,
|
|
1754
|
+
value2: ZERO,
|
|
1755
|
+
message: "All values in isIn() set are excluded by neq/notIn"
|
|
1756
|
+
};
|
|
1639
1757
|
}
|
|
1640
1758
|
/** Bitmask operators conflict. */
|
|
1641
1759
|
function bitmaskContradiction(groupIndex, constraintIndex, mask, conflicting) {
|
|
1642
|
-
return
|
|
1760
|
+
return {
|
|
1761
|
+
severity: "error",
|
|
1762
|
+
category: "contradiction",
|
|
1763
|
+
groupIndex,
|
|
1764
|
+
constraintIndex,
|
|
1765
|
+
code: "BITMASK_CONTRADICTION",
|
|
1766
|
+
value1: mask,
|
|
1767
|
+
value2: conflicting,
|
|
1768
|
+
message: "Bitmask operators conflict"
|
|
1769
|
+
};
|
|
1643
1770
|
}
|
|
1644
1771
|
/** bitmaskAny is impossible because all bits are forbidden. */
|
|
1645
1772
|
function bitmaskAnyImpossible(groupIndex, constraintIndex, mask, mustBeZero) {
|
|
1646
|
-
return
|
|
1773
|
+
return {
|
|
1774
|
+
severity: "error",
|
|
1775
|
+
category: "contradiction",
|
|
1776
|
+
groupIndex,
|
|
1777
|
+
constraintIndex,
|
|
1778
|
+
code: "BITMASK_ANY_IMPOSSIBLE",
|
|
1779
|
+
value1: mask,
|
|
1780
|
+
value2: mustBeZero,
|
|
1781
|
+
message: "bitmaskAny is impossible because all bits are forbidden"
|
|
1782
|
+
};
|
|
1647
1783
|
}
|
|
1648
1784
|
/** isIn/notIn set is not strictly sorted and deduplicated. */
|
|
1649
1785
|
function unsortedInSet(groupIndex, constraintIndex) {
|
|
1650
|
-
return
|
|
1786
|
+
return {
|
|
1787
|
+
severity: "error",
|
|
1788
|
+
category: "contradiction",
|
|
1789
|
+
groupIndex,
|
|
1790
|
+
constraintIndex,
|
|
1791
|
+
code: "UNSORTED_IN_SET",
|
|
1792
|
+
value1: ZERO,
|
|
1793
|
+
value2: ZERO,
|
|
1794
|
+
message: "isIn/notIn set is not strictly sorted and deduplicated."
|
|
1795
|
+
};
|
|
1651
1796
|
}
|
|
1652
1797
|
/** Group contains zero constraints. */
|
|
1653
1798
|
function emptyGroup(groupIndex) {
|
|
1654
|
-
return
|
|
1799
|
+
return {
|
|
1800
|
+
severity: "error",
|
|
1801
|
+
category: "vacuity",
|
|
1802
|
+
groupIndex,
|
|
1803
|
+
constraintIndex: 0,
|
|
1804
|
+
code: "EMPTY_GROUP",
|
|
1805
|
+
value1: ZERO,
|
|
1806
|
+
value2: ZERO,
|
|
1807
|
+
message: "Group contains zero constraints."
|
|
1808
|
+
};
|
|
1655
1809
|
}
|
|
1656
|
-
/** Numeric bound
|
|
1657
|
-
function dominatedBound(groupIndex, constraintIndex, value) {
|
|
1658
|
-
return
|
|
1810
|
+
/** Numeric/length bound dominated by a stricter bound. */
|
|
1811
|
+
function dominatedBound(isLength, groupIndex, constraintIndex, value) {
|
|
1812
|
+
return {
|
|
1813
|
+
severity: "warning",
|
|
1814
|
+
category: "redundancy",
|
|
1815
|
+
groupIndex,
|
|
1816
|
+
constraintIndex,
|
|
1817
|
+
code: isLength ? "DOMINATED_LENGTH_BOUND" : "DOMINATED_BOUND",
|
|
1818
|
+
value1: value,
|
|
1819
|
+
value2: ZERO,
|
|
1820
|
+
message: isLength ? "Length bound is dominated by a stricter bound" : "Numeric bound is dominated by a stricter bound"
|
|
1821
|
+
};
|
|
1659
1822
|
}
|
|
1660
|
-
/** Bound
|
|
1661
|
-
function redundantBound(groupIndex, constraintIndex, bound, eq) {
|
|
1662
|
-
return
|
|
1823
|
+
/** Bound redundant because eq()/lengthEq() is set. */
|
|
1824
|
+
function redundantBound(isLength, groupIndex, constraintIndex, bound, eq) {
|
|
1825
|
+
return {
|
|
1826
|
+
severity: "warning",
|
|
1827
|
+
category: "redundancy",
|
|
1828
|
+
groupIndex,
|
|
1829
|
+
constraintIndex,
|
|
1830
|
+
code: isLength ? "REDUNDANT_LENGTH_BOUND" : "REDUNDANT_BOUND",
|
|
1831
|
+
value1: bound,
|
|
1832
|
+
value2: eq,
|
|
1833
|
+
message: isLength ? "Length bound is redundant because lengthEq() is set" : "Bound is redundant because eq() is set"
|
|
1834
|
+
};
|
|
1663
1835
|
}
|
|
1664
1836
|
/** notIn() value was present in isIn() set. */
|
|
1665
1837
|
function setReduction(groupIndex, constraintIndex, value) {
|
|
1666
|
-
return
|
|
1838
|
+
return {
|
|
1839
|
+
severity: "warning",
|
|
1840
|
+
category: "redundancy",
|
|
1841
|
+
groupIndex,
|
|
1842
|
+
constraintIndex,
|
|
1843
|
+
code: "SET_REDUCTION",
|
|
1844
|
+
value1: value,
|
|
1845
|
+
value2: ZERO,
|
|
1846
|
+
message: "notIn() value was present in isIn() set"
|
|
1847
|
+
};
|
|
1667
1848
|
}
|
|
1668
1849
|
/** isIn() sets partially overlap. */
|
|
1669
1850
|
function setRedundancy(groupIndex, constraintIndex, intersectionCount) {
|
|
1670
|
-
return
|
|
1851
|
+
return {
|
|
1852
|
+
severity: "warning",
|
|
1853
|
+
category: "redundancy",
|
|
1854
|
+
groupIndex,
|
|
1855
|
+
constraintIndex,
|
|
1856
|
+
code: "SET_REDUNDANCY",
|
|
1857
|
+
value1: intersectionCount,
|
|
1858
|
+
value2: ZERO,
|
|
1859
|
+
message: "isIn() sets partially overlap"
|
|
1860
|
+
};
|
|
1671
1861
|
}
|
|
1672
1862
|
/** Some values in isIn() set are excluded by neq/notIn. */
|
|
1673
1863
|
function setPartiallyExcluded(groupIndex, constraintIndex, excludedCount) {
|
|
1674
|
-
return
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1864
|
+
return {
|
|
1865
|
+
severity: "warning",
|
|
1866
|
+
category: "redundancy",
|
|
1867
|
+
groupIndex,
|
|
1868
|
+
constraintIndex,
|
|
1869
|
+
code: "SET_PARTIALLY_EXCLUDED",
|
|
1870
|
+
value1: excludedCount,
|
|
1871
|
+
value2: ZERO,
|
|
1872
|
+
message: "Some values in isIn() set are excluded by neq/notIn"
|
|
1873
|
+
};
|
|
1683
1874
|
}
|
|
1684
1875
|
/** Bitmask operation is redundant. */
|
|
1685
1876
|
function redundantBitmask(groupIndex, constraintIndex, mask, existing) {
|
|
1686
|
-
return
|
|
1877
|
+
return {
|
|
1878
|
+
severity: "warning",
|
|
1879
|
+
category: "redundancy",
|
|
1880
|
+
groupIndex,
|
|
1881
|
+
constraintIndex,
|
|
1882
|
+
code: "REDUNDANT_BITMASK",
|
|
1883
|
+
value1: mask,
|
|
1884
|
+
value2: existing,
|
|
1885
|
+
message: "Bitmask operation is redundant"
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
/** gte/lte pair fusible into a single range operator. */
|
|
1889
|
+
function fusibleRange(isLength, groupIndex, constraintIndex, low, high) {
|
|
1890
|
+
return {
|
|
1891
|
+
severity: "warning",
|
|
1892
|
+
category: "redundancy",
|
|
1893
|
+
groupIndex,
|
|
1894
|
+
constraintIndex,
|
|
1895
|
+
code: isLength ? "FUSIBLE_LENGTH_RANGE" : "FUSIBLE_RANGE",
|
|
1896
|
+
value1: low,
|
|
1897
|
+
value2: high,
|
|
1898
|
+
message: isLength ? "lengthGte() and lengthLte() fuse into a single lengthBetween()" : "gte() and lte() fuse into a single between()"
|
|
1899
|
+
};
|
|
1687
1900
|
}
|
|
1688
1901
|
/** Duplicate operator in constraint. */
|
|
1689
1902
|
function duplicateConstraint(groupIndex, constraintIndex) {
|
|
1690
|
-
return
|
|
1903
|
+
return {
|
|
1904
|
+
severity: "warning",
|
|
1905
|
+
category: "redundancy",
|
|
1906
|
+
groupIndex,
|
|
1907
|
+
constraintIndex,
|
|
1908
|
+
code: "DUPLICATE_CONSTRAINT",
|
|
1909
|
+
value1: ZERO,
|
|
1910
|
+
value2: ZERO,
|
|
1911
|
+
message: "Duplicate operator in constraint"
|
|
1912
|
+
};
|
|
1691
1913
|
}
|
|
1692
|
-
/** gte()
|
|
1693
|
-
function vacuousGte(groupIndex, constraintIndex, value) {
|
|
1694
|
-
return
|
|
1914
|
+
/** gte(min)/lengthGte(0) is always true. */
|
|
1915
|
+
function vacuousGte(isLength, groupIndex, constraintIndex, value) {
|
|
1916
|
+
return {
|
|
1917
|
+
severity: "info",
|
|
1918
|
+
category: "vacuity",
|
|
1919
|
+
groupIndex,
|
|
1920
|
+
constraintIndex,
|
|
1921
|
+
code: isLength ? "VACUOUS_LENGTH_GTE" : "VACUOUS_GTE",
|
|
1922
|
+
value1: value,
|
|
1923
|
+
value2: ZERO,
|
|
1924
|
+
message: isLength ? "lengthGte(0) is always true" : "gte() bound equals type minimum (always true)"
|
|
1925
|
+
};
|
|
1695
1926
|
}
|
|
1696
|
-
/** lte()
|
|
1697
|
-
function vacuousLte(groupIndex, constraintIndex, value) {
|
|
1698
|
-
return
|
|
1927
|
+
/** lte(max)/lengthLte(max) is always true. */
|
|
1928
|
+
function vacuousLte(isLength, groupIndex, constraintIndex, value) {
|
|
1929
|
+
return {
|
|
1930
|
+
severity: "info",
|
|
1931
|
+
category: "vacuity",
|
|
1932
|
+
groupIndex,
|
|
1933
|
+
constraintIndex,
|
|
1934
|
+
code: isLength ? "VACUOUS_LENGTH_LTE" : "VACUOUS_LTE",
|
|
1935
|
+
value1: value,
|
|
1936
|
+
value2: ZERO,
|
|
1937
|
+
message: isLength ? "lengthLte() bound equals maximum (always true)" : "lte() bound equals type maximum (always true)"
|
|
1938
|
+
};
|
|
1699
1939
|
}
|
|
1700
|
-
/**
|
|
1701
|
-
function
|
|
1702
|
-
return
|
|
1940
|
+
/** Path deeper than the reference enforcer's cap. */
|
|
1941
|
+
function pathDepthExceeded(groupIndex, constraintIndex, depth, maxDepth) {
|
|
1942
|
+
return {
|
|
1943
|
+
severity: "warning",
|
|
1944
|
+
category: "compatibility",
|
|
1945
|
+
groupIndex,
|
|
1946
|
+
constraintIndex,
|
|
1947
|
+
code: "PATH_DEPTH_EXCEEDED",
|
|
1948
|
+
value1: depth,
|
|
1949
|
+
value2: maxDepth,
|
|
1950
|
+
message: "Path depth exceeds the reference enforcer cap"
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
/** Quantifier over a static array beyond the reference enforcer's iteration cap. */
|
|
1954
|
+
function quantifierOverStaticLimit(groupIndex, constraintIndex, arrayLength, maxLength) {
|
|
1955
|
+
return {
|
|
1956
|
+
severity: "warning",
|
|
1957
|
+
category: "compatibility",
|
|
1958
|
+
groupIndex,
|
|
1959
|
+
constraintIndex,
|
|
1960
|
+
code: "QUANTIFIER_OVER_STATIC_LIMIT",
|
|
1961
|
+
value1: arrayLength,
|
|
1962
|
+
value2: maxLength,
|
|
1963
|
+
message: "Quantifier over static array exceeds the reference enforcer cap"
|
|
1964
|
+
};
|
|
1703
1965
|
}
|
|
1704
|
-
/**
|
|
1705
|
-
function
|
|
1706
|
-
return
|
|
1966
|
+
/** Context property ID outside the assigned set. */
|
|
1967
|
+
function unknownContextProperty(groupIndex, constraintIndex, contextId, maxContextId) {
|
|
1968
|
+
return {
|
|
1969
|
+
severity: "warning",
|
|
1970
|
+
category: "compatibility",
|
|
1971
|
+
groupIndex,
|
|
1972
|
+
constraintIndex,
|
|
1973
|
+
code: "UNKNOWN_CONTEXT_PROPERTY",
|
|
1974
|
+
value1: contextId,
|
|
1975
|
+
value2: maxContextId,
|
|
1976
|
+
message: "Unknown context property ID"
|
|
1977
|
+
};
|
|
1707
1978
|
}
|
|
1708
|
-
/**
|
|
1709
|
-
function
|
|
1979
|
+
/** Negated operator under the existential quantifier. */
|
|
1980
|
+
function negationUnderAny(groupIndex, constraintIndex, opCode) {
|
|
1710
1981
|
return {
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
redundant: isLength ? redundantLengthBound : redundantBound,
|
|
1720
|
-
vacuousGte: isLength ? vacuousLengthGte : vacuousGte,
|
|
1721
|
-
vacuousLte: isLength ? vacuousLengthLte : vacuousLte
|
|
1982
|
+
severity: "warning",
|
|
1983
|
+
category: "compatibility",
|
|
1984
|
+
groupIndex,
|
|
1985
|
+
constraintIndex,
|
|
1986
|
+
code: "NEGATION_UNDER_ANY",
|
|
1987
|
+
value1: opCode,
|
|
1988
|
+
value2: ZERO,
|
|
1989
|
+
message: "Negated operator under any() passes when a decoy element differs"
|
|
1722
1990
|
};
|
|
1723
1991
|
}
|
|
1724
1992
|
//#endregion
|
|
@@ -1741,25 +2009,11 @@ const MAX_NOT_IN = 8;
|
|
|
1741
2009
|
const LENGTH_MAX = 4294967295n;
|
|
1742
2010
|
const UINT256_MAX = (1n << 256n) - 1n;
|
|
1743
2011
|
const INT256_SIGN_BIT = 1n << 255n;
|
|
1744
|
-
/**
|
|
1745
|
-
function
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
function isGt(a, b, signed) {
|
|
1750
|
-
return signed ? toSigned(a) > toSigned(b) : a > b;
|
|
1751
|
-
}
|
|
1752
|
-
/** a >= b. */
|
|
1753
|
-
function isGte(a, b, signed) {
|
|
1754
|
-
return signed ? toSigned(a) >= toSigned(b) : a >= b;
|
|
1755
|
-
}
|
|
1756
|
-
/** a < b. */
|
|
1757
|
-
function isLt(a, b, signed) {
|
|
1758
|
-
return signed ? toSigned(a) < toSigned(b) : a < b;
|
|
1759
|
-
}
|
|
1760
|
-
/** a <= b. */
|
|
1761
|
-
function isLte(a, b, signed) {
|
|
1762
|
-
return signed ? toSigned(a) <= toSigned(b) : a <= b;
|
|
2012
|
+
/** Signed-aware difference of two raw uint256 values: negative if a<b, zero if equal, positive if a>b. */
|
|
2013
|
+
function signedCompare(a, b, signed) {
|
|
2014
|
+
if (!signed) return a - b;
|
|
2015
|
+
const toS = (v) => v >= INT256_SIGN_BIT ? v - (1n << 256n) : v;
|
|
2016
|
+
return toS(a) - toS(b);
|
|
1763
2017
|
}
|
|
1764
2018
|
/** True for uint and int type codes. */
|
|
1765
2019
|
function isNumericType(typeCode) {
|
|
@@ -1824,6 +2078,10 @@ function getIncompat(opBase, typeInfo) {
|
|
|
1824
2078
|
code: "VALUE_OP_ON_DYNAMIC",
|
|
1825
2079
|
message: "Value operator used on dynamic type"
|
|
1826
2080
|
};
|
|
2081
|
+
if (classifyTypeCode(typeCode).typeClass !== "elementary") return {
|
|
2082
|
+
code: "VALUE_OP_ON_COMPOSITE",
|
|
2083
|
+
message: "Value operator used on composite type"
|
|
2084
|
+
};
|
|
1827
2085
|
if (isComparisonOp(opBase) && !isNumericType(typeCode)) return {
|
|
1828
2086
|
code: "NUMERIC_OP_ON_NON_NUMERIC",
|
|
1829
2087
|
message: "Comparison operator used on non-numeric type"
|
|
@@ -1832,6 +2090,10 @@ function getIncompat(opBase, typeInfo) {
|
|
|
1832
2090
|
code: "BITMASK_ON_INVALID",
|
|
1833
2091
|
message: "Bitmask operator used on incompatible type"
|
|
1834
2092
|
};
|
|
2093
|
+
if (opBase === Op.IN && typeCode === TypeCode.BOOL) return {
|
|
2094
|
+
code: "IN_ON_BOOL",
|
|
2095
|
+
message: "IN operator used on boolean type"
|
|
2096
|
+
};
|
|
1835
2097
|
return null;
|
|
1836
2098
|
}
|
|
1837
2099
|
if (isLengthOp(opBase)) {
|
|
@@ -1846,22 +2108,6 @@ function getIncompat(opBase, typeInfo) {
|
|
|
1846
2108
|
message: "Unknown operator code"
|
|
1847
2109
|
};
|
|
1848
2110
|
}
|
|
1849
|
-
/** Check operator–type compatibility and return an issue descriptor on mismatch. */
|
|
1850
|
-
function checkCompatibility(opBase, typeCode, isDynamic, staticSize) {
|
|
1851
|
-
const incompat = getIncompat(opBase, {
|
|
1852
|
-
typeCode,
|
|
1853
|
-
isDynamic,
|
|
1854
|
-
staticSize
|
|
1855
|
-
});
|
|
1856
|
-
return incompat ? {
|
|
1857
|
-
compatible: false,
|
|
1858
|
-
...incompat
|
|
1859
|
-
} : {
|
|
1860
|
-
compatible: true,
|
|
1861
|
-
code: "",
|
|
1862
|
-
message: ""
|
|
1863
|
-
};
|
|
1864
|
-
}
|
|
1865
2111
|
/**
|
|
1866
2112
|
* True if operator `opCode` is allowed on a target with the given type info.
|
|
1867
2113
|
* Tolerates the NOT flag on `opCode`.
|
|
@@ -1873,6 +2119,19 @@ function isOpAllowed(opCode, typeInfo) {
|
|
|
1873
2119
|
function readValue(opHex) {
|
|
1874
2120
|
return BigInt(`0x${opHex.slice(4)}`);
|
|
1875
2121
|
}
|
|
2122
|
+
/** First payload word deviating from the canonical encoding for the type, with its canonical form, or null. */
|
|
2123
|
+
function findNonCanonicalWord(opHex, typeCode) {
|
|
2124
|
+
const body = opHex.slice(4);
|
|
2125
|
+
for (let i = 0; i + 64 <= body.length; i += 64) {
|
|
2126
|
+
const word = BigInt(`0x${body.slice(i, i + 64)}`);
|
|
2127
|
+
const canonical = canonicalize(word, typeCode);
|
|
2128
|
+
if (canonical !== word) return {
|
|
2129
|
+
word,
|
|
2130
|
+
canonical
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
return null;
|
|
2134
|
+
}
|
|
1876
2135
|
/** Read a pair of uint256 values from a BETWEEN operator. */
|
|
1877
2136
|
function readPair(opHex) {
|
|
1878
2137
|
const body = opHex.slice(4);
|
|
@@ -1911,6 +2170,23 @@ function negateBoundOp(base) {
|
|
|
1911
2170
|
if (base === Op.LTE) return Op.GT;
|
|
1912
2171
|
return base;
|
|
1913
2172
|
}
|
|
2173
|
+
/** Fresh bound domain with no constraints recorded yet. */
|
|
2174
|
+
function emptyBoundDomain(boundIsSigned, min, max) {
|
|
2175
|
+
return {
|
|
2176
|
+
isSigned: boundIsSigned,
|
|
2177
|
+
min,
|
|
2178
|
+
max,
|
|
2179
|
+
hasEq: false,
|
|
2180
|
+
eq: 0n,
|
|
2181
|
+
hasLower: false,
|
|
2182
|
+
lower: 0n,
|
|
2183
|
+
lowerInclusive: false,
|
|
2184
|
+
hasUpper: false,
|
|
2185
|
+
upper: 0n,
|
|
2186
|
+
upperInclusive: false,
|
|
2187
|
+
holes: []
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
1914
2190
|
/** Create a fresh constraint context with domain limits from the type. */
|
|
1915
2191
|
function initContext(scope, path, typeInfo) {
|
|
1916
2192
|
const { min, max } = getDomainLimits(typeInfo.typeCode);
|
|
@@ -1918,38 +2194,12 @@ function initContext(scope, path, typeInfo) {
|
|
|
1918
2194
|
scope,
|
|
1919
2195
|
path,
|
|
1920
2196
|
typeInfo,
|
|
1921
|
-
numeric:
|
|
1922
|
-
isSigned: isSigned(typeInfo.typeCode),
|
|
1923
|
-
min,
|
|
1924
|
-
max,
|
|
1925
|
-
hasEq: false,
|
|
1926
|
-
eq: 0n,
|
|
1927
|
-
hasLower: false,
|
|
1928
|
-
lower: 0n,
|
|
1929
|
-
lowerInclusive: false,
|
|
1930
|
-
hasUpper: false,
|
|
1931
|
-
upper: 0n,
|
|
1932
|
-
upperInclusive: false,
|
|
1933
|
-
holes: []
|
|
1934
|
-
},
|
|
2197
|
+
numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
|
|
1935
2198
|
bitmask: {
|
|
1936
2199
|
mustBeOne: 0n,
|
|
1937
2200
|
mustBeZero: 0n
|
|
1938
2201
|
},
|
|
1939
|
-
length:
|
|
1940
|
-
isSigned: false,
|
|
1941
|
-
min: 0n,
|
|
1942
|
-
max: LENGTH_MAX,
|
|
1943
|
-
hasEq: false,
|
|
1944
|
-
eq: 0n,
|
|
1945
|
-
hasLower: false,
|
|
1946
|
-
lower: 0n,
|
|
1947
|
-
lowerInclusive: false,
|
|
1948
|
-
hasUpper: false,
|
|
1949
|
-
upper: 0n,
|
|
1950
|
-
upperInclusive: false,
|
|
1951
|
-
holes: []
|
|
1952
|
-
},
|
|
2202
|
+
length: emptyBoundDomain(false, 0n, LENGTH_MAX),
|
|
1953
2203
|
set: {
|
|
1954
2204
|
hasIn: false,
|
|
1955
2205
|
inValues: [],
|
|
@@ -1958,33 +2208,33 @@ function initContext(scope, path, typeInfo) {
|
|
|
1958
2208
|
};
|
|
1959
2209
|
}
|
|
1960
2210
|
/** Narrow the numeric bound domain with an operator and emit issues on contradiction or redundancy. */
|
|
1961
|
-
function updateBound(domain, base, isNegated, value,
|
|
2211
|
+
function updateBound(domain, base, isNegated, value, isLength, groupIndex, constraintIndex, issues) {
|
|
1962
2212
|
let changedEq = false;
|
|
1963
2213
|
let changedLower = false;
|
|
1964
2214
|
let changedUpper = false;
|
|
1965
2215
|
if (isNegated) {
|
|
1966
2216
|
if (base === Op.EQ) {
|
|
1967
|
-
if (domain.hasEq && domain.eq === value) issues.push(
|
|
2217
|
+
if (domain.hasEq && domain.eq === value) issues.push(eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
1968
2218
|
let alreadyHole = false;
|
|
1969
2219
|
for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) {
|
|
1970
2220
|
alreadyHole = true;
|
|
1971
2221
|
break;
|
|
1972
2222
|
}
|
|
1973
2223
|
if (!alreadyHole && domain.holes.length < MAX_HOLES) domain.holes.push(value);
|
|
1974
|
-
} else updateBound(domain, negateBoundOp(base), false, value,
|
|
2224
|
+
} else updateBound(domain, negateBoundOp(base), false, value, isLength, groupIndex, constraintIndex, issues);
|
|
1975
2225
|
return;
|
|
1976
2226
|
}
|
|
1977
|
-
if (base === Op.GTE && value === domain.min) issues.push(
|
|
1978
|
-
else if (base === Op.LTE && value === domain.max) issues.push(
|
|
1979
|
-
if (
|
|
1980
|
-
else if (base === Op.GT && value === domain.max) issues.push(
|
|
1981
|
-
else if (base === Op.LT && value === domain.min) issues.push(
|
|
2227
|
+
if (base === Op.GTE && value === domain.min) issues.push(vacuousGte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2228
|
+
else if (base === Op.LTE && value === domain.max) issues.push(vacuousLte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2229
|
+
if (signedCompare(value, domain.min, domain.isSigned) < 0n || signedCompare(value, domain.max, domain.isSigned) > 0n) issues.push(outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2230
|
+
else if (base === Op.GT && value === domain.max) issues.push(impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2231
|
+
else if (base === Op.LT && value === domain.min) issues.push(impossibleLt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
1982
2232
|
if (base === Op.EQ) {
|
|
1983
2233
|
if (!domain.hasEq || domain.eq !== value) changedEq = true;
|
|
1984
2234
|
if (domain.hasEq) {
|
|
1985
|
-
if (domain.eq !== value) issues.push(
|
|
2235
|
+
if (domain.eq !== value) issues.push(conflictingEquality(isLength, groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(value)));
|
|
1986
2236
|
}
|
|
1987
|
-
for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) issues.push(
|
|
2237
|
+
for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) issues.push(eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
1988
2238
|
domain.hasEq = true;
|
|
1989
2239
|
domain.eq = value;
|
|
1990
2240
|
} else if (base === Op.GT || base === Op.GTE) {
|
|
@@ -1992,15 +2242,15 @@ function updateBound(domain, base, isNegated, value, emit, groupIndex, constrain
|
|
|
1992
2242
|
if (domain.hasLower) {
|
|
1993
2243
|
let redundant = false;
|
|
1994
2244
|
let strictlyBetter = false;
|
|
1995
|
-
if (
|
|
2245
|
+
if (signedCompare(value, domain.lower, domain.isSigned) < 0n) redundant = true;
|
|
1996
2246
|
else if (value === domain.lower) {
|
|
1997
2247
|
if (domain.lowerInclusive) if (!inclusive) strictlyBetter = true;
|
|
1998
2248
|
else redundant = true;
|
|
1999
2249
|
else if (!inclusive) redundant = true;
|
|
2000
2250
|
} else strictlyBetter = true;
|
|
2001
|
-
if (redundant) issues.push(
|
|
2251
|
+
if (redundant) issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2002
2252
|
if (strictlyBetter) {
|
|
2003
|
-
issues.push(
|
|
2253
|
+
issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.lower)));
|
|
2004
2254
|
domain.lower = value;
|
|
2005
2255
|
domain.lowerInclusive = inclusive;
|
|
2006
2256
|
changedLower = true;
|
|
@@ -2016,15 +2266,15 @@ function updateBound(domain, base, isNegated, value, emit, groupIndex, constrain
|
|
|
2016
2266
|
if (domain.hasUpper) {
|
|
2017
2267
|
let redundant = false;
|
|
2018
2268
|
let strictlyBetter = false;
|
|
2019
|
-
if (
|
|
2269
|
+
if (signedCompare(value, domain.upper, domain.isSigned) > 0n) redundant = true;
|
|
2020
2270
|
else if (value === domain.upper) {
|
|
2021
2271
|
if (domain.upperInclusive) if (!inclusive) strictlyBetter = true;
|
|
2022
2272
|
else redundant = true;
|
|
2023
2273
|
else if (!inclusive) redundant = true;
|
|
2024
2274
|
} else strictlyBetter = true;
|
|
2025
|
-
if (redundant) issues.push(
|
|
2275
|
+
if (redundant) issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2026
2276
|
if (strictlyBetter) {
|
|
2027
|
-
issues.push(
|
|
2277
|
+
issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.upper)));
|
|
2028
2278
|
domain.upper = value;
|
|
2029
2279
|
domain.upperInclusive = inclusive;
|
|
2030
2280
|
changedUpper = true;
|
|
@@ -2037,16 +2287,16 @@ function updateBound(domain, base, isNegated, value, emit, groupIndex, constrain
|
|
|
2037
2287
|
}
|
|
2038
2288
|
}
|
|
2039
2289
|
if (changedEq || changedLower) {
|
|
2040
|
-
if (domain.hasEq && domain.hasLower) if (domain.lowerInclusive ?
|
|
2041
|
-
else issues.push(
|
|
2290
|
+
if (domain.hasEq && domain.hasLower) if (domain.lowerInclusive ? signedCompare(domain.eq, domain.lower, domain.isSigned) < 0n : signedCompare(domain.eq, domain.lower, domain.isSigned) <= 0n) issues.push(boundsExcludeEquality(isLength, groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(domain.lower)));
|
|
2291
|
+
else issues.push(redundantBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.lower), bigintToHex(domain.eq)));
|
|
2042
2292
|
}
|
|
2043
2293
|
if (changedEq || changedUpper) {
|
|
2044
|
-
if (domain.hasEq && domain.hasUpper) if (domain.upperInclusive ?
|
|
2045
|
-
else issues.push(
|
|
2294
|
+
if (domain.hasEq && domain.hasUpper) if (domain.upperInclusive ? signedCompare(domain.eq, domain.upper, domain.isSigned) > 0n : signedCompare(domain.eq, domain.upper, domain.isSigned) >= 0n) issues.push(boundsExcludeEquality(isLength, groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(domain.upper)));
|
|
2295
|
+
else issues.push(redundantBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.upper), bigintToHex(domain.eq)));
|
|
2046
2296
|
}
|
|
2047
2297
|
if (changedLower || changedUpper) {
|
|
2048
2298
|
if (domain.hasLower && domain.hasUpper) {
|
|
2049
|
-
if (
|
|
2299
|
+
if (signedCompare(domain.lower, domain.upper, domain.isSigned) > 0n || domain.lower === domain.upper && (!domain.lowerInclusive || !domain.upperInclusive)) issues.push(impossibleRange(isLength, groupIndex, constraintIndex, bigintToHex(domain.lower), bigintToHex(domain.upper)));
|
|
2050
2300
|
}
|
|
2051
2301
|
}
|
|
2052
2302
|
}
|
|
@@ -2142,27 +2392,78 @@ function checkDuplicates(issues, operators, groupIndex, constraintIndex) {
|
|
|
2142
2392
|
seen.add(key);
|
|
2143
2393
|
}
|
|
2144
2394
|
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Report a lone unnegated gte/lte pair (value or length domain) as fusible into the
|
|
2397
|
+
* corresponding single range operator. Fires only for a satisfiable pair on a compatible
|
|
2398
|
+
* type; contradictions and dominated bounds are already reported by the domain checks.
|
|
2399
|
+
*/
|
|
2400
|
+
function checkFusibleRange(ctx, operators, groupIndex, constraintIndex, issues) {
|
|
2401
|
+
const numericPair = findLonePair(operators, Op.GTE, Op.LTE);
|
|
2402
|
+
if (numericPair && getIncompat(Op.GTE, ctx.typeInfo) === null && signedCompare(numericPair.low, numericPair.high, ctx.numeric.isSigned) <= 0n) issues.push(fusibleRange(false, groupIndex, constraintIndex, bigintToHex(numericPair.low), bigintToHex(numericPair.high)));
|
|
2403
|
+
const lengthPair = findLonePair(operators, Op.LENGTH_GTE, Op.LENGTH_LTE);
|
|
2404
|
+
if (lengthPair && getIncompat(Op.LENGTH_GTE, ctx.typeInfo) === null && lengthPair.low <= lengthPair.high) issues.push(fusibleRange(true, groupIndex, constraintIndex, bigintToHex(lengthPair.low), bigintToHex(lengthPair.high)));
|
|
2405
|
+
}
|
|
2406
|
+
/** Return the operand pair when the operators contain exactly one of each unnegated bound opcode. */
|
|
2407
|
+
function findLonePair(operators, lowerOp, upperOp) {
|
|
2408
|
+
let lowerCount = 0;
|
|
2409
|
+
let upperCount = 0;
|
|
2410
|
+
let low = 0n;
|
|
2411
|
+
let high = 0n;
|
|
2412
|
+
for (const opHex of operators) {
|
|
2413
|
+
const opCode = parseInt(opHex.slice(2, 4), 16);
|
|
2414
|
+
const dataLength = (opHex.length - 4) / 2;
|
|
2415
|
+
if (dataLength > 65535 || !isValidOperatorData(opCode & ~Op.NOT, dataLength)) continue;
|
|
2416
|
+
if (opCode === lowerOp) {
|
|
2417
|
+
lowerCount++;
|
|
2418
|
+
low = readValue(opHex);
|
|
2419
|
+
} else if (opCode === upperOp) {
|
|
2420
|
+
upperCount++;
|
|
2421
|
+
high = readValue(opHex);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
return lowerCount === 1 && upperCount === 1 ? {
|
|
2425
|
+
low,
|
|
2426
|
+
high
|
|
2427
|
+
} : null;
|
|
2428
|
+
}
|
|
2145
2429
|
/** Validate all operators on a single constraint against its domain context. */
|
|
2146
2430
|
function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues) {
|
|
2147
2431
|
const operators = constraint.operators;
|
|
2432
|
+
const underAny = constraint.scope === Scope.CALLDATA && parsePathSteps(constraint.path).includes(Quantifier.ANY);
|
|
2148
2433
|
for (const opHex of operators) {
|
|
2149
2434
|
const opCode = parseInt(opHex.slice(2, 4), 16);
|
|
2150
2435
|
const base = opCode & ~Op.NOT;
|
|
2151
2436
|
const isNegated = (opCode & Op.NOT) !== 0;
|
|
2152
|
-
const
|
|
2153
|
-
if (!
|
|
2154
|
-
issues.push(fromOpRule(
|
|
2437
|
+
const dataLength = (opHex.length - 4) / 2;
|
|
2438
|
+
if (dataLength > 65535 || !isValidOperatorData(base, dataLength)) {
|
|
2439
|
+
issues.push(fromOpRule("UNKNOWN_OPERATOR", "Unknown operator code", groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
if (underAny && isNegated) issues.push(negationUnderAny(groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
2443
|
+
const incompat = getIncompat(base, ctx.typeInfo);
|
|
2444
|
+
if (incompat) {
|
|
2445
|
+
issues.push(fromOpRule(incompat.code, incompat.message, groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
2155
2446
|
continue;
|
|
2156
2447
|
}
|
|
2157
2448
|
if (isValueOp(base)) {
|
|
2449
|
+
const typeCode = ctx.typeInfo.typeCode;
|
|
2450
|
+
if (isLeftAligned(typeCode)) {
|
|
2451
|
+
const nonCanonical = findNonCanonicalWord(opHex, typeCode);
|
|
2452
|
+
if (nonCanonical !== null) {
|
|
2453
|
+
issues.push(nonCanonicalOperand(groupIndex, constraintIndex, bigintToHex(nonCanonical.word), bigintToHex(nonCanonical.canonical)));
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2158
2457
|
if (base >= Op.EQ && base <= Op.BETWEEN) if (base === Op.BETWEEN) {
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2458
|
+
if (!isNegated) {
|
|
2459
|
+
const { low, high } = readPair(opHex);
|
|
2460
|
+
updateBound(ctx.numeric, Op.GTE, false, low, false, groupIndex, constraintIndex, issues);
|
|
2461
|
+
updateBound(ctx.numeric, Op.LTE, false, high, false, groupIndex, constraintIndex, issues);
|
|
2462
|
+
}
|
|
2162
2463
|
} else {
|
|
2163
2464
|
const value = readValue(opHex);
|
|
2164
2465
|
const holesBefore = ctx.numeric.holes.length;
|
|
2165
|
-
updateBound(ctx.numeric, base, isNegated, value,
|
|
2466
|
+
updateBound(ctx.numeric, base, isNegated, value, false, groupIndex, constraintIndex, issues);
|
|
2166
2467
|
if (ctx.numeric.holes.length > holesBefore) checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
2167
2468
|
}
|
|
2168
2469
|
else if (isBitmaskOp(base)) updateBitmask(ctx, base, isNegated, readValue(opHex), groupIndex, constraintIndex, issues);
|
|
@@ -2172,12 +2473,14 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
|
|
|
2172
2473
|
else updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues);
|
|
2173
2474
|
}
|
|
2174
2475
|
} else if (isLengthOp(base)) if (base === Op.LENGTH_BETWEEN) {
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2476
|
+
if (!isNegated) {
|
|
2477
|
+
const { low, high } = readPair(opHex);
|
|
2478
|
+
updateBound(ctx.length, Op.GTE, false, low, true, groupIndex, constraintIndex, issues);
|
|
2479
|
+
updateBound(ctx.length, Op.LTE, false, high, true, groupIndex, constraintIndex, issues);
|
|
2480
|
+
}
|
|
2178
2481
|
} else {
|
|
2179
2482
|
const value = readValue(opHex);
|
|
2180
|
-
updateBound(ctx.length, normalizeLengthOp(base), isNegated, value,
|
|
2483
|
+
updateBound(ctx.length, normalizeLengthOp(base), isNegated, value, true, groupIndex, constraintIndex, issues);
|
|
2181
2484
|
}
|
|
2182
2485
|
}
|
|
2183
2486
|
checkDuplicates(issues, operators, groupIndex, constraintIndex);
|
|
@@ -2198,10 +2501,20 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2198
2501
|
let typeInfo;
|
|
2199
2502
|
if (constraint.scope === Scope.CALLDATA) {
|
|
2200
2503
|
const steps = parsePathSteps(constraint.path);
|
|
2201
|
-
|
|
2504
|
+
if (steps.length > Limits.MAX_PATH_DEPTH) issues.push(pathDepthExceeded(groupIndex, constraintIndex, bigintToHex(BigInt(steps.length)), bigintToHex(BigInt(Limits.MAX_PATH_DEPTH))));
|
|
2505
|
+
const walk = Descriptor.walkPath(descBytes, steps);
|
|
2506
|
+
if (walk.quantifiedStaticLength > Limits.MAX_QUANTIFIED_ARRAY_LENGTH) issues.push(quantifierOverStaticLimit(groupIndex, constraintIndex, bigintToHex(BigInt(walk.quantifiedStaticLength)), bigintToHex(BigInt(Limits.MAX_QUANTIFIED_ARRAY_LENGTH))));
|
|
2507
|
+
typeInfo = walk.typeInfo;
|
|
2202
2508
|
} else {
|
|
2203
2509
|
const ctxId = parsePathSteps(constraint.path)[0];
|
|
2204
|
-
|
|
2510
|
+
if (ctxId > MAX_CONTEXT_PROPERTY_ID) {
|
|
2511
|
+
issues.push(unknownContextProperty(groupIndex, constraintIndex, bigintToHex(BigInt(ctxId)), bigintToHex(BigInt(MAX_CONTEXT_PROPERTY_ID))));
|
|
2512
|
+
typeInfo = {
|
|
2513
|
+
typeCode: TypeCode.UINT_MAX,
|
|
2514
|
+
isDynamic: false,
|
|
2515
|
+
staticSize: 32
|
|
2516
|
+
};
|
|
2517
|
+
} else typeInfo = {
|
|
2205
2518
|
typeCode: lookupContextProperty(ctxId).typeCode,
|
|
2206
2519
|
isDynamic: false,
|
|
2207
2520
|
staticSize: 32
|
|
@@ -2211,6 +2524,7 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2211
2524
|
contexts.push(ctx);
|
|
2212
2525
|
}
|
|
2213
2526
|
validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues);
|
|
2527
|
+
checkFusibleRange(ctx, constraint.operators, groupIndex, constraintIndex, issues);
|
|
2214
2528
|
}
|
|
2215
2529
|
}
|
|
2216
2530
|
/**
|
|
@@ -2379,18 +2693,31 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2379
2693
|
return this;
|
|
2380
2694
|
}
|
|
2381
2695
|
/**
|
|
2382
|
-
* Build the policy into an encoded binary blob.
|
|
2696
|
+
* Build the policy into an encoded binary blob with strict validation.
|
|
2697
|
+
* Throws on any issue, regardless of severity. Use {@link validate} to
|
|
2698
|
+
* inspect issues, or {@link buildUnsafe} to encode without validation.
|
|
2383
2699
|
* @returns The policy as a 0x-prefixed hex string.
|
|
2384
|
-
* @throws {CallciumError} If any group is empty or validation finds
|
|
2700
|
+
* @throws {CallciumError} If any group is empty or validation finds any issue.
|
|
2385
2701
|
*/
|
|
2386
2702
|
build() {
|
|
2387
2703
|
this.checkGroups();
|
|
2388
2704
|
const policyData = this.toPolicyData();
|
|
2389
|
-
const
|
|
2390
|
-
if (
|
|
2705
|
+
const issues = PolicyValidator.validate(policyData);
|
|
2706
|
+
if (issues.length > 0) throw new CallciumError("VALIDATION_ERROR", issues[0].message);
|
|
2391
2707
|
return PolicyCoder.encode(policyData);
|
|
2392
2708
|
}
|
|
2393
2709
|
/**
|
|
2710
|
+
* Build the policy into an encoded binary blob without validation.
|
|
2711
|
+
* The resulting policy may be invalid. Prefer {@link build}; use this
|
|
2712
|
+
* only to knowingly bypass a reported issue.
|
|
2713
|
+
* @returns The policy as a 0x-prefixed hex string.
|
|
2714
|
+
* @throws {CallciumError} If any group is empty.
|
|
2715
|
+
*/
|
|
2716
|
+
buildUnsafe() {
|
|
2717
|
+
this.checkGroups();
|
|
2718
|
+
return PolicyCoder.encode(this.toPolicyData());
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2394
2721
|
* Validate the policy without encoding.
|
|
2395
2722
|
* @returns All validation issues found.
|
|
2396
2723
|
*/
|
|
@@ -2415,6 +2742,12 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2415
2742
|
};
|
|
2416
2743
|
//#endregion
|
|
2417
2744
|
//#region src/constraint.ts
|
|
2745
|
+
/** Strip an optional 0x prefix and validate a 40-hex-char (20-byte) address body. */
|
|
2746
|
+
function addressBody(value) {
|
|
2747
|
+
const body = value.startsWith("0x") ? value.slice(2) : value;
|
|
2748
|
+
if (body.length !== 40) throw new CallciumError("INVALID_CONSTRAINT", `Invalid address length: expected 40 hex chars, got ${body.length}`);
|
|
2749
|
+
return body;
|
|
2750
|
+
}
|
|
2418
2751
|
/** Convert a scalar value to a 32-byte big-endian word. */
|
|
2419
2752
|
function encodeWord(value) {
|
|
2420
2753
|
const word = new Uint8Array(32);
|
|
@@ -2423,8 +2756,7 @@ function encodeWord(value) {
|
|
|
2423
2756
|
return word;
|
|
2424
2757
|
}
|
|
2425
2758
|
if (typeof value === "string") {
|
|
2426
|
-
const body =
|
|
2427
|
-
if (body.length !== 40) throw new CallciumError("INVALID_CONSTRAINT", `Invalid address length: expected 40 hex chars, got ${body.length}`);
|
|
2759
|
+
const body = addressBody(value);
|
|
2428
2760
|
for (let i = 0; i < 20; i++) {
|
|
2429
2761
|
const byte = parseInt(body.substring(i * 2, i * 2 + 2), 16);
|
|
2430
2762
|
if (Number.isNaN(byte)) throw new CallciumError("INVALID_CONSTRAINT", `Invalid hex character in address at position ${i * 2}`);
|
|
@@ -2461,8 +2793,7 @@ function setOp(opCode, values) {
|
|
|
2461
2793
|
if (typeof v === "bigint") return v;
|
|
2462
2794
|
if (typeof v === "number") return BigInt(v);
|
|
2463
2795
|
if (typeof v === "boolean") return v ? 1n : 0n;
|
|
2464
|
-
|
|
2465
|
-
return BigInt("0x" + (body || "0"));
|
|
2796
|
+
return BigInt("0x" + addressBody(v));
|
|
2466
2797
|
});
|
|
2467
2798
|
bigs.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
2468
2799
|
const deduped = [];
|
|
@@ -2611,6 +2942,14 @@ function chainId() {
|
|
|
2611
2942
|
function txOrigin() {
|
|
2612
2943
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.TX_ORIGIN]));
|
|
2613
2944
|
}
|
|
2945
|
+
/** Target the `block.basefee` context property. */
|
|
2946
|
+
function baseFee() {
|
|
2947
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BASE_FEE]));
|
|
2948
|
+
}
|
|
2949
|
+
/** Target the `tx.gasprice` context property. */
|
|
2950
|
+
function gasPrice() {
|
|
2951
|
+
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.GAS_PRICE]));
|
|
2952
|
+
}
|
|
2614
2953
|
//#endregion
|
|
2615
2954
|
//#region src/reader.ts
|
|
2616
2955
|
/** Read 32 bytes at offset, bounds-checked. */
|
|
@@ -2653,7 +2992,7 @@ function descendTuple(node, callData, head, base, childIndex) {
|
|
|
2653
2992
|
if (node.isDynamic) {
|
|
2654
2993
|
const ptrResult = readPointer(callData, head);
|
|
2655
2994
|
if (typeof ptrResult !== "number") return {
|
|
2656
|
-
|
|
2995
|
+
ok: false,
|
|
2657
2996
|
code: ptrResult.code
|
|
2658
2997
|
};
|
|
2659
2998
|
tupleBase = base + ptrResult;
|
|
@@ -2664,42 +3003,48 @@ function descendTuple(node, callData, head, base, childIndex) {
|
|
|
2664
3003
|
}
|
|
2665
3004
|
for (let i = 0; i < childIndex; i++) cursor += headContribution(node.fields[i]);
|
|
2666
3005
|
return {
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
3006
|
+
ok: true,
|
|
3007
|
+
location: {
|
|
3008
|
+
head: cursor,
|
|
3009
|
+
base: tupleBase,
|
|
3010
|
+
node: node.fields[childIndex]
|
|
3011
|
+
}
|
|
2671
3012
|
};
|
|
2672
3013
|
}
|
|
2673
3014
|
/** Descend into a dynamic array element by index. */
|
|
2674
3015
|
function descendDynamicArray(node, callData, head, base, childIndex) {
|
|
2675
3016
|
const arrayBaseResult = readPointer(callData, head);
|
|
2676
3017
|
if (typeof arrayBaseResult !== "number") return {
|
|
2677
|
-
|
|
3018
|
+
ok: false,
|
|
2678
3019
|
code: arrayBaseResult.code
|
|
2679
3020
|
};
|
|
2680
3021
|
const arrayBase = base + arrayBaseResult;
|
|
2681
3022
|
const lengthResult = readPointer(callData, arrayBase);
|
|
2682
3023
|
if (typeof lengthResult !== "number") return {
|
|
2683
|
-
|
|
3024
|
+
ok: false,
|
|
2684
3025
|
code: lengthResult.code
|
|
2685
3026
|
};
|
|
2686
3027
|
if (childIndex >= lengthResult) return {
|
|
2687
|
-
|
|
3028
|
+
ok: false,
|
|
2688
3029
|
code: "ARRAY_INDEX_OUT_OF_BOUNDS"
|
|
2689
3030
|
};
|
|
2690
3031
|
const headsBase = arrayBase + 32;
|
|
2691
3032
|
const elem = node.element;
|
|
2692
3033
|
if (elem.isDynamic) return {
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
3034
|
+
ok: true,
|
|
3035
|
+
location: {
|
|
3036
|
+
head: headsBase + childIndex * 32,
|
|
3037
|
+
base: headsBase,
|
|
3038
|
+
node: elem
|
|
3039
|
+
}
|
|
2697
3040
|
};
|
|
2698
3041
|
return {
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
3042
|
+
ok: true,
|
|
3043
|
+
location: {
|
|
3044
|
+
head: headsBase + childIndex * elem.staticSize,
|
|
3045
|
+
base: arrayBase,
|
|
3046
|
+
node: elem
|
|
3047
|
+
}
|
|
2703
3048
|
};
|
|
2704
3049
|
}
|
|
2705
3050
|
/** Descend into a static array element by index. */
|
|
@@ -2709,22 +3054,26 @@ function descendStaticArray(node, callData, head, base, childIndex) {
|
|
|
2709
3054
|
if (elem.isDynamic) {
|
|
2710
3055
|
const ptrResult = readPointer(callData, head);
|
|
2711
3056
|
if (typeof ptrResult !== "number") return {
|
|
2712
|
-
|
|
3057
|
+
ok: false,
|
|
2713
3058
|
code: ptrResult.code
|
|
2714
3059
|
};
|
|
2715
3060
|
const arrayBase = base + ptrResult;
|
|
2716
3061
|
return {
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
3062
|
+
ok: true,
|
|
3063
|
+
location: {
|
|
3064
|
+
head: arrayBase + childIndex * 32,
|
|
3065
|
+
base: arrayBase,
|
|
3066
|
+
node: elem
|
|
3067
|
+
}
|
|
2721
3068
|
};
|
|
2722
3069
|
}
|
|
2723
3070
|
return {
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
3071
|
+
ok: true,
|
|
3072
|
+
location: {
|
|
3073
|
+
head: head + childIndex * elem.staticSize,
|
|
3074
|
+
base,
|
|
3075
|
+
node: elem
|
|
3076
|
+
}
|
|
2728
3077
|
};
|
|
2729
3078
|
}
|
|
2730
3079
|
/**
|
|
@@ -2764,13 +3113,13 @@ function locate(tree, callData, pathBytes, baseOffset) {
|
|
|
2764
3113
|
};
|
|
2765
3114
|
}
|
|
2766
3115
|
const result = descend(node, callData, head, base, step);
|
|
2767
|
-
if (result.
|
|
3116
|
+
if (!result.ok) return {
|
|
2768
3117
|
ok: false,
|
|
2769
3118
|
code: result.code
|
|
2770
3119
|
};
|
|
2771
|
-
head = result.head;
|
|
2772
|
-
base = result.base;
|
|
2773
|
-
node = result.node;
|
|
3120
|
+
head = result.location.head;
|
|
3121
|
+
base = result.location.base;
|
|
3122
|
+
node = result.location.node;
|
|
2774
3123
|
}
|
|
2775
3124
|
return {
|
|
2776
3125
|
ok: true,
|
|
@@ -2782,6 +3131,29 @@ function locate(tree, callData, pathBytes, baseOffset) {
|
|
|
2782
3131
|
}
|
|
2783
3132
|
};
|
|
2784
3133
|
}
|
|
3134
|
+
/**
|
|
3135
|
+
* Build the element-shape fields shared by dynamic- and static-array nodes, branching only on
|
|
3136
|
+
* whether the element itself is dynamic.
|
|
3137
|
+
*/
|
|
3138
|
+
function makeArrayShape(length, elem, dynamicHeadsBase, staticDataOffset, staticCompositeBase) {
|
|
3139
|
+
return elem.isDynamic ? {
|
|
3140
|
+
length,
|
|
3141
|
+
elementNode: elem,
|
|
3142
|
+
elementIsDynamic: true,
|
|
3143
|
+
elementStaticSize: 0,
|
|
3144
|
+
headsBase: dynamicHeadsBase,
|
|
3145
|
+
dataOffset: 0,
|
|
3146
|
+
compositeBase: dynamicHeadsBase
|
|
3147
|
+
} : {
|
|
3148
|
+
length,
|
|
3149
|
+
elementNode: elem,
|
|
3150
|
+
elementIsDynamic: false,
|
|
3151
|
+
elementStaticSize: elem.staticSize,
|
|
3152
|
+
headsBase: 0,
|
|
3153
|
+
dataOffset: staticDataOffset,
|
|
3154
|
+
compositeBase: staticCompositeBase
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
2785
3157
|
/** Extract array shape from a location pointing to an array node. */
|
|
2786
3158
|
function arrayShape(callData, location) {
|
|
2787
3159
|
const { head, base, node } = location;
|
|
@@ -2797,66 +3169,26 @@ function arrayShape(callData, location) {
|
|
|
2797
3169
|
ok: false,
|
|
2798
3170
|
code: lengthResult.code
|
|
2799
3171
|
};
|
|
2800
|
-
const elem = node.element;
|
|
2801
3172
|
const headsBase = arrayBase + 32;
|
|
2802
|
-
if (elem.isDynamic) return {
|
|
2803
|
-
ok: true,
|
|
2804
|
-
shape: {
|
|
2805
|
-
length: lengthResult,
|
|
2806
|
-
elementNode: elem,
|
|
2807
|
-
elementIsDynamic: true,
|
|
2808
|
-
elementStaticSize: 0,
|
|
2809
|
-
headsBase,
|
|
2810
|
-
dataOffset: 0,
|
|
2811
|
-
compositeBase: headsBase
|
|
2812
|
-
}
|
|
2813
|
-
};
|
|
2814
3173
|
return {
|
|
2815
3174
|
ok: true,
|
|
2816
|
-
shape:
|
|
2817
|
-
length: lengthResult,
|
|
2818
|
-
elementNode: elem,
|
|
2819
|
-
elementIsDynamic: false,
|
|
2820
|
-
elementStaticSize: elem.staticSize,
|
|
2821
|
-
headsBase: 0,
|
|
2822
|
-
dataOffset: headsBase,
|
|
2823
|
-
compositeBase: arrayBase
|
|
2824
|
-
}
|
|
3175
|
+
shape: makeArrayShape(lengthResult, node.element, headsBase, headsBase, arrayBase)
|
|
2825
3176
|
};
|
|
2826
3177
|
}
|
|
2827
3178
|
if (node.type === "staticArray") {
|
|
2828
3179
|
const elem = node.element;
|
|
3180
|
+
let dynamicHeadsBase = 0;
|
|
2829
3181
|
if (elem.isDynamic) {
|
|
2830
3182
|
const arrayBaseResult = readPointer(callData, head);
|
|
2831
3183
|
if (typeof arrayBaseResult !== "number") return {
|
|
2832
3184
|
ok: false,
|
|
2833
3185
|
code: arrayBaseResult.code
|
|
2834
3186
|
};
|
|
2835
|
-
|
|
2836
|
-
return {
|
|
2837
|
-
ok: true,
|
|
2838
|
-
shape: {
|
|
2839
|
-
length: node.length,
|
|
2840
|
-
elementNode: elem,
|
|
2841
|
-
elementIsDynamic: true,
|
|
2842
|
-
elementStaticSize: 0,
|
|
2843
|
-
headsBase: arrayBase,
|
|
2844
|
-
dataOffset: 0,
|
|
2845
|
-
compositeBase: arrayBase
|
|
2846
|
-
}
|
|
2847
|
-
};
|
|
3187
|
+
dynamicHeadsBase = base + arrayBaseResult;
|
|
2848
3188
|
}
|
|
2849
3189
|
return {
|
|
2850
3190
|
ok: true,
|
|
2851
|
-
shape:
|
|
2852
|
-
length: node.length,
|
|
2853
|
-
elementNode: elem,
|
|
2854
|
-
elementIsDynamic: false,
|
|
2855
|
-
elementStaticSize: elem.staticSize,
|
|
2856
|
-
headsBase: 0,
|
|
2857
|
-
dataOffset: head,
|
|
2858
|
-
compositeBase: base
|
|
2859
|
-
}
|
|
3191
|
+
shape: makeArrayShape(node.length, elem, dynamicHeadsBase, head, base)
|
|
2860
3192
|
};
|
|
2861
3193
|
}
|
|
2862
3194
|
throw new CallciumError("INVALID_PATH", `Cannot compute array shape for non-array node (${node.type}).`);
|
|
@@ -2914,9 +3246,16 @@ function loadSlice(callData, location) {
|
|
|
2914
3246
|
ok: false,
|
|
2915
3247
|
code: lengthResult.code
|
|
2916
3248
|
};
|
|
3249
|
+
const dataOffset = dataStart + 32;
|
|
3250
|
+
const node = location.node;
|
|
3251
|
+
const stride = node.type === "dynamicArray" ? node.element.isDynamic ? 32 : node.element.staticSize : 1;
|
|
3252
|
+
if (dataOffset > callData.length || lengthResult > Math.floor((callData.length - dataOffset) / stride)) return {
|
|
3253
|
+
ok: false,
|
|
3254
|
+
code: "CALLDATA_OUT_OF_BOUNDS"
|
|
3255
|
+
};
|
|
2917
3256
|
return {
|
|
2918
3257
|
ok: true,
|
|
2919
|
-
dataOffset
|
|
3258
|
+
dataOffset,
|
|
2920
3259
|
length: lengthResult
|
|
2921
3260
|
};
|
|
2922
3261
|
}
|
|
@@ -2927,13 +3266,13 @@ function descendPath(callData, location, pathBytes) {
|
|
|
2927
3266
|
for (let stepIndex = 0; stepIndex < stepCount; stepIndex++) {
|
|
2928
3267
|
const step = readStep(pathBytes, stepIndex);
|
|
2929
3268
|
const result = descend(node, callData, head, base, step);
|
|
2930
|
-
if (result.
|
|
3269
|
+
if (!result.ok) return {
|
|
2931
3270
|
ok: false,
|
|
2932
3271
|
code: result.code
|
|
2933
3272
|
};
|
|
2934
|
-
head = result.head;
|
|
2935
|
-
base = result.base;
|
|
2936
|
-
node = result.node;
|
|
3273
|
+
head = result.location.head;
|
|
3274
|
+
base = result.location.base;
|
|
3275
|
+
node = result.location.node;
|
|
2937
3276
|
}
|
|
2938
3277
|
return {
|
|
2939
3278
|
ok: true,
|
|
@@ -2946,6 +3285,12 @@ function descendPath(callData, location, pathBytes) {
|
|
|
2946
3285
|
}
|
|
2947
3286
|
//#endregion
|
|
2948
3287
|
//#region src/policy-enforcer.ts
|
|
3288
|
+
/** Violations that abort evaluation instead of failing only their group (spec §10.3). */
|
|
3289
|
+
const ABORT_VIOLATION_CODES = new Set([
|
|
3290
|
+
"CALLDATA_OUT_OF_BOUNDS",
|
|
3291
|
+
"ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
3292
|
+
"QUANTIFIER_LIMIT_EXCEEDED"
|
|
3293
|
+
]);
|
|
2949
3294
|
/** Convert a hex address to a 256-bit bigint (zero-padded to 32 bytes). */
|
|
2950
3295
|
function addressToBigInt(hex) {
|
|
2951
3296
|
const bytes = hexToBytes(hex);
|
|
@@ -2980,7 +3325,7 @@ function resolveLeafTypeCode(elementNode, remainingPath) {
|
|
|
2980
3325
|
* @param policy - Binary policy blob as 0x-prefixed hex string.
|
|
2981
3326
|
* @param callData - ABI-encoded call data as 0x-prefixed hex string.
|
|
2982
3327
|
* @param context - Optional execution context for context-scoped rules.
|
|
2983
|
-
* @returns Pass with matched group index, or fail with one violation per
|
|
3328
|
+
* @returns Pass with matched group index, or fail with one violation per evaluated failing group.
|
|
2984
3329
|
* @throws {CallciumError} If the policy blob is structurally malformed.
|
|
2985
3330
|
*/
|
|
2986
3331
|
function check(policy, callData, context) {
|
|
@@ -3012,6 +3357,10 @@ function check(policy, callData, context) {
|
|
|
3012
3357
|
const violation = evaluateRule(rule, tree, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
|
|
3013
3358
|
if (violation !== null) {
|
|
3014
3359
|
allViolations.push(violation);
|
|
3360
|
+
if (ABORT_VIOLATION_CODES.has(violation.code)) return {
|
|
3361
|
+
ok: false,
|
|
3362
|
+
violations: allViolations
|
|
3363
|
+
};
|
|
3015
3364
|
groupFailed = true;
|
|
3016
3365
|
break;
|
|
3017
3366
|
}
|
|
@@ -3044,7 +3393,6 @@ function evaluateRule(rule, tree, callDataBytes, baseOffset, groupIndex, ruleInd
|
|
|
3044
3393
|
const pathBytes = hexToBytes(rule.path.value);
|
|
3045
3394
|
const opCode = rule.opCode.value;
|
|
3046
3395
|
const operandData = hexToBytes(rule.data.value);
|
|
3047
|
-
if (pathBytes.length / 2 > Limits.MAX_PATH_DEPTH) throw new CallciumError("INVALID_PATH", `Path depth ${pathBytes.length / 2} exceeds maximum ${Limits.MAX_PATH_DEPTH}.`);
|
|
3048
3396
|
if (scope === Scope.CONTEXT) return evaluateContextRule(pathBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value, context);
|
|
3049
3397
|
const locResult = locate(tree, callDataBytes, pathBytes, baseOffset);
|
|
3050
3398
|
if (!locResult.ok) return {
|
|
@@ -3110,6 +3458,7 @@ function applyLeafOperator(callDataBytes, location, opCode, operandData) {
|
|
|
3110
3458
|
resolvedValue: bigintToHex(BigInt(valueLength))
|
|
3111
3459
|
};
|
|
3112
3460
|
}
|
|
3461
|
+
if (node.isDynamic || node.type !== "elementary") throw new CallciumError("NOT_SCALAR", "Value operator on a target without a scalar word.");
|
|
3113
3462
|
const result = loadScalar(callDataBytes, location);
|
|
3114
3463
|
if (!result.ok) return { error: result.code };
|
|
3115
3464
|
const value = canonicalize(toBigInt(result.value, 0), node.typeCode);
|
|
@@ -3266,6 +3615,6 @@ const PolicyEnforcer = {
|
|
|
3266
3615
|
enforce
|
|
3267
3616
|
};
|
|
3268
3617
|
//#endregion
|
|
3269
|
-
export { CallciumError, ContextProperty, Descriptor, DescriptorCoder, Limits, Op, PolicyBuilder, PolicyCoder, PolicyEnforcer, PolicyValidator, PolicyViolationError, Quantifier, Scope, TypeCode, arg, blockNumber, blockTimestamp, bytesToHex, chainId, hexToBytes, isLengthOp, isOpAllowed, isQuantifier, lookupContextProperty, lookupOp, lookupQuantifier, lookupScope, lookupTypeCode, msgSender, msgValue, parsePathSteps, toAddress, txOrigin };
|
|
3618
|
+
export { CallciumError, ContextProperty, Descriptor, DescriptorCoder, Limits, Op, PolicyBuilder, PolicyCoder, PolicyEnforcer, PolicyValidator, PolicyViolationError, Quantifier, Scope, TypeCode, arg, baseFee, blockNumber, blockTimestamp, bytesToHex, chainId, gasPrice, hexToBytes, isLengthOp, isOpAllowed, isQuantifier, lookupContextProperty, lookupOp, lookupQuantifier, lookupScope, lookupTypeCode, msgSender, msgValue, parsePathSteps, toAddress, txOrigin };
|
|
3270
3619
|
|
|
3271
3620
|
//# sourceMappingURL=index.js.map
|