@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.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: 64
197
+ typeCode: TypeCode.ADDRESS
181
198
  },
182
199
  {
183
200
  key: "MSG_VALUE",
184
201
  code: 1,
185
202
  label: "msg.value",
186
- typeCode: 31
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: 31
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: 31
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: 31
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: 64
227
+ typeCode: TypeCode.ADDRESS
211
228
  },
212
229
  {
213
230
  key: "BASE_FEE",
214
231
  code: 6,
215
232
  label: "block.basefee",
216
- typeCode: 31
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: 31
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 && isQuantifier(step)) quantifiedStaticLength = staticArrayLength(desc, cursor);
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
- const base = opCode & ~Op.NOT;
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
- result = (value & toBigInt(operandData, 0)) !== 0n;
1566
- break;
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
- case Op.LENGTH_BETWEEN: {
1586
- const length = BigInt(valueLength);
1587
- const min = toBigInt(operandData, 0);
1588
- const max = toBigInt(operandData, 32);
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
- /** Construct a fully populated Issue. */
1600
- function makeIssue(severity, category, groupIndex, constraintIndex, code, value1, value2, message) {
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
- /** Create an issue from operator compatibility results. */
1613
- function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
1614
- return makeIssue("error", "typeMismatch", groupIndex, constraintIndex, code, opCode, ZERO, message);
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
- /** eq(v) and neq(v) on same path. */
1617
- function eqNeqContradiction(groupIndex, constraintIndex, value) {
1618
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "EQ_NEQ_CONTRADICTION", value, ZERO, "eq(v) and neq(v) on same path");
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
- /** Multiple eq() operators with different values. */
1621
- function conflictingEquality(groupIndex, constraintIndex, existing, newValue) {
1622
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "CONFLICTING_EQUALITY", existing, newValue, "Multiple eq() operators with different values");
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 the type. */
1625
- function outOfPhysicalBounds(groupIndex, constraintIndex, value) {
1626
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "OUT_OF_PHYSICAL_BOUNDS", value, ZERO, "Value is outside the physical range of the type");
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 type maximum is impossible. */
1629
- function impossibleGt(groupIndex, constraintIndex, value) {
1630
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_GT", value, ZERO, "gt() on type maximum is impossible");
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 type minimum is impossible. */
1633
- function impossibleLt(groupIndex, constraintIndex, value) {
1634
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LT", value, ZERO, "lt() on type minimum is impossible");
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 makeIssue("error", "contradiction", groupIndex, constraintIndex, "BOUNDS_EXCLUDE_EQUALITY", eq, bound, "eq() value is excluded by bound");
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 makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_RANGE", lower, upper, "Lower bound exceeds upper bound");
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 makeIssue("error", "contradiction", groupIndex, constraintIndex, "SET_EXCLUDES_EQUALITY", value, ZERO, "Set operation excludes existing eq() value");
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 makeIssue("error", "contradiction", groupIndex, constraintIndex, "EMPTY_SET_INTERSECTION", ZERO, ZERO, "Multiple isIn() sets have no intersection");
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 makeIssue("error", "contradiction", groupIndex, constraintIndex, "SET_FULLY_EXCLUDED", ZERO, ZERO, "All values in isIn() set are excluded by neq/notIn");
1655
- }
1656
- /** lengthEq(v) and lengthNeq(v) on same path. */
1657
- function lengthEqNeqContradiction(groupIndex, constraintIndex, value) {
1658
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "LENGTH_EQ_NEQ_CONTRADICTION", value, ZERO, "lengthEq(v) and lengthNeq(v) on same path");
1659
- }
1660
- /** Multiple lengthEq() operators with different values. */
1661
- function conflictingLength(groupIndex, constraintIndex, existing, newValue) {
1662
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "CONFLICTING_LENGTH", existing, newValue, "Multiple lengthEq() operators with different values");
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 makeIssue("error", "contradiction", groupIndex, constraintIndex, "BITMASK_CONTRADICTION", mask, conflicting, "Bitmask operators conflict");
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 makeIssue("error", "contradiction", groupIndex, constraintIndex, "BITMASK_ANY_IMPOSSIBLE", mask, mustBeZero, "bitmaskAny is impossible because all bits are forbidden");
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 makeIssue("error", "contradiction", groupIndex, constraintIndex, "UNSORTED_IN_SET", ZERO, ZERO, "isIn/notIn set is not strictly sorted and deduplicated.");
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 makeIssue("error", "vacuity", groupIndex, 0, "EMPTY_GROUP", ZERO, ZERO, "Group contains zero constraints.");
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 is dominated by a stricter bound. */
1701
- function dominatedBound(groupIndex, constraintIndex, value) {
1702
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "DOMINATED_BOUND", value, ZERO, "Numeric bound is dominated by a stricter bound");
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 is redundant because eq() is set. */
1705
- function redundantBound(groupIndex, constraintIndex, bound, eq) {
1706
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "REDUNDANT_BOUND", bound, eq, "Bound is redundant because eq() is set");
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 makeIssue("warning", "redundancy", groupIndex, constraintIndex, "SET_REDUCTION", value, ZERO, "notIn() value was present in isIn() set");
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 makeIssue("warning", "redundancy", groupIndex, constraintIndex, "SET_REDUNDANCY", intersectionCount, ZERO, "isIn() sets partially overlap");
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 makeIssue("warning", "redundancy", groupIndex, constraintIndex, "SET_PARTIALLY_EXCLUDED", excludedCount, ZERO, "Some values in isIn() set are excluded by neq/notIn");
1719
- }
1720
- /** Length bound is dominated by a stricter bound. */
1721
- function dominatedLengthBound(groupIndex, constraintIndex, value) {
1722
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "DOMINATED_LENGTH_BOUND", value, ZERO, "Length bound is dominated by a stricter bound");
1723
- }
1724
- /** Length bound is redundant because lengthEq() is set. */
1725
- function redundantLengthBound(groupIndex, constraintIndex, bound, eq) {
1726
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "REDUNDANT_LENGTH_BOUND", bound, eq, "Length bound is redundant because lengthEq() is set");
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 makeIssue("warning", "redundancy", groupIndex, constraintIndex, "REDUNDANT_BITMASK", mask, existing, "Bitmask operation is redundant");
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 makeIssue("warning", "redundancy", groupIndex, constraintIndex, "DUPLICATE_CONSTRAINT", ZERO, ZERO, "Duplicate operator in constraint");
1735
- }
1736
- /** gte() bound equals type minimum (always true). */
1737
- function vacuousGte(groupIndex, constraintIndex, value) {
1738
- return makeIssue("info", "vacuity", groupIndex, constraintIndex, "VACUOUS_GTE", value, ZERO, "gte() bound equals type minimum (always true)");
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
- /** lte() bound equals type maximum (always true). */
1741
- function vacuousLte(groupIndex, constraintIndex, value) {
1742
- return makeIssue("info", "vacuity", groupIndex, constraintIndex, "VACUOUS_LTE", value, ZERO, "lte() bound equals type maximum (always true)");
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
- /** lengthGte(0) is always true. */
1745
- function vacuousLengthGte(groupIndex, constraintIndex, value) {
1746
- return makeIssue("info", "vacuity", groupIndex, constraintIndex, "VACUOUS_LENGTH_GTE", value, ZERO, "lengthGte(0) is always true");
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
- /** lengthLte() bound equals maximum (always true). */
1749
- function vacuousLengthLte(groupIndex, constraintIndex, value) {
1750
- return makeIssue("info", "vacuity", groupIndex, constraintIndex, "VACUOUS_LENGTH_LTE", value, ZERO, "lengthLte() bound equals maximum (always true)");
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 makeIssue("warning", "compatibility", groupIndex, constraintIndex, "PATH_DEPTH_EXCEEDED", depth, maxDepth, "Path depth exceeds the reference enforcer cap");
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 makeIssue("warning", "compatibility", groupIndex, constraintIndex, "QUANTIFIER_OVER_STATIC_LIMIT", arrayLength, maxLength, "Quantifier over static array exceeds the reference enforcer cap");
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 makeIssue("warning", "compatibility", groupIndex, constraintIndex, "UNKNOWN_CONTEXT_PROPERTY", contextId, maxContextId, "Unknown context property ID");
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
- eqNeqContradiction: isLength ? lengthEqNeqContradiction : eqNeqContradiction,
1772
- conflicting: isLength ? conflictingLength : conflictingEquality,
1773
- outOfPhysicalBounds: isLength ? outOfPhysicalLengthBounds : outOfPhysicalBounds,
1774
- impossibleGt: isLength ? impossibleLengthGt : impossibleGt,
1775
- impossibleLt: isLength ? impossibleLengthLt : impossibleLt,
1776
- boundsExcludeEquality: isLength ? boundsExcludeLength : boundsExcludeEquality,
1777
- impossibleRange: isLength ? impossibleLengthRange : impossibleRange,
1778
- dominated: isLength ? dominatedLengthBound : dominatedBound,
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 number of tracked "holes" (neq exclusions) and notIn values per
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
- /** Reinterpret a uint256 bigint as a signed int256. */
1805
- function toSigned(v) {
1806
- return v >= INT256_SIGN_BIT ? v - (1n << 256n) : v;
1807
- }
1808
- /** a > b. */
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, emit, groupIndex, constraintIndex, issues) {
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(emit.eqNeqContradiction(groupIndex, constraintIndex, bigintToHex(value)));
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 && domain.holes.length < MAX_HOLES) domain.holes.push(value);
2042
- } else updateBound(domain, negateBoundOp(base), false, value, emit, groupIndex, constraintIndex, issues);
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(emit.vacuousGte(groupIndex, constraintIndex, bigintToHex(value)));
2046
- else if (base === Op.LTE && value === domain.max) issues.push(emit.vacuousLte(groupIndex, constraintIndex, bigintToHex(value)));
2047
- if (isLt(value, domain.min, domain.isSigned) || isGt(value, domain.max, domain.isSigned)) issues.push(emit.outOfPhysicalBounds(groupIndex, constraintIndex, bigintToHex(value)));
2048
- else if (base === Op.GT && value === domain.max) issues.push(emit.impossibleGt(groupIndex, constraintIndex, bigintToHex(value)));
2049
- else if (base === Op.LT && value === domain.min) issues.push(emit.impossibleLt(groupIndex, constraintIndex, bigintToHex(value)));
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(emit.conflicting(groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(value)));
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(emit.eqNeqContradiction(groupIndex, constraintIndex, bigintToHex(value)));
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 (isLt(value, domain.lower, domain.isSigned)) redundant = true;
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(emit.dominated(groupIndex, constraintIndex, bigintToHex(value)));
2270
+ if (redundant) issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2070
2271
  if (strictlyBetter) {
2071
- issues.push(emit.dominated(groupIndex, constraintIndex, bigintToHex(domain.lower)));
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 (isGt(value, domain.upper, domain.isSigned)) redundant = true;
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(emit.dominated(groupIndex, constraintIndex, bigintToHex(value)));
2294
+ if (redundant) issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2094
2295
  if (strictlyBetter) {
2095
- issues.push(emit.dominated(groupIndex, constraintIndex, bigintToHex(domain.upper)));
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 ? isLt(domain.eq, domain.lower, domain.isSigned) : isLte(domain.eq, domain.lower, domain.isSigned)) issues.push(emit.boundsExcludeEquality(groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(domain.lower)));
2109
- else issues.push(emit.redundant(groupIndex, constraintIndex, bigintToHex(domain.lower), bigintToHex(domain.eq)));
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 ? isGt(domain.eq, domain.upper, domain.isSigned) : isGte(domain.eq, domain.upper, domain.isSigned)) issues.push(emit.boundsExcludeEquality(groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(domain.upper)));
2113
- else issues.push(emit.redundant(groupIndex, constraintIndex, bigintToHex(domain.upper), bigintToHex(domain.eq)));
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 (isGt(domain.lower, domain.upper, domain.isSigned) || domain.lower === domain.upper && (!domain.lowerInclusive || !domain.upperInclusive)) issues.push(emit.impossibleRange(groupIndex, constraintIndex, bigintToHex(domain.lower), bigintToHex(domain.upper)));
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
- if (ctx.set.notInValues.length < MAX_NOT_IN) ctx.set.notInValues.push(value);
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 compat = checkCompatibility(base, ctx.typeInfo.typeCode, ctx.typeInfo.isDynamic, ctx.typeInfo.staticSize);
2223
- if (!compat.compatible) {
2224
- issues.push(fromOpRule(compat.code, compat.message, groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
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
- updateBound(ctx.numeric, Op.GTE, isNegated, low, boundIssues(false), groupIndex, constraintIndex, issues);
2231
- updateBound(ctx.numeric, Op.LTE, isNegated, high, boundIssues(false), groupIndex, constraintIndex, issues);
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, boundIssues(false), groupIndex, constraintIndex, issues);
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
- updateBound(ctx.length, Op.GTE, isNegated, low, boundIssues(true), groupIndex, constraintIndex, issues);
2247
- updateBound(ctx.length, Op.LTE, isNegated, high, boundIssues(true), groupIndex, constraintIndex, issues);
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, boundIssues(true), groupIndex, constraintIndex, issues);
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
- const walk = Descriptor.walkPath(descBytes, steps);
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 = value.startsWith("0x") ? value.slice(2) : value;
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
- const body = v.startsWith("0x") ? v.slice(2) : v;
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
- type: "violation",
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
- type: "ok",
2769
- head: cursor,
2770
- base: tupleBase,
2771
- node: node.fields[childIndex]
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
- type: "violation",
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
- type: "violation",
3056
+ ok: false,
2785
3057
  code: lengthResult.code
2786
3058
  };
2787
3059
  if (childIndex >= lengthResult) return {
2788
- type: "violation",
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
- type: "ok",
2795
- head: headsBase + childIndex * 32,
2796
- base: headsBase,
2797
- node: elem
3066
+ ok: true,
3067
+ location: {
3068
+ head: headsBase + childIndex * 32,
3069
+ base: headsBase,
3070
+ node: elem
3071
+ }
2798
3072
  };
2799
3073
  return {
2800
- type: "ok",
2801
- head: headsBase + childIndex * elem.staticSize,
2802
- base: arrayBase,
2803
- node: elem
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
- type: "violation",
3089
+ ok: false,
2814
3090
  code: ptrResult.code
2815
3091
  };
2816
3092
  const arrayBase = base + ptrResult;
2817
3093
  return {
2818
- type: "ok",
2819
- head: arrayBase + childIndex * 32,
2820
- base: arrayBase,
2821
- node: elem
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
- type: "ok",
2826
- head: head + childIndex * elem.staticSize,
2827
- base,
2828
- node: elem
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.type === "violation") return {
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
- const arrayBase = base + arrayBaseResult;
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.type === "violation") return {
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 §10.3). */
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 = canonicalize(toBigInt(result.value, 0), node.typeCode);
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) return {
3243
- group: groupIndex,
3244
- rule: ruleIndex,
3245
- code: result.error,
3246
- scope: Scope.CALLDATA,
3247
- path: pathHex,
3248
- opCode,
3249
- operandData: bytesToHex(operandData),
3250
- typeCode
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,