@callcium/sdk 0.4.0 → 0.6.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 +41 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +665 -371
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -172,54 +172,71 @@ function lookupScope(code) {
|
|
|
172
172
|
if (!info) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${code}`);
|
|
173
173
|
return info;
|
|
174
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
|
+
};
|
|
175
192
|
const CTX_PROP_TABLE = [
|
|
176
193
|
{
|
|
177
194
|
key: "MSG_SENDER",
|
|
178
195
|
code: 0,
|
|
179
196
|
label: "msg.sender",
|
|
180
|
-
typeCode:
|
|
197
|
+
typeCode: TypeCode.ADDRESS
|
|
181
198
|
},
|
|
182
199
|
{
|
|
183
200
|
key: "MSG_VALUE",
|
|
184
201
|
code: 1,
|
|
185
202
|
label: "msg.value",
|
|
186
|
-
typeCode:
|
|
203
|
+
typeCode: TypeCode.UINT_MAX
|
|
187
204
|
},
|
|
188
205
|
{
|
|
189
206
|
key: "BLOCK_TIMESTAMP",
|
|
190
207
|
code: 2,
|
|
191
208
|
label: "block.timestamp",
|
|
192
|
-
typeCode:
|
|
209
|
+
typeCode: TypeCode.UINT_MAX
|
|
193
210
|
},
|
|
194
211
|
{
|
|
195
212
|
key: "BLOCK_NUMBER",
|
|
196
213
|
code: 3,
|
|
197
214
|
label: "block.number",
|
|
198
|
-
typeCode:
|
|
215
|
+
typeCode: TypeCode.UINT_MAX
|
|
199
216
|
},
|
|
200
217
|
{
|
|
201
218
|
key: "CHAIN_ID",
|
|
202
219
|
code: 4,
|
|
203
220
|
label: "block.chainid",
|
|
204
|
-
typeCode:
|
|
221
|
+
typeCode: TypeCode.UINT_MAX
|
|
205
222
|
},
|
|
206
223
|
{
|
|
207
224
|
key: "TX_ORIGIN",
|
|
208
225
|
code: 5,
|
|
209
226
|
label: "tx.origin",
|
|
210
|
-
typeCode:
|
|
227
|
+
typeCode: TypeCode.ADDRESS
|
|
211
228
|
},
|
|
212
229
|
{
|
|
213
230
|
key: "BASE_FEE",
|
|
214
231
|
code: 6,
|
|
215
232
|
label: "block.basefee",
|
|
216
|
-
typeCode:
|
|
233
|
+
typeCode: TypeCode.UINT_MAX
|
|
217
234
|
},
|
|
218
235
|
{
|
|
219
236
|
key: "GAS_PRICE",
|
|
220
237
|
code: 7,
|
|
221
238
|
label: "tx.gasprice",
|
|
222
|
-
typeCode:
|
|
239
|
+
typeCode: TypeCode.UINT_MAX
|
|
223
240
|
}
|
|
224
241
|
];
|
|
225
242
|
/** Well-known context property IDs for context-scope rules. */
|
|
@@ -426,23 +443,6 @@ function lookupQuantifier(code) {
|
|
|
426
443
|
if (!info) throw new CallciumError("INVALID_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
|
|
427
444
|
return info;
|
|
428
445
|
}
|
|
429
|
-
/** ABI type code ranges and sentinel values for the descriptor format. */
|
|
430
|
-
const TypeCode = {
|
|
431
|
-
UINT_MIN: 0,
|
|
432
|
-
UINT_MAX: 31,
|
|
433
|
-
INT_MIN: 32,
|
|
434
|
-
INT_MAX: 63,
|
|
435
|
-
ADDRESS: 64,
|
|
436
|
-
BOOL: 65,
|
|
437
|
-
FUNCTION: 66,
|
|
438
|
-
FIXED_BYTES_MIN: 80,
|
|
439
|
-
FIXED_BYTES_MAX: 111,
|
|
440
|
-
BYTES: 112,
|
|
441
|
-
STRING: 113,
|
|
442
|
-
STATIC_ARRAY: 128,
|
|
443
|
-
DYNAMIC_ARRAY: 129,
|
|
444
|
-
TUPLE: 144
|
|
445
|
-
};
|
|
446
446
|
/** Throw an UNKNOWN_TYPE_CODE error. */
|
|
447
447
|
function unknownTypeCode(code) {
|
|
448
448
|
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code 0x${code.toString(16).padStart(2, "0")}`);
|
|
@@ -574,7 +574,7 @@ function inspect$1(desc, offset) {
|
|
|
574
574
|
* @param desc - Raw descriptor bytes.
|
|
575
575
|
* @param steps - Path steps, length >= 1.
|
|
576
576
|
* @returns TypeInfo for the node at the resolved path.
|
|
577
|
-
* @throws {CallciumError} On empty steps, out-of-bounds param, or descent into elementary type.
|
|
577
|
+
* @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent into elementary type.
|
|
578
578
|
*/
|
|
579
579
|
function typeAt(desc, steps) {
|
|
580
580
|
return walkPath(desc, steps).typeInfo;
|
|
@@ -586,7 +586,7 @@ function typeAt(desc, steps) {
|
|
|
586
586
|
* @param desc - Raw descriptor bytes.
|
|
587
587
|
* @param steps - Path steps, length >= 1.
|
|
588
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.
|
|
589
|
+
* @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent into elementary type.
|
|
590
590
|
*/
|
|
591
591
|
function walkPath(desc, steps) {
|
|
592
592
|
if (steps.length === 0) throw new CallciumError("INVALID_PATH", "Path must have at least one step.");
|
|
@@ -603,7 +603,11 @@ function walkPath(desc, steps) {
|
|
|
603
603
|
if (step >= fields) throw new CallciumError("INVALID_PATH", `Tuple field index ${step} out of range (tuple has ${fields} fields).`);
|
|
604
604
|
cursor = tupleFieldOffset(desc, cursor, step);
|
|
605
605
|
} else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) {
|
|
606
|
-
if (typeCode === TypeCode.STATIC_ARRAY
|
|
606
|
+
if (typeCode === TypeCode.STATIC_ARRAY) if (isQuantifier(step)) quantifiedStaticLength = staticArrayLength(desc, cursor);
|
|
607
|
+
else {
|
|
608
|
+
const length = staticArrayLength(desc, cursor);
|
|
609
|
+
if (step >= length) throw new CallciumError("INVALID_PATH", `Array index ${step} out of range (length=${length}).`);
|
|
610
|
+
}
|
|
607
611
|
cursor = arrayElementOffset(cursor);
|
|
608
612
|
} else throw new CallciumError("INVALID_PATH", `Cannot descend into elementary type at offset ${cursor}.`);
|
|
609
613
|
}
|
|
@@ -807,6 +811,7 @@ function commaPositions(input, start, end) {
|
|
|
807
811
|
*/
|
|
808
812
|
function parseUint(input, start, end) {
|
|
809
813
|
if (start >= end) return -1;
|
|
814
|
+
if (end - start > 1 && input.charCodeAt(start) === 48) return -1;
|
|
810
815
|
let value = 0;
|
|
811
816
|
for (let i = start; i < end; i++) {
|
|
812
817
|
const code = input.charCodeAt(i);
|
|
@@ -1457,6 +1462,13 @@ function isSigned(typeCode) {
|
|
|
1457
1462
|
return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
|
|
1458
1463
|
}
|
|
1459
1464
|
/**
|
|
1465
|
+
* Return true when the type code represents a left-aligned type (fixed bytes or function),
|
|
1466
|
+
* whose value occupies the high bytes of the word with zero padding below.
|
|
1467
|
+
*/
|
|
1468
|
+
function isLeftAligned(typeCode) {
|
|
1469
|
+
return typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX || typeCode === TypeCode.FUNCTION;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1460
1472
|
* Canonicalize a raw 256-bit calldata word to its ABI value for the declared type (Policy Spec §7.8).
|
|
1461
1473
|
* A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
|
|
1462
1474
|
* unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
|
|
@@ -1490,6 +1502,15 @@ function isLengthOp(opCode) {
|
|
|
1490
1502
|
const base = opCode & ~Op.NOT;
|
|
1491
1503
|
return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
|
|
1492
1504
|
}
|
|
1505
|
+
/** Map a LENGTH_* opcode base to its core value-comparison twin. */
|
|
1506
|
+
function lengthToValueOp(base) {
|
|
1507
|
+
if (base === Op.LENGTH_EQ) return Op.EQ;
|
|
1508
|
+
if (base === Op.LENGTH_GT) return Op.GT;
|
|
1509
|
+
if (base === Op.LENGTH_LT) return Op.LT;
|
|
1510
|
+
if (base === Op.LENGTH_GTE) return Op.GTE;
|
|
1511
|
+
if (base === Op.LENGTH_LTE) return Op.LTE;
|
|
1512
|
+
return Op.BETWEEN;
|
|
1513
|
+
}
|
|
1493
1514
|
/** Return true when LENGTH_* operators are valid for the given type code (bytes, string, or dynamic array). */
|
|
1494
1515
|
function isLengthValidType(typeCode) {
|
|
1495
1516
|
return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
|
|
@@ -1529,7 +1550,12 @@ function isIn(value, data) {
|
|
|
1529
1550
|
*/
|
|
1530
1551
|
function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
1531
1552
|
const negate = (opCode & Op.NOT) !== 0;
|
|
1532
|
-
|
|
1553
|
+
let base = opCode & ~Op.NOT;
|
|
1554
|
+
if (isLengthOp(base)) {
|
|
1555
|
+
value = BigInt(valueLength);
|
|
1556
|
+
typeCode = TypeCode.UINT_MAX;
|
|
1557
|
+
base = lengthToValueOp(base);
|
|
1558
|
+
}
|
|
1533
1559
|
let result;
|
|
1534
1560
|
switch (base) {
|
|
1535
1561
|
case Op.EQ:
|
|
@@ -1561,32 +1587,14 @@ function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
|
1561
1587
|
result = (value & mask) === mask;
|
|
1562
1588
|
break;
|
|
1563
1589
|
}
|
|
1564
|
-
case Op.BITMASK_ANY:
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
case Op.BITMASK_NONE:
|
|
1568
|
-
result = (value & toBigInt(operandData, 0)) === 0n;
|
|
1569
|
-
break;
|
|
1570
|
-
case Op.LENGTH_EQ:
|
|
1571
|
-
result = BigInt(valueLength) === toBigInt(operandData, 0);
|
|
1572
|
-
break;
|
|
1573
|
-
case Op.LENGTH_GT:
|
|
1574
|
-
result = BigInt(valueLength) > toBigInt(operandData, 0);
|
|
1575
|
-
break;
|
|
1576
|
-
case Op.LENGTH_LT:
|
|
1577
|
-
result = BigInt(valueLength) < toBigInt(operandData, 0);
|
|
1578
|
-
break;
|
|
1579
|
-
case Op.LENGTH_GTE:
|
|
1580
|
-
result = BigInt(valueLength) >= toBigInt(operandData, 0);
|
|
1581
|
-
break;
|
|
1582
|
-
case Op.LENGTH_LTE:
|
|
1583
|
-
result = BigInt(valueLength) <= toBigInt(operandData, 0);
|
|
1590
|
+
case Op.BITMASK_ANY: {
|
|
1591
|
+
const mask = toBigInt(operandData, 0);
|
|
1592
|
+
result = (value & mask) !== 0n;
|
|
1584
1593
|
break;
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1589
|
-
result = length >= min && length <= max;
|
|
1594
|
+
}
|
|
1595
|
+
case Op.BITMASK_NONE: {
|
|
1596
|
+
const mask = toBigInt(operandData, 0);
|
|
1597
|
+
result = (value & mask) === 0n;
|
|
1590
1598
|
break;
|
|
1591
1599
|
}
|
|
1592
1600
|
default: throw new CallciumError("INVALID_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
|
|
@@ -1596,230 +1604,435 @@ function applyOperator(opCode, value, valueLength, operandData, typeCode) {
|
|
|
1596
1604
|
//#endregion
|
|
1597
1605
|
//#region src/validation-issue.ts
|
|
1598
1606
|
const ZERO = `0x${"0".repeat(64)}`;
|
|
1599
|
-
/**
|
|
1600
|
-
function
|
|
1607
|
+
/** Create an issue from operator compatibility results. */
|
|
1608
|
+
function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
|
|
1601
1609
|
return {
|
|
1602
|
-
severity,
|
|
1603
|
-
category,
|
|
1610
|
+
severity: "error",
|
|
1611
|
+
category: "typeMismatch",
|
|
1604
1612
|
groupIndex,
|
|
1605
1613
|
constraintIndex,
|
|
1606
1614
|
code,
|
|
1607
|
-
value1,
|
|
1608
|
-
value2,
|
|
1615
|
+
value1: opCode,
|
|
1616
|
+
value2: ZERO,
|
|
1609
1617
|
message
|
|
1610
1618
|
};
|
|
1611
1619
|
}
|
|
1612
|
-
/**
|
|
1613
|
-
function
|
|
1614
|
-
return
|
|
1620
|
+
/** Issue for a calldata path that does not navigate the descriptor. */
|
|
1621
|
+
function unnavigablePath(groupIndex, constraintIndex) {
|
|
1622
|
+
return {
|
|
1623
|
+
severity: "error",
|
|
1624
|
+
category: "typeMismatch",
|
|
1625
|
+
groupIndex,
|
|
1626
|
+
constraintIndex,
|
|
1627
|
+
code: "UNNAVIGABLE_PATH",
|
|
1628
|
+
value1: ZERO,
|
|
1629
|
+
value2: ZERO,
|
|
1630
|
+
message: "Path does not navigate the descriptor"
|
|
1631
|
+
};
|
|
1615
1632
|
}
|
|
1616
|
-
/**
|
|
1617
|
-
function
|
|
1618
|
-
return
|
|
1633
|
+
/** Issue for an operand that is not canonically encoded for the declared type. */
|
|
1634
|
+
function nonCanonicalOperand(groupIndex, constraintIndex, operand, canonical) {
|
|
1635
|
+
return {
|
|
1636
|
+
severity: "error",
|
|
1637
|
+
category: "typeMismatch",
|
|
1638
|
+
groupIndex,
|
|
1639
|
+
constraintIndex,
|
|
1640
|
+
code: "NON_CANONICAL_OPERAND",
|
|
1641
|
+
value1: operand,
|
|
1642
|
+
value2: canonical,
|
|
1643
|
+
message: "Operand is not the canonical encoding for the declared type"
|
|
1644
|
+
};
|
|
1619
1645
|
}
|
|
1620
|
-
/**
|
|
1621
|
-
function
|
|
1622
|
-
return
|
|
1646
|
+
/** eq(v)/neq(v) or lengthEq(v)/lengthNeq(v) on the same path. */
|
|
1647
|
+
function eqNeqContradiction(isLength, groupIndex, constraintIndex, value) {
|
|
1648
|
+
return {
|
|
1649
|
+
severity: "error",
|
|
1650
|
+
category: "contradiction",
|
|
1651
|
+
groupIndex,
|
|
1652
|
+
constraintIndex,
|
|
1653
|
+
code: isLength ? "LENGTH_EQ_NEQ_CONTRADICTION" : "EQ_NEQ_CONTRADICTION",
|
|
1654
|
+
value1: value,
|
|
1655
|
+
value2: ZERO,
|
|
1656
|
+
message: isLength ? "lengthEq(v) and lengthNeq(v) on same path" : "eq(v) and neq(v) on same path"
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
/** Multiple eq()/lengthEq() operators with different values. */
|
|
1660
|
+
function conflictingEquality(isLength, groupIndex, constraintIndex, existing, newValue) {
|
|
1661
|
+
return {
|
|
1662
|
+
severity: "error",
|
|
1663
|
+
category: "contradiction",
|
|
1664
|
+
groupIndex,
|
|
1665
|
+
constraintIndex,
|
|
1666
|
+
code: isLength ? "CONFLICTING_LENGTH" : "CONFLICTING_EQUALITY",
|
|
1667
|
+
value1: existing,
|
|
1668
|
+
value2: newValue,
|
|
1669
|
+
message: isLength ? "Multiple lengthEq() operators with different values" : "Multiple eq() operators with different values"
|
|
1670
|
+
};
|
|
1623
1671
|
}
|
|
1624
|
-
/** Value is outside the physical range of
|
|
1625
|
-
function outOfPhysicalBounds(groupIndex, constraintIndex, value) {
|
|
1626
|
-
return
|
|
1672
|
+
/** Value/length is outside the physical range of its type. */
|
|
1673
|
+
function outOfPhysicalBounds(isLength, groupIndex, constraintIndex, value) {
|
|
1674
|
+
return {
|
|
1675
|
+
severity: "error",
|
|
1676
|
+
category: "contradiction",
|
|
1677
|
+
groupIndex,
|
|
1678
|
+
constraintIndex,
|
|
1679
|
+
code: isLength ? "OUT_OF_PHYSICAL_LENGTH_BOUNDS" : "OUT_OF_PHYSICAL_BOUNDS",
|
|
1680
|
+
value1: value,
|
|
1681
|
+
value2: ZERO,
|
|
1682
|
+
message: isLength ? "Length value is outside the physical range" : "Value is outside the physical range of the type"
|
|
1683
|
+
};
|
|
1627
1684
|
}
|
|
1628
|
-
/** gt() on
|
|
1629
|
-
function impossibleGt(groupIndex, constraintIndex, value) {
|
|
1630
|
-
return
|
|
1685
|
+
/** gt()/lengthGt() on the maximum is impossible. */
|
|
1686
|
+
function impossibleGt(isLength, groupIndex, constraintIndex, value) {
|
|
1687
|
+
return {
|
|
1688
|
+
severity: "error",
|
|
1689
|
+
category: "contradiction",
|
|
1690
|
+
groupIndex,
|
|
1691
|
+
constraintIndex,
|
|
1692
|
+
code: isLength ? "IMPOSSIBLE_LENGTH_GT" : "IMPOSSIBLE_GT",
|
|
1693
|
+
value1: value,
|
|
1694
|
+
value2: ZERO,
|
|
1695
|
+
message: isLength ? "lengthGt() on maximum length is impossible" : "gt() on type maximum is impossible"
|
|
1696
|
+
};
|
|
1631
1697
|
}
|
|
1632
|
-
/** lt() on
|
|
1633
|
-
function impossibleLt(groupIndex, constraintIndex, value) {
|
|
1634
|
-
return
|
|
1698
|
+
/** lt()/lengthLt() on the minimum is impossible. */
|
|
1699
|
+
function impossibleLt(isLength, groupIndex, constraintIndex, value) {
|
|
1700
|
+
return {
|
|
1701
|
+
severity: "error",
|
|
1702
|
+
category: "contradiction",
|
|
1703
|
+
groupIndex,
|
|
1704
|
+
constraintIndex,
|
|
1705
|
+
code: isLength ? "IMPOSSIBLE_LENGTH_LT" : "IMPOSSIBLE_LT",
|
|
1706
|
+
value1: value,
|
|
1707
|
+
value2: ZERO,
|
|
1708
|
+
message: isLength ? "lengthLt() on minimum length is impossible" : "lt() on type minimum is impossible"
|
|
1709
|
+
};
|
|
1635
1710
|
}
|
|
1636
|
-
/** eq() value is excluded by bound. */
|
|
1637
|
-
function boundsExcludeEquality(groupIndex, constraintIndex, eq, bound) {
|
|
1638
|
-
return
|
|
1711
|
+
/** eq()/lengthEq() value is excluded by a bound. */
|
|
1712
|
+
function boundsExcludeEquality(isLength, groupIndex, constraintIndex, eq, bound) {
|
|
1713
|
+
return {
|
|
1714
|
+
severity: "error",
|
|
1715
|
+
category: "contradiction",
|
|
1716
|
+
groupIndex,
|
|
1717
|
+
constraintIndex,
|
|
1718
|
+
code: isLength ? "BOUNDS_EXCLUDE_LENGTH" : "BOUNDS_EXCLUDE_EQUALITY",
|
|
1719
|
+
value1: eq,
|
|
1720
|
+
value2: bound,
|
|
1721
|
+
message: isLength ? "lengthEq() value is excluded by bound" : "eq() value is excluded by bound"
|
|
1722
|
+
};
|
|
1639
1723
|
}
|
|
1640
|
-
/** Lower bound exceeds upper bound. */
|
|
1641
|
-
function impossibleRange(groupIndex, constraintIndex, lower, upper) {
|
|
1642
|
-
return
|
|
1724
|
+
/** Lower bound/length exceeds the upper bound. */
|
|
1725
|
+
function impossibleRange(isLength, groupIndex, constraintIndex, lower, upper) {
|
|
1726
|
+
return {
|
|
1727
|
+
severity: "error",
|
|
1728
|
+
category: "contradiction",
|
|
1729
|
+
groupIndex,
|
|
1730
|
+
constraintIndex,
|
|
1731
|
+
code: isLength ? "IMPOSSIBLE_LENGTH_RANGE" : "IMPOSSIBLE_RANGE",
|
|
1732
|
+
value1: lower,
|
|
1733
|
+
value2: upper,
|
|
1734
|
+
message: isLength ? "Lower length bound exceeds upper bound" : "Lower bound exceeds upper bound"
|
|
1735
|
+
};
|
|
1643
1736
|
}
|
|
1644
1737
|
/** Set operation excludes existing eq() value. */
|
|
1645
1738
|
function setExcludesEquality(groupIndex, constraintIndex, value) {
|
|
1646
|
-
return
|
|
1739
|
+
return {
|
|
1740
|
+
severity: "error",
|
|
1741
|
+
category: "contradiction",
|
|
1742
|
+
groupIndex,
|
|
1743
|
+
constraintIndex,
|
|
1744
|
+
code: "SET_EXCLUDES_EQUALITY",
|
|
1745
|
+
value1: value,
|
|
1746
|
+
value2: ZERO,
|
|
1747
|
+
message: "Set operation excludes existing eq() value"
|
|
1748
|
+
};
|
|
1647
1749
|
}
|
|
1648
1750
|
/** Multiple isIn() sets have no intersection. */
|
|
1649
1751
|
function emptySetIntersection(groupIndex, constraintIndex) {
|
|
1650
|
-
return
|
|
1752
|
+
return {
|
|
1753
|
+
severity: "error",
|
|
1754
|
+
category: "contradiction",
|
|
1755
|
+
groupIndex,
|
|
1756
|
+
constraintIndex,
|
|
1757
|
+
code: "EMPTY_SET_INTERSECTION",
|
|
1758
|
+
value1: ZERO,
|
|
1759
|
+
value2: ZERO,
|
|
1760
|
+
message: "Multiple isIn() sets have no intersection"
|
|
1761
|
+
};
|
|
1651
1762
|
}
|
|
1652
1763
|
/** All values in isIn() set are excluded by neq/notIn. */
|
|
1653
1764
|
function setFullyExcluded(groupIndex, constraintIndex) {
|
|
1654
|
-
return
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
}
|
|
1664
|
-
/** lengthEq() value is excluded by bound. */
|
|
1665
|
-
function boundsExcludeLength(groupIndex, constraintIndex, eq, bound) {
|
|
1666
|
-
return makeIssue("error", "contradiction", groupIndex, constraintIndex, "BOUNDS_EXCLUDE_LENGTH", eq, bound, "lengthEq() value is excluded by bound");
|
|
1667
|
-
}
|
|
1668
|
-
/** Lower length bound exceeds upper bound. */
|
|
1669
|
-
function impossibleLengthRange(groupIndex, constraintIndex, lower, upper) {
|
|
1670
|
-
return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LENGTH_RANGE", lower, upper, "Lower length bound exceeds upper bound");
|
|
1671
|
-
}
|
|
1672
|
-
/** Length value is outside the physical range. */
|
|
1673
|
-
function outOfPhysicalLengthBounds(groupIndex, constraintIndex, value) {
|
|
1674
|
-
return makeIssue("error", "contradiction", groupIndex, constraintIndex, "OUT_OF_PHYSICAL_LENGTH_BOUNDS", value, ZERO, "Length value is outside the physical range");
|
|
1675
|
-
}
|
|
1676
|
-
/** lengthGt() on maximum length is impossible. */
|
|
1677
|
-
function impossibleLengthGt(groupIndex, constraintIndex, value) {
|
|
1678
|
-
return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LENGTH_GT", value, ZERO, "lengthGt() on maximum length is impossible");
|
|
1679
|
-
}
|
|
1680
|
-
/** lengthLt() on minimum length is impossible. */
|
|
1681
|
-
function impossibleLengthLt(groupIndex, constraintIndex, value) {
|
|
1682
|
-
return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LENGTH_LT", value, ZERO, "lengthLt() on minimum length is impossible");
|
|
1765
|
+
return {
|
|
1766
|
+
severity: "error",
|
|
1767
|
+
category: "contradiction",
|
|
1768
|
+
groupIndex,
|
|
1769
|
+
constraintIndex,
|
|
1770
|
+
code: "SET_FULLY_EXCLUDED",
|
|
1771
|
+
value1: ZERO,
|
|
1772
|
+
value2: ZERO,
|
|
1773
|
+
message: "All values in isIn() set are excluded by neq/notIn"
|
|
1774
|
+
};
|
|
1683
1775
|
}
|
|
1684
1776
|
/** Bitmask operators conflict. */
|
|
1685
1777
|
function bitmaskContradiction(groupIndex, constraintIndex, mask, conflicting) {
|
|
1686
|
-
return
|
|
1778
|
+
return {
|
|
1779
|
+
severity: "error",
|
|
1780
|
+
category: "contradiction",
|
|
1781
|
+
groupIndex,
|
|
1782
|
+
constraintIndex,
|
|
1783
|
+
code: "BITMASK_CONTRADICTION",
|
|
1784
|
+
value1: mask,
|
|
1785
|
+
value2: conflicting,
|
|
1786
|
+
message: "Bitmask operators conflict"
|
|
1787
|
+
};
|
|
1687
1788
|
}
|
|
1688
1789
|
/** bitmaskAny is impossible because all bits are forbidden. */
|
|
1689
1790
|
function bitmaskAnyImpossible(groupIndex, constraintIndex, mask, mustBeZero) {
|
|
1690
|
-
return
|
|
1791
|
+
return {
|
|
1792
|
+
severity: "error",
|
|
1793
|
+
category: "contradiction",
|
|
1794
|
+
groupIndex,
|
|
1795
|
+
constraintIndex,
|
|
1796
|
+
code: "BITMASK_ANY_IMPOSSIBLE",
|
|
1797
|
+
value1: mask,
|
|
1798
|
+
value2: mustBeZero,
|
|
1799
|
+
message: "bitmaskAny is impossible because all bits are forbidden"
|
|
1800
|
+
};
|
|
1691
1801
|
}
|
|
1692
1802
|
/** isIn/notIn set is not strictly sorted and deduplicated. */
|
|
1693
1803
|
function unsortedInSet(groupIndex, constraintIndex) {
|
|
1694
|
-
return
|
|
1804
|
+
return {
|
|
1805
|
+
severity: "error",
|
|
1806
|
+
category: "contradiction",
|
|
1807
|
+
groupIndex,
|
|
1808
|
+
constraintIndex,
|
|
1809
|
+
code: "UNSORTED_IN_SET",
|
|
1810
|
+
value1: ZERO,
|
|
1811
|
+
value2: ZERO,
|
|
1812
|
+
message: "isIn/notIn set is not strictly sorted and deduplicated."
|
|
1813
|
+
};
|
|
1695
1814
|
}
|
|
1696
1815
|
/** Group contains zero constraints. */
|
|
1697
1816
|
function emptyGroup(groupIndex) {
|
|
1698
|
-
return
|
|
1817
|
+
return {
|
|
1818
|
+
severity: "error",
|
|
1819
|
+
category: "vacuity",
|
|
1820
|
+
groupIndex,
|
|
1821
|
+
constraintIndex: 0,
|
|
1822
|
+
code: "EMPTY_GROUP",
|
|
1823
|
+
value1: ZERO,
|
|
1824
|
+
value2: ZERO,
|
|
1825
|
+
message: "Group contains zero constraints."
|
|
1826
|
+
};
|
|
1699
1827
|
}
|
|
1700
|
-
/** Numeric bound
|
|
1701
|
-
function dominatedBound(groupIndex, constraintIndex, value) {
|
|
1702
|
-
return
|
|
1828
|
+
/** Numeric/length bound dominated by a stricter bound. */
|
|
1829
|
+
function dominatedBound(isLength, groupIndex, constraintIndex, value) {
|
|
1830
|
+
return {
|
|
1831
|
+
severity: "warning",
|
|
1832
|
+
category: "redundancy",
|
|
1833
|
+
groupIndex,
|
|
1834
|
+
constraintIndex,
|
|
1835
|
+
code: isLength ? "DOMINATED_LENGTH_BOUND" : "DOMINATED_BOUND",
|
|
1836
|
+
value1: value,
|
|
1837
|
+
value2: ZERO,
|
|
1838
|
+
message: isLength ? "Length bound is dominated by a stricter bound" : "Numeric bound is dominated by a stricter bound"
|
|
1839
|
+
};
|
|
1703
1840
|
}
|
|
1704
|
-
/** Bound
|
|
1705
|
-
function redundantBound(groupIndex, constraintIndex, bound, eq) {
|
|
1706
|
-
return
|
|
1841
|
+
/** Bound redundant because eq()/lengthEq() is set. */
|
|
1842
|
+
function redundantBound(isLength, groupIndex, constraintIndex, bound, eq) {
|
|
1843
|
+
return {
|
|
1844
|
+
severity: "warning",
|
|
1845
|
+
category: "redundancy",
|
|
1846
|
+
groupIndex,
|
|
1847
|
+
constraintIndex,
|
|
1848
|
+
code: isLength ? "REDUNDANT_LENGTH_BOUND" : "REDUNDANT_BOUND",
|
|
1849
|
+
value1: bound,
|
|
1850
|
+
value2: eq,
|
|
1851
|
+
message: isLength ? "Length bound is redundant because lengthEq() is set" : "Bound is redundant because eq() is set"
|
|
1852
|
+
};
|
|
1707
1853
|
}
|
|
1708
1854
|
/** notIn() value was present in isIn() set. */
|
|
1709
1855
|
function setReduction(groupIndex, constraintIndex, value) {
|
|
1710
|
-
return
|
|
1856
|
+
return {
|
|
1857
|
+
severity: "warning",
|
|
1858
|
+
category: "redundancy",
|
|
1859
|
+
groupIndex,
|
|
1860
|
+
constraintIndex,
|
|
1861
|
+
code: "SET_REDUCTION",
|
|
1862
|
+
value1: value,
|
|
1863
|
+
value2: ZERO,
|
|
1864
|
+
message: "notIn() value was present in isIn() set"
|
|
1865
|
+
};
|
|
1711
1866
|
}
|
|
1712
1867
|
/** isIn() sets partially overlap. */
|
|
1713
1868
|
function setRedundancy(groupIndex, constraintIndex, intersectionCount) {
|
|
1714
|
-
return
|
|
1869
|
+
return {
|
|
1870
|
+
severity: "warning",
|
|
1871
|
+
category: "redundancy",
|
|
1872
|
+
groupIndex,
|
|
1873
|
+
constraintIndex,
|
|
1874
|
+
code: "SET_REDUNDANCY",
|
|
1875
|
+
value1: intersectionCount,
|
|
1876
|
+
value2: ZERO,
|
|
1877
|
+
message: "isIn() sets partially overlap"
|
|
1878
|
+
};
|
|
1715
1879
|
}
|
|
1716
1880
|
/** Some values in isIn() set are excluded by neq/notIn. */
|
|
1717
1881
|
function setPartiallyExcluded(groupIndex, constraintIndex, excludedCount) {
|
|
1718
|
-
return
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1882
|
+
return {
|
|
1883
|
+
severity: "warning",
|
|
1884
|
+
category: "redundancy",
|
|
1885
|
+
groupIndex,
|
|
1886
|
+
constraintIndex,
|
|
1887
|
+
code: "SET_PARTIALLY_EXCLUDED",
|
|
1888
|
+
value1: excludedCount,
|
|
1889
|
+
value2: ZERO,
|
|
1890
|
+
message: "Some values in isIn() set are excluded by neq/notIn"
|
|
1891
|
+
};
|
|
1727
1892
|
}
|
|
1728
1893
|
/** Bitmask operation is redundant. */
|
|
1729
1894
|
function redundantBitmask(groupIndex, constraintIndex, mask, existing) {
|
|
1730
|
-
return
|
|
1895
|
+
return {
|
|
1896
|
+
severity: "warning",
|
|
1897
|
+
category: "redundancy",
|
|
1898
|
+
groupIndex,
|
|
1899
|
+
constraintIndex,
|
|
1900
|
+
code: "REDUNDANT_BITMASK",
|
|
1901
|
+
value1: mask,
|
|
1902
|
+
value2: existing,
|
|
1903
|
+
message: "Bitmask operation is redundant"
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
/** gte/lte pair fusible into a single range operator. */
|
|
1907
|
+
function fusibleRange(isLength, groupIndex, constraintIndex, low, high) {
|
|
1908
|
+
return {
|
|
1909
|
+
severity: "warning",
|
|
1910
|
+
category: "redundancy",
|
|
1911
|
+
groupIndex,
|
|
1912
|
+
constraintIndex,
|
|
1913
|
+
code: isLength ? "FUSIBLE_LENGTH_RANGE" : "FUSIBLE_RANGE",
|
|
1914
|
+
value1: low,
|
|
1915
|
+
value2: high,
|
|
1916
|
+
message: isLength ? "lengthGte() and lengthLte() fuse into a single lengthBetween()" : "gte() and lte() fuse into a single between()"
|
|
1917
|
+
};
|
|
1731
1918
|
}
|
|
1732
1919
|
/** Duplicate operator in constraint. */
|
|
1733
1920
|
function duplicateConstraint(groupIndex, constraintIndex) {
|
|
1734
|
-
return
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1921
|
+
return {
|
|
1922
|
+
severity: "warning",
|
|
1923
|
+
category: "redundancy",
|
|
1924
|
+
groupIndex,
|
|
1925
|
+
constraintIndex,
|
|
1926
|
+
code: "DUPLICATE_CONSTRAINT",
|
|
1927
|
+
value1: ZERO,
|
|
1928
|
+
value2: ZERO,
|
|
1929
|
+
message: "Duplicate operator in constraint"
|
|
1930
|
+
};
|
|
1739
1931
|
}
|
|
1740
|
-
/**
|
|
1741
|
-
function
|
|
1742
|
-
return
|
|
1932
|
+
/** gte(min)/lengthGte(0) is always true. */
|
|
1933
|
+
function vacuousGte(isLength, groupIndex, constraintIndex, value) {
|
|
1934
|
+
return {
|
|
1935
|
+
severity: "info",
|
|
1936
|
+
category: "vacuity",
|
|
1937
|
+
groupIndex,
|
|
1938
|
+
constraintIndex,
|
|
1939
|
+
code: isLength ? "VACUOUS_LENGTH_GTE" : "VACUOUS_GTE",
|
|
1940
|
+
value1: value,
|
|
1941
|
+
value2: ZERO,
|
|
1942
|
+
message: isLength ? "lengthGte(0) is always true" : "gte() bound equals type minimum (always true)"
|
|
1943
|
+
};
|
|
1743
1944
|
}
|
|
1744
|
-
/**
|
|
1745
|
-
function
|
|
1746
|
-
return
|
|
1945
|
+
/** lte(max)/lengthLte(max) is always true. */
|
|
1946
|
+
function vacuousLte(isLength, groupIndex, constraintIndex, value) {
|
|
1947
|
+
return {
|
|
1948
|
+
severity: "info",
|
|
1949
|
+
category: "vacuity",
|
|
1950
|
+
groupIndex,
|
|
1951
|
+
constraintIndex,
|
|
1952
|
+
code: isLength ? "VACUOUS_LENGTH_LTE" : "VACUOUS_LTE",
|
|
1953
|
+
value1: value,
|
|
1954
|
+
value2: ZERO,
|
|
1955
|
+
message: isLength ? "lengthLte() bound equals maximum (always true)" : "lte() bound equals type maximum (always true)"
|
|
1956
|
+
};
|
|
1747
1957
|
}
|
|
1748
|
-
/**
|
|
1749
|
-
function
|
|
1750
|
-
return
|
|
1958
|
+
/** Negated range with inverted bounds excludes nothing (always true). */
|
|
1959
|
+
function vacuousNegatedRange(isLength, groupIndex, constraintIndex, low, high) {
|
|
1960
|
+
return {
|
|
1961
|
+
severity: "info",
|
|
1962
|
+
category: "vacuity",
|
|
1963
|
+
groupIndex,
|
|
1964
|
+
constraintIndex,
|
|
1965
|
+
code: isLength ? "VACUOUS_NEGATED_LENGTH_RANGE" : "VACUOUS_NEGATED_RANGE",
|
|
1966
|
+
value1: low,
|
|
1967
|
+
value2: high,
|
|
1968
|
+
message: isLength ? "Negated lengthBetween() with low > high excludes nothing (always true)" : "Negated between() with low > high excludes nothing (always true)"
|
|
1969
|
+
};
|
|
1751
1970
|
}
|
|
1752
1971
|
/** Path deeper than the reference enforcer's cap. */
|
|
1753
1972
|
function pathDepthExceeded(groupIndex, constraintIndex, depth, maxDepth) {
|
|
1754
|
-
return
|
|
1973
|
+
return {
|
|
1974
|
+
severity: "warning",
|
|
1975
|
+
category: "compatibility",
|
|
1976
|
+
groupIndex,
|
|
1977
|
+
constraintIndex,
|
|
1978
|
+
code: "PATH_DEPTH_EXCEEDED",
|
|
1979
|
+
value1: depth,
|
|
1980
|
+
value2: maxDepth,
|
|
1981
|
+
message: "Path depth exceeds the reference enforcer cap"
|
|
1982
|
+
};
|
|
1755
1983
|
}
|
|
1756
1984
|
/** Quantifier over a static array beyond the reference enforcer's iteration cap. */
|
|
1757
1985
|
function quantifierOverStaticLimit(groupIndex, constraintIndex, arrayLength, maxLength) {
|
|
1758
|
-
return
|
|
1986
|
+
return {
|
|
1987
|
+
severity: "warning",
|
|
1988
|
+
category: "compatibility",
|
|
1989
|
+
groupIndex,
|
|
1990
|
+
constraintIndex,
|
|
1991
|
+
code: "QUANTIFIER_OVER_STATIC_LIMIT",
|
|
1992
|
+
value1: arrayLength,
|
|
1993
|
+
value2: maxLength,
|
|
1994
|
+
message: "Quantifier over static array exceeds the reference enforcer cap"
|
|
1995
|
+
};
|
|
1759
1996
|
}
|
|
1760
1997
|
/** Context property ID outside the assigned set. */
|
|
1761
1998
|
function unknownContextProperty(groupIndex, constraintIndex, contextId, maxContextId) {
|
|
1762
|
-
return
|
|
1999
|
+
return {
|
|
2000
|
+
severity: "warning",
|
|
2001
|
+
category: "compatibility",
|
|
2002
|
+
groupIndex,
|
|
2003
|
+
constraintIndex,
|
|
2004
|
+
code: "UNKNOWN_CONTEXT_PROPERTY",
|
|
2005
|
+
value1: contextId,
|
|
2006
|
+
value2: maxContextId,
|
|
2007
|
+
message: "Unknown context property ID"
|
|
2008
|
+
};
|
|
1763
2009
|
}
|
|
1764
2010
|
/** Negated operator under the existential quantifier. */
|
|
1765
2011
|
function negationUnderAny(groupIndex, constraintIndex, opCode) {
|
|
1766
|
-
return makeIssue("warning", "compatibility", groupIndex, constraintIndex, "NEGATION_UNDER_ANY", opCode, ZERO, "Negated operator under any() passes when a decoy element differs");
|
|
1767
|
-
}
|
|
1768
|
-
/** Pre-select value or length issue factories for a bound domain. */
|
|
1769
|
-
function boundIssues(isLength) {
|
|
1770
2012
|
return {
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
redundant: isLength ? redundantLengthBound : redundantBound,
|
|
1780
|
-
vacuousGte: isLength ? vacuousLengthGte : vacuousGte,
|
|
1781
|
-
vacuousLte: isLength ? vacuousLengthLte : vacuousLte
|
|
2013
|
+
severity: "warning",
|
|
2014
|
+
category: "compatibility",
|
|
2015
|
+
groupIndex,
|
|
2016
|
+
constraintIndex,
|
|
2017
|
+
code: "NEGATION_UNDER_ANY",
|
|
2018
|
+
value1: opCode,
|
|
2019
|
+
value2: ZERO,
|
|
2020
|
+
message: "Negated operator under any() passes when a decoy element differs"
|
|
1782
2021
|
};
|
|
1783
2022
|
}
|
|
1784
2023
|
//#endregion
|
|
1785
2024
|
//#region src/policy-validator.ts
|
|
1786
2025
|
/**
|
|
1787
|
-
* Maximum
|
|
1788
|
-
* domain. A hole is a single value excluded via a negated equality (neq).
|
|
1789
|
-
* During cross-operator analysis, holes punch values out of isIn sets — if
|
|
1790
|
-
* every member of an isIn set is excluded by holes or notIn, the constraint
|
|
1791
|
-
* is unsatisfiable (SET_FULLY_EXCLUDED). When only some are excluded, the
|
|
1792
|
-
* validator emits SET_PARTIALLY_EXCLUDED.
|
|
1793
|
-
*
|
|
1794
|
-
* Both caps are protocol-defined limits. Beyond these limits,
|
|
1795
|
-
* additional exclusions are silently ignored — the validator becomes
|
|
1796
|
-
* best-effort for set-reduction diagnostics but never produces false
|
|
1797
|
-
* positives, only potential false negatives (missed redundancy warnings).
|
|
2026
|
+
* Maximum length value for dynamic types.
|
|
1798
2027
|
*/
|
|
1799
|
-
const MAX_HOLES = 8;
|
|
1800
|
-
const MAX_NOT_IN = 8;
|
|
1801
2028
|
const LENGTH_MAX = 4294967295n;
|
|
1802
2029
|
const UINT256_MAX = (1n << 256n) - 1n;
|
|
1803
2030
|
const INT256_SIGN_BIT = 1n << 255n;
|
|
1804
|
-
/**
|
|
1805
|
-
function
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
function isGt(a, b, signed) {
|
|
1810
|
-
return signed ? toSigned(a) > toSigned(b) : a > b;
|
|
1811
|
-
}
|
|
1812
|
-
/** a >= b. */
|
|
1813
|
-
function isGte(a, b, signed) {
|
|
1814
|
-
return signed ? toSigned(a) >= toSigned(b) : a >= b;
|
|
1815
|
-
}
|
|
1816
|
-
/** a < b. */
|
|
1817
|
-
function isLt(a, b, signed) {
|
|
1818
|
-
return signed ? toSigned(a) < toSigned(b) : a < b;
|
|
1819
|
-
}
|
|
1820
|
-
/** a <= b. */
|
|
1821
|
-
function isLte(a, b, signed) {
|
|
1822
|
-
return signed ? toSigned(a) <= toSigned(b) : a <= b;
|
|
2031
|
+
/** Signed-aware difference of two raw uint256 values: negative if a<b, zero if equal, positive if a>b. */
|
|
2032
|
+
function signedCompare(a, b, signed) {
|
|
2033
|
+
if (!signed) return a - b;
|
|
2034
|
+
const toS = (v) => v >= INT256_SIGN_BIT ? v - (1n << 256n) : v;
|
|
2035
|
+
return toS(a) - toS(b);
|
|
1823
2036
|
}
|
|
1824
2037
|
/** True for uint and int type codes. */
|
|
1825
2038
|
function isNumericType(typeCode) {
|
|
@@ -1914,22 +2127,6 @@ function getIncompat(opBase, typeInfo) {
|
|
|
1914
2127
|
message: "Unknown operator code"
|
|
1915
2128
|
};
|
|
1916
2129
|
}
|
|
1917
|
-
/** Check operator–type compatibility and return an issue descriptor on mismatch. */
|
|
1918
|
-
function checkCompatibility(opBase, typeCode, isDynamic, staticSize) {
|
|
1919
|
-
const incompat = getIncompat(opBase, {
|
|
1920
|
-
typeCode,
|
|
1921
|
-
isDynamic,
|
|
1922
|
-
staticSize
|
|
1923
|
-
});
|
|
1924
|
-
return incompat ? {
|
|
1925
|
-
compatible: false,
|
|
1926
|
-
...incompat
|
|
1927
|
-
} : {
|
|
1928
|
-
compatible: true,
|
|
1929
|
-
code: "",
|
|
1930
|
-
message: ""
|
|
1931
|
-
};
|
|
1932
|
-
}
|
|
1933
2130
|
/**
|
|
1934
2131
|
* True if operator `opCode` is allowed on a target with the given type info.
|
|
1935
2132
|
* Tolerates the NOT flag on `opCode`.
|
|
@@ -1941,6 +2138,19 @@ function isOpAllowed(opCode, typeInfo) {
|
|
|
1941
2138
|
function readValue(opHex) {
|
|
1942
2139
|
return BigInt(`0x${opHex.slice(4)}`);
|
|
1943
2140
|
}
|
|
2141
|
+
/** First payload word deviating from the canonical encoding for the type, with its canonical form, or null. */
|
|
2142
|
+
function findNonCanonicalWord(opHex, typeCode) {
|
|
2143
|
+
const body = opHex.slice(4);
|
|
2144
|
+
for (let i = 0; i + 64 <= body.length; i += 64) {
|
|
2145
|
+
const word = BigInt(`0x${body.slice(i, i + 64)}`);
|
|
2146
|
+
const canonical = canonicalize(word, typeCode);
|
|
2147
|
+
if (canonical !== word) return {
|
|
2148
|
+
word,
|
|
2149
|
+
canonical
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
1944
2154
|
/** Read a pair of uint256 values from a BETWEEN operator. */
|
|
1945
2155
|
function readPair(opHex) {
|
|
1946
2156
|
const body = opHex.slice(4);
|
|
@@ -1979,6 +2189,23 @@ function negateBoundOp(base) {
|
|
|
1979
2189
|
if (base === Op.LTE) return Op.GT;
|
|
1980
2190
|
return base;
|
|
1981
2191
|
}
|
|
2192
|
+
/** Fresh bound domain with no constraints recorded yet. */
|
|
2193
|
+
function emptyBoundDomain(boundIsSigned, min, max) {
|
|
2194
|
+
return {
|
|
2195
|
+
isSigned: boundIsSigned,
|
|
2196
|
+
min,
|
|
2197
|
+
max,
|
|
2198
|
+
hasEq: false,
|
|
2199
|
+
eq: 0n,
|
|
2200
|
+
hasLower: false,
|
|
2201
|
+
lower: 0n,
|
|
2202
|
+
lowerInclusive: false,
|
|
2203
|
+
hasUpper: false,
|
|
2204
|
+
upper: 0n,
|
|
2205
|
+
upperInclusive: false,
|
|
2206
|
+
holes: []
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
1982
2209
|
/** Create a fresh constraint context with domain limits from the type. */
|
|
1983
2210
|
function initContext(scope, path, typeInfo) {
|
|
1984
2211
|
const { min, max } = getDomainLimits(typeInfo.typeCode);
|
|
@@ -1986,38 +2213,12 @@ function initContext(scope, path, typeInfo) {
|
|
|
1986
2213
|
scope,
|
|
1987
2214
|
path,
|
|
1988
2215
|
typeInfo,
|
|
1989
|
-
numeric:
|
|
1990
|
-
isSigned: isSigned(typeInfo.typeCode),
|
|
1991
|
-
min,
|
|
1992
|
-
max,
|
|
1993
|
-
hasEq: false,
|
|
1994
|
-
eq: 0n,
|
|
1995
|
-
hasLower: false,
|
|
1996
|
-
lower: 0n,
|
|
1997
|
-
lowerInclusive: false,
|
|
1998
|
-
hasUpper: false,
|
|
1999
|
-
upper: 0n,
|
|
2000
|
-
upperInclusive: false,
|
|
2001
|
-
holes: []
|
|
2002
|
-
},
|
|
2216
|
+
numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
|
|
2003
2217
|
bitmask: {
|
|
2004
2218
|
mustBeOne: 0n,
|
|
2005
2219
|
mustBeZero: 0n
|
|
2006
2220
|
},
|
|
2007
|
-
length:
|
|
2008
|
-
isSigned: false,
|
|
2009
|
-
min: 0n,
|
|
2010
|
-
max: LENGTH_MAX,
|
|
2011
|
-
hasEq: false,
|
|
2012
|
-
eq: 0n,
|
|
2013
|
-
hasLower: false,
|
|
2014
|
-
lower: 0n,
|
|
2015
|
-
lowerInclusive: false,
|
|
2016
|
-
hasUpper: false,
|
|
2017
|
-
upper: 0n,
|
|
2018
|
-
upperInclusive: false,
|
|
2019
|
-
holes: []
|
|
2020
|
-
},
|
|
2221
|
+
length: emptyBoundDomain(false, 0n, LENGTH_MAX),
|
|
2021
2222
|
set: {
|
|
2022
2223
|
hasIn: false,
|
|
2023
2224
|
inValues: [],
|
|
@@ -2026,33 +2227,33 @@ function initContext(scope, path, typeInfo) {
|
|
|
2026
2227
|
};
|
|
2027
2228
|
}
|
|
2028
2229
|
/** Narrow the numeric bound domain with an operator and emit issues on contradiction or redundancy. */
|
|
2029
|
-
function updateBound(domain, base, isNegated, value,
|
|
2230
|
+
function updateBound(domain, base, isNegated, value, isLength, groupIndex, constraintIndex, issues) {
|
|
2030
2231
|
let changedEq = false;
|
|
2031
2232
|
let changedLower = false;
|
|
2032
2233
|
let changedUpper = false;
|
|
2033
2234
|
if (isNegated) {
|
|
2034
2235
|
if (base === Op.EQ) {
|
|
2035
|
-
if (domain.hasEq && domain.eq === value) issues.push(
|
|
2236
|
+
if (domain.hasEq && domain.eq === value) issues.push(eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2036
2237
|
let alreadyHole = false;
|
|
2037
2238
|
for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) {
|
|
2038
2239
|
alreadyHole = true;
|
|
2039
2240
|
break;
|
|
2040
2241
|
}
|
|
2041
|
-
if (!alreadyHole
|
|
2042
|
-
} else updateBound(domain, negateBoundOp(base), false, value,
|
|
2242
|
+
if (!alreadyHole) domain.holes.push(value);
|
|
2243
|
+
} else updateBound(domain, negateBoundOp(base), false, value, isLength, groupIndex, constraintIndex, issues);
|
|
2043
2244
|
return;
|
|
2044
2245
|
}
|
|
2045
|
-
if (base === Op.GTE && value === domain.min) issues.push(
|
|
2046
|
-
else if (base === Op.LTE && value === domain.max) issues.push(
|
|
2047
|
-
if (
|
|
2048
|
-
else if (base === Op.GT && value === domain.max) issues.push(
|
|
2049
|
-
else if (base === Op.LT && value === domain.min) issues.push(
|
|
2246
|
+
if (base === Op.GTE && value === domain.min) issues.push(vacuousGte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2247
|
+
else if (base === Op.LTE && value === domain.max) issues.push(vacuousLte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2248
|
+
if (signedCompare(value, domain.min, domain.isSigned) < 0n || signedCompare(value, domain.max, domain.isSigned) > 0n) issues.push(outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2249
|
+
else if (base === Op.GT && value === domain.max) issues.push(impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2250
|
+
else if (base === Op.LT && value === domain.min) issues.push(impossibleLt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2050
2251
|
if (base === Op.EQ) {
|
|
2051
2252
|
if (!domain.hasEq || domain.eq !== value) changedEq = true;
|
|
2052
2253
|
if (domain.hasEq) {
|
|
2053
|
-
if (domain.eq !== value) issues.push(
|
|
2254
|
+
if (domain.eq !== value) issues.push(conflictingEquality(isLength, groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(value)));
|
|
2054
2255
|
}
|
|
2055
|
-
for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) issues.push(
|
|
2256
|
+
for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) issues.push(eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2056
2257
|
domain.hasEq = true;
|
|
2057
2258
|
domain.eq = value;
|
|
2058
2259
|
} else if (base === Op.GT || base === Op.GTE) {
|
|
@@ -2060,15 +2261,15 @@ function updateBound(domain, base, isNegated, value, emit, groupIndex, constrain
|
|
|
2060
2261
|
if (domain.hasLower) {
|
|
2061
2262
|
let redundant = false;
|
|
2062
2263
|
let strictlyBetter = false;
|
|
2063
|
-
if (
|
|
2264
|
+
if (signedCompare(value, domain.lower, domain.isSigned) < 0n) redundant = true;
|
|
2064
2265
|
else if (value === domain.lower) {
|
|
2065
2266
|
if (domain.lowerInclusive) if (!inclusive) strictlyBetter = true;
|
|
2066
2267
|
else redundant = true;
|
|
2067
2268
|
else if (!inclusive) redundant = true;
|
|
2068
2269
|
} else strictlyBetter = true;
|
|
2069
|
-
if (redundant) issues.push(
|
|
2270
|
+
if (redundant) issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2070
2271
|
if (strictlyBetter) {
|
|
2071
|
-
issues.push(
|
|
2272
|
+
issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.lower)));
|
|
2072
2273
|
domain.lower = value;
|
|
2073
2274
|
domain.lowerInclusive = inclusive;
|
|
2074
2275
|
changedLower = true;
|
|
@@ -2084,15 +2285,15 @@ function updateBound(domain, base, isNegated, value, emit, groupIndex, constrain
|
|
|
2084
2285
|
if (domain.hasUpper) {
|
|
2085
2286
|
let redundant = false;
|
|
2086
2287
|
let strictlyBetter = false;
|
|
2087
|
-
if (
|
|
2288
|
+
if (signedCompare(value, domain.upper, domain.isSigned) > 0n) redundant = true;
|
|
2088
2289
|
else if (value === domain.upper) {
|
|
2089
2290
|
if (domain.upperInclusive) if (!inclusive) strictlyBetter = true;
|
|
2090
2291
|
else redundant = true;
|
|
2091
2292
|
else if (!inclusive) redundant = true;
|
|
2092
2293
|
} else strictlyBetter = true;
|
|
2093
|
-
if (redundant) issues.push(
|
|
2294
|
+
if (redundant) issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2094
2295
|
if (strictlyBetter) {
|
|
2095
|
-
issues.push(
|
|
2296
|
+
issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.upper)));
|
|
2096
2297
|
domain.upper = value;
|
|
2097
2298
|
domain.upperInclusive = inclusive;
|
|
2098
2299
|
changedUpper = true;
|
|
@@ -2105,16 +2306,16 @@ function updateBound(domain, base, isNegated, value, emit, groupIndex, constrain
|
|
|
2105
2306
|
}
|
|
2106
2307
|
}
|
|
2107
2308
|
if (changedEq || changedLower) {
|
|
2108
|
-
if (domain.hasEq && domain.hasLower) if (domain.lowerInclusive ?
|
|
2109
|
-
else issues.push(
|
|
2309
|
+
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)));
|
|
2310
|
+
else issues.push(redundantBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.lower), bigintToHex(domain.eq)));
|
|
2110
2311
|
}
|
|
2111
2312
|
if (changedEq || changedUpper) {
|
|
2112
|
-
if (domain.hasEq && domain.hasUpper) if (domain.upperInclusive ?
|
|
2113
|
-
else issues.push(
|
|
2313
|
+
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)));
|
|
2314
|
+
else issues.push(redundantBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.upper), bigintToHex(domain.eq)));
|
|
2114
2315
|
}
|
|
2115
2316
|
if (changedLower || changedUpper) {
|
|
2116
2317
|
if (domain.hasLower && domain.hasUpper) {
|
|
2117
|
-
if (
|
|
2318
|
+
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)));
|
|
2118
2319
|
}
|
|
2119
2320
|
}
|
|
2120
2321
|
}
|
|
@@ -2170,10 +2371,11 @@ function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues)
|
|
|
2170
2371
|
}
|
|
2171
2372
|
if (inSet) issues.push(setReduction(groupIndex, constraintIndex, bigintToHex(value)));
|
|
2172
2373
|
}
|
|
2173
|
-
|
|
2374
|
+
ctx.set.notInValues.push(value);
|
|
2174
2375
|
}
|
|
2175
2376
|
checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
2176
2377
|
} else {
|
|
2378
|
+
for (const value of values) if (signedCompare(value, ctx.numeric.min, ctx.numeric.isSigned) < 0n || signedCompare(value, ctx.numeric.max, ctx.numeric.isSigned) > 0n) issues.push(outOfPhysicalBounds(false, groupIndex, constraintIndex, bigintToHex(value)));
|
|
2177
2379
|
if (ctx.numeric.hasEq) {
|
|
2178
2380
|
let found = false;
|
|
2179
2381
|
for (const candidate of values) if (candidate === ctx.numeric.eq) {
|
|
@@ -2210,6 +2412,40 @@ function checkDuplicates(issues, operators, groupIndex, constraintIndex) {
|
|
|
2210
2412
|
seen.add(key);
|
|
2211
2413
|
}
|
|
2212
2414
|
}
|
|
2415
|
+
/**
|
|
2416
|
+
* Report a lone unnegated gte/lte pair (value or length domain) as fusible into the
|
|
2417
|
+
* corresponding single range operator. Fires only for a satisfiable pair on a compatible
|
|
2418
|
+
* type; contradictions and dominated bounds are already reported by the domain checks.
|
|
2419
|
+
*/
|
|
2420
|
+
function checkFusibleRange(ctx, operators, groupIndex, constraintIndex, issues) {
|
|
2421
|
+
const numericPair = findLonePair(operators, Op.GTE, Op.LTE);
|
|
2422
|
+
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)));
|
|
2423
|
+
const lengthPair = findLonePair(operators, Op.LENGTH_GTE, Op.LENGTH_LTE);
|
|
2424
|
+
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)));
|
|
2425
|
+
}
|
|
2426
|
+
/** Return the operand pair when the operators contain exactly one of each unnegated bound opcode. */
|
|
2427
|
+
function findLonePair(operators, lowerOp, upperOp) {
|
|
2428
|
+
let lowerCount = 0;
|
|
2429
|
+
let upperCount = 0;
|
|
2430
|
+
let low = 0n;
|
|
2431
|
+
let high = 0n;
|
|
2432
|
+
for (const opHex of operators) {
|
|
2433
|
+
const opCode = parseInt(opHex.slice(2, 4), 16);
|
|
2434
|
+
const dataLength = (opHex.length - 4) / 2;
|
|
2435
|
+
if (dataLength > 65535 || !isValidOperatorData(opCode & ~Op.NOT, dataLength)) continue;
|
|
2436
|
+
if (opCode === lowerOp) {
|
|
2437
|
+
lowerCount++;
|
|
2438
|
+
low = readValue(opHex);
|
|
2439
|
+
} else if (opCode === upperOp) {
|
|
2440
|
+
upperCount++;
|
|
2441
|
+
high = readValue(opHex);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
return lowerCount === 1 && upperCount === 1 ? {
|
|
2445
|
+
low,
|
|
2446
|
+
high
|
|
2447
|
+
} : null;
|
|
2448
|
+
}
|
|
2213
2449
|
/** Validate all operators on a single constraint against its domain context. */
|
|
2214
2450
|
function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues) {
|
|
2215
2451
|
const operators = constraint.operators;
|
|
@@ -2218,21 +2454,36 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
|
|
|
2218
2454
|
const opCode = parseInt(opHex.slice(2, 4), 16);
|
|
2219
2455
|
const base = opCode & ~Op.NOT;
|
|
2220
2456
|
const isNegated = (opCode & Op.NOT) !== 0;
|
|
2457
|
+
const dataLength = (opHex.length - 4) / 2;
|
|
2458
|
+
if (dataLength > 65535 || !isValidOperatorData(base, dataLength)) {
|
|
2459
|
+
issues.push(fromOpRule("UNKNOWN_OPERATOR", "Unknown operator code", groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
2460
|
+
continue;
|
|
2461
|
+
}
|
|
2221
2462
|
if (underAny && isNegated) issues.push(negationUnderAny(groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
2222
|
-
const
|
|
2223
|
-
if (
|
|
2224
|
-
issues.push(fromOpRule(
|
|
2463
|
+
const incompat = getIncompat(base, ctx.typeInfo);
|
|
2464
|
+
if (incompat) {
|
|
2465
|
+
issues.push(fromOpRule(incompat.code, incompat.message, groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
|
|
2225
2466
|
continue;
|
|
2226
2467
|
}
|
|
2227
2468
|
if (isValueOp(base)) {
|
|
2469
|
+
const typeCode = ctx.typeInfo.typeCode;
|
|
2470
|
+
if (isLeftAligned(typeCode)) {
|
|
2471
|
+
const nonCanonical = findNonCanonicalWord(opHex, typeCode);
|
|
2472
|
+
if (nonCanonical !== null) {
|
|
2473
|
+
issues.push(nonCanonicalOperand(groupIndex, constraintIndex, bigintToHex(nonCanonical.word), bigintToHex(nonCanonical.canonical)));
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2228
2477
|
if (base >= Op.EQ && base <= Op.BETWEEN) if (base === Op.BETWEEN) {
|
|
2229
2478
|
const { low, high } = readPair(opHex);
|
|
2230
|
-
|
|
2231
|
-
|
|
2479
|
+
if (!isNegated) {
|
|
2480
|
+
updateBound(ctx.numeric, Op.GTE, false, low, false, groupIndex, constraintIndex, issues);
|
|
2481
|
+
updateBound(ctx.numeric, Op.LTE, false, high, false, groupIndex, constraintIndex, issues);
|
|
2482
|
+
} else if (signedCompare(low, high, ctx.numeric.isSigned) > 0n) issues.push(vacuousNegatedRange(false, groupIndex, constraintIndex, bigintToHex(low), bigintToHex(high)));
|
|
2232
2483
|
} else {
|
|
2233
2484
|
const value = readValue(opHex);
|
|
2234
2485
|
const holesBefore = ctx.numeric.holes.length;
|
|
2235
|
-
updateBound(ctx.numeric, base, isNegated, value,
|
|
2486
|
+
updateBound(ctx.numeric, base, isNegated, value, false, groupIndex, constraintIndex, issues);
|
|
2236
2487
|
if (ctx.numeric.holes.length > holesBefore) checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
|
|
2237
2488
|
}
|
|
2238
2489
|
else if (isBitmaskOp(base)) updateBitmask(ctx, base, isNegated, readValue(opHex), groupIndex, constraintIndex, issues);
|
|
@@ -2243,11 +2494,13 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
|
|
|
2243
2494
|
}
|
|
2244
2495
|
} else if (isLengthOp(base)) if (base === Op.LENGTH_BETWEEN) {
|
|
2245
2496
|
const { low, high } = readPair(opHex);
|
|
2246
|
-
|
|
2247
|
-
|
|
2497
|
+
if (!isNegated) {
|
|
2498
|
+
updateBound(ctx.length, Op.GTE, false, low, true, groupIndex, constraintIndex, issues);
|
|
2499
|
+
updateBound(ctx.length, Op.LTE, false, high, true, groupIndex, constraintIndex, issues);
|
|
2500
|
+
} else if (low > high) issues.push(vacuousNegatedRange(true, groupIndex, constraintIndex, bigintToHex(low), bigintToHex(high)));
|
|
2248
2501
|
} else {
|
|
2249
2502
|
const value = readValue(opHex);
|
|
2250
|
-
updateBound(ctx.length, normalizeLengthOp(base), isNegated, value,
|
|
2503
|
+
updateBound(ctx.length, normalizeLengthOp(base), isNegated, value, true, groupIndex, constraintIndex, issues);
|
|
2251
2504
|
}
|
|
2252
2505
|
}
|
|
2253
2506
|
checkDuplicates(issues, operators, groupIndex, constraintIndex);
|
|
@@ -2269,7 +2522,16 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2269
2522
|
if (constraint.scope === Scope.CALLDATA) {
|
|
2270
2523
|
const steps = parsePathSteps(constraint.path);
|
|
2271
2524
|
if (steps.length > Limits.MAX_PATH_DEPTH) issues.push(pathDepthExceeded(groupIndex, constraintIndex, bigintToHex(BigInt(steps.length)), bigintToHex(BigInt(Limits.MAX_PATH_DEPTH))));
|
|
2272
|
-
|
|
2525
|
+
let walk;
|
|
2526
|
+
try {
|
|
2527
|
+
walk = Descriptor.walkPath(descBytes, steps);
|
|
2528
|
+
} catch (error) {
|
|
2529
|
+
if (error instanceof CallciumError && error.code === "INVALID_PATH") {
|
|
2530
|
+
issues.push(unnavigablePath(groupIndex, constraintIndex));
|
|
2531
|
+
continue;
|
|
2532
|
+
}
|
|
2533
|
+
throw error;
|
|
2534
|
+
}
|
|
2273
2535
|
if (walk.quantifiedStaticLength > Limits.MAX_QUANTIFIED_ARRAY_LENGTH) issues.push(quantifierOverStaticLimit(groupIndex, constraintIndex, bigintToHex(BigInt(walk.quantifiedStaticLength)), bigintToHex(BigInt(Limits.MAX_QUANTIFIED_ARRAY_LENGTH))));
|
|
2274
2536
|
typeInfo = walk.typeInfo;
|
|
2275
2537
|
} else {
|
|
@@ -2291,16 +2553,20 @@ function validateGroup(data, descBytes, groupIndex, issues) {
|
|
|
2291
2553
|
contexts.push(ctx);
|
|
2292
2554
|
}
|
|
2293
2555
|
validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues);
|
|
2556
|
+
checkFusibleRange(ctx, constraint.operators, groupIndex, constraintIndex, issues);
|
|
2294
2557
|
}
|
|
2295
2558
|
}
|
|
2296
2559
|
/**
|
|
2297
2560
|
* Validate a policy for type mismatches, contradictions, redundancies, and vacuities.
|
|
2298
2561
|
* @param data - The canonical policy data to validate.
|
|
2299
2562
|
* @returns All validation issues found, ordered by group and constraint index.
|
|
2563
|
+
* @throws {CallciumError} If the descriptor is not well-formed; semantic validation is
|
|
2564
|
+
* defined only over a well-formed descriptor.
|
|
2300
2565
|
*/
|
|
2301
2566
|
function validate(data) {
|
|
2302
2567
|
const issues = [];
|
|
2303
2568
|
const descBytes = hexToBytes(data.descriptor);
|
|
2569
|
+
decodeDescriptor(descBytes);
|
|
2304
2570
|
for (let groupIndex = 0; groupIndex < data.groups.length; groupIndex++) {
|
|
2305
2571
|
if (data.groups[groupIndex].length === 0) {
|
|
2306
2572
|
issues.push(emptyGroup(groupIndex));
|
|
@@ -2508,6 +2774,12 @@ var PolicyBuilder = class PolicyBuilder {
|
|
|
2508
2774
|
};
|
|
2509
2775
|
//#endregion
|
|
2510
2776
|
//#region src/constraint.ts
|
|
2777
|
+
/** Strip an optional 0x prefix and validate a 40-hex-char (20-byte) address body. */
|
|
2778
|
+
function addressBody(value) {
|
|
2779
|
+
const body = value.startsWith("0x") ? value.slice(2) : value;
|
|
2780
|
+
if (body.length !== 40) throw new CallciumError("INVALID_CONSTRAINT", `Invalid address length: expected 40 hex chars, got ${body.length}`);
|
|
2781
|
+
return body;
|
|
2782
|
+
}
|
|
2511
2783
|
/** Convert a scalar value to a 32-byte big-endian word. */
|
|
2512
2784
|
function encodeWord(value) {
|
|
2513
2785
|
const word = new Uint8Array(32);
|
|
@@ -2516,8 +2788,7 @@ function encodeWord(value) {
|
|
|
2516
2788
|
return word;
|
|
2517
2789
|
}
|
|
2518
2790
|
if (typeof value === "string") {
|
|
2519
|
-
const body =
|
|
2520
|
-
if (body.length !== 40) throw new CallciumError("INVALID_CONSTRAINT", `Invalid address length: expected 40 hex chars, got ${body.length}`);
|
|
2791
|
+
const body = addressBody(value);
|
|
2521
2792
|
for (let i = 0; i < 20; i++) {
|
|
2522
2793
|
const byte = parseInt(body.substring(i * 2, i * 2 + 2), 16);
|
|
2523
2794
|
if (Number.isNaN(byte)) throw new CallciumError("INVALID_CONSTRAINT", `Invalid hex character in address at position ${i * 2}`);
|
|
@@ -2554,8 +2825,7 @@ function setOp(opCode, values) {
|
|
|
2554
2825
|
if (typeof v === "bigint") return v;
|
|
2555
2826
|
if (typeof v === "number") return BigInt(v);
|
|
2556
2827
|
if (typeof v === "boolean") return v ? 1n : 0n;
|
|
2557
|
-
|
|
2558
|
-
return BigInt("0x" + (body || "0"));
|
|
2828
|
+
return BigInt("0x" + addressBody(v));
|
|
2559
2829
|
});
|
|
2560
2830
|
bigs.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
2561
2831
|
const deduped = [];
|
|
@@ -2754,7 +3024,7 @@ function descendTuple(node, callData, head, base, childIndex) {
|
|
|
2754
3024
|
if (node.isDynamic) {
|
|
2755
3025
|
const ptrResult = readPointer(callData, head);
|
|
2756
3026
|
if (typeof ptrResult !== "number") return {
|
|
2757
|
-
|
|
3027
|
+
ok: false,
|
|
2758
3028
|
code: ptrResult.code
|
|
2759
3029
|
};
|
|
2760
3030
|
tupleBase = base + ptrResult;
|
|
@@ -2765,42 +3035,48 @@ function descendTuple(node, callData, head, base, childIndex) {
|
|
|
2765
3035
|
}
|
|
2766
3036
|
for (let i = 0; i < childIndex; i++) cursor += headContribution(node.fields[i]);
|
|
2767
3037
|
return {
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
3038
|
+
ok: true,
|
|
3039
|
+
location: {
|
|
3040
|
+
head: cursor,
|
|
3041
|
+
base: tupleBase,
|
|
3042
|
+
node: node.fields[childIndex]
|
|
3043
|
+
}
|
|
2772
3044
|
};
|
|
2773
3045
|
}
|
|
2774
3046
|
/** Descend into a dynamic array element by index. */
|
|
2775
3047
|
function descendDynamicArray(node, callData, head, base, childIndex) {
|
|
2776
3048
|
const arrayBaseResult = readPointer(callData, head);
|
|
2777
3049
|
if (typeof arrayBaseResult !== "number") return {
|
|
2778
|
-
|
|
3050
|
+
ok: false,
|
|
2779
3051
|
code: arrayBaseResult.code
|
|
2780
3052
|
};
|
|
2781
3053
|
const arrayBase = base + arrayBaseResult;
|
|
2782
3054
|
const lengthResult = readPointer(callData, arrayBase);
|
|
2783
3055
|
if (typeof lengthResult !== "number") return {
|
|
2784
|
-
|
|
3056
|
+
ok: false,
|
|
2785
3057
|
code: lengthResult.code
|
|
2786
3058
|
};
|
|
2787
3059
|
if (childIndex >= lengthResult) return {
|
|
2788
|
-
|
|
3060
|
+
ok: false,
|
|
2789
3061
|
code: "ARRAY_INDEX_OUT_OF_BOUNDS"
|
|
2790
3062
|
};
|
|
2791
3063
|
const headsBase = arrayBase + 32;
|
|
2792
3064
|
const elem = node.element;
|
|
2793
3065
|
if (elem.isDynamic) return {
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3066
|
+
ok: true,
|
|
3067
|
+
location: {
|
|
3068
|
+
head: headsBase + childIndex * 32,
|
|
3069
|
+
base: headsBase,
|
|
3070
|
+
node: elem
|
|
3071
|
+
}
|
|
2798
3072
|
};
|
|
2799
3073
|
return {
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
3074
|
+
ok: true,
|
|
3075
|
+
location: {
|
|
3076
|
+
head: headsBase + childIndex * elem.staticSize,
|
|
3077
|
+
base: arrayBase,
|
|
3078
|
+
node: elem
|
|
3079
|
+
}
|
|
2804
3080
|
};
|
|
2805
3081
|
}
|
|
2806
3082
|
/** Descend into a static array element by index. */
|
|
@@ -2810,22 +3086,26 @@ function descendStaticArray(node, callData, head, base, childIndex) {
|
|
|
2810
3086
|
if (elem.isDynamic) {
|
|
2811
3087
|
const ptrResult = readPointer(callData, head);
|
|
2812
3088
|
if (typeof ptrResult !== "number") return {
|
|
2813
|
-
|
|
3089
|
+
ok: false,
|
|
2814
3090
|
code: ptrResult.code
|
|
2815
3091
|
};
|
|
2816
3092
|
const arrayBase = base + ptrResult;
|
|
2817
3093
|
return {
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
3094
|
+
ok: true,
|
|
3095
|
+
location: {
|
|
3096
|
+
head: arrayBase + childIndex * 32,
|
|
3097
|
+
base: arrayBase,
|
|
3098
|
+
node: elem
|
|
3099
|
+
}
|
|
2822
3100
|
};
|
|
2823
3101
|
}
|
|
2824
3102
|
return {
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
3103
|
+
ok: true,
|
|
3104
|
+
location: {
|
|
3105
|
+
head: head + childIndex * elem.staticSize,
|
|
3106
|
+
base,
|
|
3107
|
+
node: elem
|
|
3108
|
+
}
|
|
2829
3109
|
};
|
|
2830
3110
|
}
|
|
2831
3111
|
/**
|
|
@@ -2865,13 +3145,13 @@ function locate(tree, callData, pathBytes, baseOffset) {
|
|
|
2865
3145
|
};
|
|
2866
3146
|
}
|
|
2867
3147
|
const result = descend(node, callData, head, base, step);
|
|
2868
|
-
if (result.
|
|
3148
|
+
if (!result.ok) return {
|
|
2869
3149
|
ok: false,
|
|
2870
3150
|
code: result.code
|
|
2871
3151
|
};
|
|
2872
|
-
head = result.head;
|
|
2873
|
-
base = result.base;
|
|
2874
|
-
node = result.node;
|
|
3152
|
+
head = result.location.head;
|
|
3153
|
+
base = result.location.base;
|
|
3154
|
+
node = result.location.node;
|
|
2875
3155
|
}
|
|
2876
3156
|
return {
|
|
2877
3157
|
ok: true,
|
|
@@ -2883,6 +3163,29 @@ function locate(tree, callData, pathBytes, baseOffset) {
|
|
|
2883
3163
|
}
|
|
2884
3164
|
};
|
|
2885
3165
|
}
|
|
3166
|
+
/**
|
|
3167
|
+
* Build the element-shape fields shared by dynamic- and static-array nodes, branching only on
|
|
3168
|
+
* whether the element itself is dynamic.
|
|
3169
|
+
*/
|
|
3170
|
+
function makeArrayShape(length, elem, dynamicHeadsBase, staticDataOffset, staticCompositeBase) {
|
|
3171
|
+
return elem.isDynamic ? {
|
|
3172
|
+
length,
|
|
3173
|
+
elementNode: elem,
|
|
3174
|
+
elementIsDynamic: true,
|
|
3175
|
+
elementStaticSize: 0,
|
|
3176
|
+
headsBase: dynamicHeadsBase,
|
|
3177
|
+
dataOffset: 0,
|
|
3178
|
+
compositeBase: dynamicHeadsBase
|
|
3179
|
+
} : {
|
|
3180
|
+
length,
|
|
3181
|
+
elementNode: elem,
|
|
3182
|
+
elementIsDynamic: false,
|
|
3183
|
+
elementStaticSize: elem.staticSize,
|
|
3184
|
+
headsBase: 0,
|
|
3185
|
+
dataOffset: staticDataOffset,
|
|
3186
|
+
compositeBase: staticCompositeBase
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
2886
3189
|
/** Extract array shape from a location pointing to an array node. */
|
|
2887
3190
|
function arrayShape(callData, location) {
|
|
2888
3191
|
const { head, base, node } = location;
|
|
@@ -2898,66 +3201,26 @@ function arrayShape(callData, location) {
|
|
|
2898
3201
|
ok: false,
|
|
2899
3202
|
code: lengthResult.code
|
|
2900
3203
|
};
|
|
2901
|
-
const elem = node.element;
|
|
2902
3204
|
const headsBase = arrayBase + 32;
|
|
2903
|
-
if (elem.isDynamic) return {
|
|
2904
|
-
ok: true,
|
|
2905
|
-
shape: {
|
|
2906
|
-
length: lengthResult,
|
|
2907
|
-
elementNode: elem,
|
|
2908
|
-
elementIsDynamic: true,
|
|
2909
|
-
elementStaticSize: 0,
|
|
2910
|
-
headsBase,
|
|
2911
|
-
dataOffset: 0,
|
|
2912
|
-
compositeBase: headsBase
|
|
2913
|
-
}
|
|
2914
|
-
};
|
|
2915
3205
|
return {
|
|
2916
3206
|
ok: true,
|
|
2917
|
-
shape:
|
|
2918
|
-
length: lengthResult,
|
|
2919
|
-
elementNode: elem,
|
|
2920
|
-
elementIsDynamic: false,
|
|
2921
|
-
elementStaticSize: elem.staticSize,
|
|
2922
|
-
headsBase: 0,
|
|
2923
|
-
dataOffset: headsBase,
|
|
2924
|
-
compositeBase: arrayBase
|
|
2925
|
-
}
|
|
3207
|
+
shape: makeArrayShape(lengthResult, node.element, headsBase, headsBase, arrayBase)
|
|
2926
3208
|
};
|
|
2927
3209
|
}
|
|
2928
3210
|
if (node.type === "staticArray") {
|
|
2929
3211
|
const elem = node.element;
|
|
3212
|
+
let dynamicHeadsBase = 0;
|
|
2930
3213
|
if (elem.isDynamic) {
|
|
2931
3214
|
const arrayBaseResult = readPointer(callData, head);
|
|
2932
3215
|
if (typeof arrayBaseResult !== "number") return {
|
|
2933
3216
|
ok: false,
|
|
2934
3217
|
code: arrayBaseResult.code
|
|
2935
3218
|
};
|
|
2936
|
-
|
|
2937
|
-
return {
|
|
2938
|
-
ok: true,
|
|
2939
|
-
shape: {
|
|
2940
|
-
length: node.length,
|
|
2941
|
-
elementNode: elem,
|
|
2942
|
-
elementIsDynamic: true,
|
|
2943
|
-
elementStaticSize: 0,
|
|
2944
|
-
headsBase: arrayBase,
|
|
2945
|
-
dataOffset: 0,
|
|
2946
|
-
compositeBase: arrayBase
|
|
2947
|
-
}
|
|
2948
|
-
};
|
|
3219
|
+
dynamicHeadsBase = base + arrayBaseResult;
|
|
2949
3220
|
}
|
|
2950
3221
|
return {
|
|
2951
3222
|
ok: true,
|
|
2952
|
-
shape:
|
|
2953
|
-
length: node.length,
|
|
2954
|
-
elementNode: elem,
|
|
2955
|
-
elementIsDynamic: false,
|
|
2956
|
-
elementStaticSize: elem.staticSize,
|
|
2957
|
-
headsBase: 0,
|
|
2958
|
-
dataOffset: head,
|
|
2959
|
-
compositeBase: base
|
|
2960
|
-
}
|
|
3223
|
+
shape: makeArrayShape(node.length, elem, dynamicHeadsBase, head, base)
|
|
2961
3224
|
};
|
|
2962
3225
|
}
|
|
2963
3226
|
throw new CallciumError("INVALID_PATH", `Cannot compute array shape for non-array node (${node.type}).`);
|
|
@@ -3035,13 +3298,13 @@ function descendPath(callData, location, pathBytes) {
|
|
|
3035
3298
|
for (let stepIndex = 0; stepIndex < stepCount; stepIndex++) {
|
|
3036
3299
|
const step = readStep(pathBytes, stepIndex);
|
|
3037
3300
|
const result = descend(node, callData, head, base, step);
|
|
3038
|
-
if (result.
|
|
3301
|
+
if (!result.ok) return {
|
|
3039
3302
|
ok: false,
|
|
3040
3303
|
code: result.code
|
|
3041
3304
|
};
|
|
3042
|
-
head = result.head;
|
|
3043
|
-
base = result.base;
|
|
3044
|
-
node = result.node;
|
|
3305
|
+
head = result.location.head;
|
|
3306
|
+
base = result.location.base;
|
|
3307
|
+
node = result.location.node;
|
|
3045
3308
|
}
|
|
3046
3309
|
return {
|
|
3047
3310
|
ok: true,
|
|
@@ -3054,10 +3317,11 @@ function descendPath(callData, location, pathBytes) {
|
|
|
3054
3317
|
}
|
|
3055
3318
|
//#endregion
|
|
3056
3319
|
//#region src/policy-enforcer.ts
|
|
3057
|
-
/** Violations that abort evaluation instead of failing only their group (spec §
|
|
3320
|
+
/** Violations that abort evaluation instead of failing only their group (spec §9.3). */
|
|
3058
3321
|
const ABORT_VIOLATION_CODES = new Set([
|
|
3059
3322
|
"CALLDATA_OUT_OF_BOUNDS",
|
|
3060
3323
|
"ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
3324
|
+
"NON_CANONICAL_VALUE",
|
|
3061
3325
|
"QUANTIFIER_LIMIT_EXCEEDED"
|
|
3062
3326
|
]);
|
|
3063
3327
|
/** Convert a hex address to a 256-bit bigint (zero-padded to 32 bytes). */
|
|
@@ -3227,9 +3491,14 @@ function applyLeafOperator(callDataBytes, location, opCode, operandData) {
|
|
|
3227
3491
|
resolvedValue: bigintToHex(BigInt(valueLength))
|
|
3228
3492
|
};
|
|
3229
3493
|
}
|
|
3494
|
+
if (node.isDynamic || node.type !== "elementary") throw new CallciumError("NOT_SCALAR", "Value operator on a target without a scalar word.");
|
|
3230
3495
|
const result = loadScalar(callDataBytes, location);
|
|
3231
3496
|
if (!result.ok) return { error: result.code };
|
|
3232
|
-
const value =
|
|
3497
|
+
const value = toBigInt(result.value, 0);
|
|
3498
|
+
if (canonicalize(value, node.typeCode) !== value) return {
|
|
3499
|
+
error: "NON_CANONICAL_VALUE",
|
|
3500
|
+
resolvedValue: bigintToHex(value)
|
|
3501
|
+
};
|
|
3233
3502
|
return {
|
|
3234
3503
|
passed: applyOperator(opCode, value, 32, operandData, node.typeCode),
|
|
3235
3504
|
resolvedValue: bigintToHex(value)
|
|
@@ -3239,16 +3508,29 @@ function applyLeafOperator(callDataBytes, location, opCode, operandData) {
|
|
|
3239
3508
|
function evaluateLeaf(location, callDataBytes, opCode, operandData, groupIndex, ruleIndex, pathHex) {
|
|
3240
3509
|
const result = applyLeafOperator(callDataBytes, location, opCode, operandData);
|
|
3241
3510
|
const typeCode = location.node.typeCode;
|
|
3242
|
-
if ("error" in result)
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3511
|
+
if ("error" in result) {
|
|
3512
|
+
if (result.error === "NON_CANONICAL_VALUE") return {
|
|
3513
|
+
group: groupIndex,
|
|
3514
|
+
rule: ruleIndex,
|
|
3515
|
+
code: result.error,
|
|
3516
|
+
scope: Scope.CALLDATA,
|
|
3517
|
+
path: pathHex,
|
|
3518
|
+
opCode,
|
|
3519
|
+
operandData: bytesToHex(operandData),
|
|
3520
|
+
typeCode,
|
|
3521
|
+
resolvedValue: result.resolvedValue
|
|
3522
|
+
};
|
|
3523
|
+
return {
|
|
3524
|
+
group: groupIndex,
|
|
3525
|
+
rule: ruleIndex,
|
|
3526
|
+
code: result.error,
|
|
3527
|
+
scope: Scope.CALLDATA,
|
|
3528
|
+
path: pathHex,
|
|
3529
|
+
opCode,
|
|
3530
|
+
operandData: bytesToHex(operandData),
|
|
3531
|
+
typeCode
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3252
3534
|
if (!result.passed) return {
|
|
3253
3535
|
group: groupIndex,
|
|
3254
3536
|
rule: ruleIndex,
|
|
@@ -3338,6 +3620,18 @@ function evaluateQuantifier(qResult, callDataBytes, opCode, operandData, groupIn
|
|
|
3338
3620
|
} else leafLocation = elemLocation;
|
|
3339
3621
|
const applied = applyLeafOperator(callDataBytes, leafLocation, opCode, operandData);
|
|
3340
3622
|
if ("error" in applied) {
|
|
3623
|
+
if (applied.error === "NON_CANONICAL_VALUE") return {
|
|
3624
|
+
group: groupIndex,
|
|
3625
|
+
rule: ruleIndex,
|
|
3626
|
+
code: applied.error,
|
|
3627
|
+
scope: Scope.CALLDATA,
|
|
3628
|
+
path: pathHex,
|
|
3629
|
+
opCode,
|
|
3630
|
+
operandData: operandHex,
|
|
3631
|
+
typeCode: leafTypeCode,
|
|
3632
|
+
elementIndex: elemIndex,
|
|
3633
|
+
resolvedValue: applied.resolvedValue
|
|
3634
|
+
};
|
|
3341
3635
|
if (isUniversal) return {
|
|
3342
3636
|
group: groupIndex,
|
|
3343
3637
|
rule: ruleIndex,
|