@callcium/sdk 0.3.0 → 0.5.0

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