@callcium/sdk 0.9.0 → 0.11.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.
@@ -1,32 +1,29 @@
1
1
  import { bigintToHex, bytesToHex, hexToBytes } from "./bytes";
2
- import {
3
- classifyTypeCode,
4
- isQuantifier,
5
- isValidOperatorData,
6
- Limits,
7
- lookupContextProperty,
8
- MAX_CONTEXT_PROPERTY_ID,
9
- Op,
10
- Quantifier,
11
- Scope,
12
- TypeCode,
13
- } from "./constants";
2
+ import { fieldMax, findContextProperty, MAX_CONTEXT_PROPERTY_ID, Op, PolicyFormat, Scope, TypeCode } from "./constants";
14
3
  import { Descriptor, type TypeInfo } from "./descriptor";
15
4
  import { decodeDescriptor } from "./descriptor-coder";
16
5
  import { CallciumError, type CallciumErrorCode } from "./errors";
17
- import { canonicalize, isLeftAligned, isSigned, isLengthOp, isLengthValidType } from "./operators";
18
- import { parsePathSteps } from "./policy-coder";
6
+ import {
7
+ canonicalize,
8
+ isLeftAligned,
9
+ isSigned,
10
+ isLengthOp,
11
+ isLengthValidType,
12
+ isElementary,
13
+ isValidOperatorData,
14
+ toOperatorBytes,
15
+ } from "./operators";
16
+ import { Quantifier, isQuantifier, parsePathSteps } from "./path";
19
17
  import * as ValidationIssue from "./validation-issue";
20
18
 
21
- import type { Constraint, Hex, Issue, PolicyData } from "./types";
19
+ import type { Constraint, Hex, Issue, IssueCode, PolicyData } from "./types";
22
20
 
23
21
  ///////////////////////////////////////////////////////////////////////////
24
22
  // Internal types
25
23
  ///////////////////////////////////////////////////////////////////////////
26
24
 
27
25
  /** Codes a path walk raises when the path does not navigate the descriptor (PV-1). */
28
- const NAVIGABILITY_CODES: ReadonlySet<CallciumErrorCode> = new Set<CallciumErrorCode>([
29
- "EMPTY_PATH",
26
+ const navigabilityCodes: ReadonlySet<CallciumErrorCode> = new Set<CallciumErrorCode>([
30
27
  "PARAM_INDEX_OUT_OF_BOUNDS",
31
28
  "TUPLE_FIELD_OUT_OF_BOUNDS",
32
29
  "STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
@@ -91,8 +88,15 @@ const INT256_SIGN_BIT = 1n << 255n;
91
88
  /** Signed-aware difference of two raw uint256 values: negative if a<b, zero if equal, positive if a>b. */
92
89
  function signedCompare(a: bigint, b: bigint, signed: boolean): bigint {
93
90
  if (!signed) return a - b;
94
- const toS = (v: bigint) => (v >= INT256_SIGN_BIT ? v - (1n << 256n) : v);
95
- return toS(a) - toS(b);
91
+ const toSigned = (word: bigint) => (word >= INT256_SIGN_BIT ? word - (1n << 256n) : word);
92
+ return toSigned(a) - toSigned(b);
93
+ }
94
+
95
+ /** True when a value lies outside the domain's physical bounds. */
96
+ function isOutsideDomain(domain: BoundDomain, value: bigint): boolean {
97
+ return (
98
+ signedCompare(value, domain.min, domain.isSigned) < 0n || signedCompare(value, domain.max, domain.isSigned) > 0n
99
+ );
96
100
  }
97
101
 
98
102
  ///////////////////////////////////////////////////////////////////////////
@@ -159,13 +163,13 @@ function isBitmaskOp(opBase: number): boolean {
159
163
  ///////////////////////////////////////////////////////////////////////////
160
164
 
161
165
  /** Return the incompatibility reason, or null if the operator is allowed. */
162
- function getIncompat(opBase: number, typeInfo: TypeInfo): { code: string; message: string } | null {
166
+ function getIncompat(opBase: number, typeInfo: TypeInfo): { code: IssueCode; message: string } | null {
163
167
  const { typeCode, isDynamic, staticSize } = typeInfo;
164
168
  if (isValueOp(opBase)) {
165
169
  if (isDynamic || staticSize !== 32) {
166
170
  return { code: "VALUE_OP_ON_DYNAMIC", message: "Value operator used on dynamic type" };
167
171
  }
168
- if (classifyTypeCode(typeCode).typeClass !== "elementary") {
172
+ if (!isElementary(typeCode)) {
169
173
  return { code: "VALUE_OP_ON_COMPOSITE", message: "Value operator used on composite type" };
170
174
  }
171
175
  if (isComparisonOp(opBase) && !isNumericType(typeCode)) {
@@ -307,7 +311,8 @@ function initContext(scope: number, path: Hex, steps: number[], typeInfo: TypeIn
307
311
  steps,
308
312
  typeInfo,
309
313
  numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
310
- bitmask: { mustBeOne: 0n, mustBeZero: 0n },
314
+ // Bits above the target's width are zero in every canonical value.
315
+ bitmask: { mustBeOne: 0n, mustBeZero: isBitmaskCompatible(typeInfo.typeCode) ? UINT256_MAX ^ max : 0n },
311
316
  length: emptyBoundDomain(false, 0n, LENGTH_MAX),
312
317
  set: {
313
318
  hasIn: false,
@@ -334,6 +339,10 @@ function updateBound(
334
339
  ): void {
335
340
  if (isNegated) {
336
341
  if (base === Op.EQ) {
342
+ // A hole outside the type's domain excludes nothing.
343
+ if (isOutsideDomain(domain, value)) {
344
+ issues.push(ValidationIssue.outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
345
+ }
337
346
  if (domain.hasEq && domain.eq === value) {
338
347
  issues.push(ValidationIssue.eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
339
348
  }
@@ -361,10 +370,7 @@ function updateBound(
361
370
  }
362
371
 
363
372
  // Physical bounds and impossibility.
364
- if (
365
- signedCompare(value, domain.min, domain.isSigned) < 0n ||
366
- signedCompare(value, domain.max, domain.isSigned) > 0n
367
- ) {
373
+ if (isOutsideDomain(domain, value)) {
368
374
  issues.push(ValidationIssue.outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
369
375
  } else if (base === Op.GT && value === domain.max) {
370
376
  issues.push(ValidationIssue.impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
@@ -684,6 +690,13 @@ function updateSet(
684
690
  constraintIndex: number,
685
691
  issues: Issue[],
686
692
  ): void {
693
+ // Physical bounds: a member outside the type's domain never occurs, so it neither matches nor excludes anything.
694
+ for (const value of values) {
695
+ if (isOutsideDomain(ctx.numeric, value)) {
696
+ issues.push(ValidationIssue.outOfPhysicalBounds(false, groupIndex, constraintIndex, bigintToHex(value)));
697
+ }
698
+ }
699
+
687
700
  if (isNegated) {
688
701
  for (const value of values) {
689
702
  if (ctx.numeric.hasEq && ctx.numeric.eq === value) {
@@ -705,16 +718,6 @@ function updateSet(
705
718
  }
706
719
  checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
707
720
  } else {
708
- // Physical bounds: a member outside the type's domain can never be matched.
709
- for (const value of values) {
710
- if (
711
- signedCompare(value, ctx.numeric.min, ctx.numeric.isSigned) < 0n ||
712
- signedCompare(value, ctx.numeric.max, ctx.numeric.isSigned) > 0n
713
- ) {
714
- issues.push(ValidationIssue.outOfPhysicalBounds(false, groupIndex, constraintIndex, bigintToHex(value)));
715
- }
716
- }
717
-
718
721
  if (ctx.numeric.hasEq) {
719
722
  let found = false;
720
723
  for (const candidate of values) {
@@ -820,6 +823,11 @@ function checkFusibleRange(
820
823
  }
821
824
  }
822
825
 
826
+ /** Check whether a payload length fits the `dataLength` field and matches the operator's arity. */
827
+ function hasValidPayload(opBase: number, dataLength: number): boolean {
828
+ return dataLength <= fieldMax(PolicyFormat.RULE_DATALENGTH_SIZE) && isValidOperatorData(opBase, dataLength);
829
+ }
830
+
823
831
  /** Return the operand pair when the operators contain exactly one of each unnegated bound opcode. */
824
832
  function findLonePair(operators: Hex[], lowerOp: number, upperOp: number): { low: bigint; high: bigint } | null {
825
833
  let lowerCount = 0;
@@ -830,7 +838,7 @@ function findLonePair(operators: Hex[], lowerOp: number, upperOp: number): { low
830
838
  for (const opHex of operators) {
831
839
  const opCode = parseInt(opHex.slice(2, 4), 16);
832
840
  const dataLength = (opHex.length - 4) / 2;
833
- if (dataLength > 0xffff || !isValidOperatorData(opCode & ~Op.NOT, dataLength)) continue;
841
+ if (!hasValidPayload(opCode & ~Op.NOT, dataLength)) continue;
834
842
  if (opCode === lowerOp) {
835
843
  lowerCount++;
836
844
  low = readValue(opHex);
@@ -859,12 +867,14 @@ function validateConstraint(
859
867
  const underAny = constraint.scope === Scope.CALLDATA && ctx.steps.includes(Quantifier.ANY);
860
868
 
861
869
  for (const opHex of operators) {
862
- const opCode = parseInt(opHex.slice(2, 4), 16);
870
+ // Every later read slices the hex, so the operator is established as bytes first.
871
+ const opBytes = toOperatorBytes(opHex);
872
+ const opCode = opBytes[0]!;
863
873
  const base = opCode & ~Op.NOT;
864
874
 
865
875
  // An unassigned opcode or mismatched payload size has no defined semantics to analyze.
866
- const dataLength = (opHex.length - 4) / 2;
867
- if (dataLength > 0xffff || !isValidOperatorData(base, dataLength)) {
876
+ const dataLength = opBytes.length - 1;
877
+ if (!hasValidPayload(base, dataLength)) {
868
878
  issues.push(
869
879
  ValidationIssue.fromOpRule(
870
880
  "UNKNOWN_OPERATOR",
@@ -901,7 +911,8 @@ function validateConstraint(
901
911
  if (base === Op.EQ_CTX) {
902
912
  // The operand names a context property, not a value: nothing to fold into a domain.
903
913
  const ctxOperand = readValue(opHex);
904
- if (ctxOperand > BigInt(MAX_CONTEXT_PROPERTY_ID)) {
914
+ const property = findContextProperty(Number(ctxOperand));
915
+ if (!property) {
905
916
  issues.push(
906
917
  ValidationIssue.unknownContextProperty(
907
918
  groupIndex,
@@ -913,7 +924,7 @@ function validateConstraint(
913
924
  } else {
914
925
  // Compatibility has already narrowed the target to an address or unsigned type,
915
926
  // so the pairing reduces to whether both sides are addresses.
916
- const propertyIsAddress = lookupContextProperty(Number(ctxOperand)).typeCode === TypeCode.ADDRESS;
927
+ const propertyIsAddress = property.typeCode === TypeCode.ADDRESS;
917
928
  const targetIsAddress = ctx.typeInfo.typeCode === TypeCode.ADDRESS;
918
929
  if (propertyIsAddress !== targetIsAddress) {
919
930
  issues.push(
@@ -1033,16 +1044,17 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
1033
1044
 
1034
1045
  if (!ctx) {
1035
1046
  const steps = parsePathSteps(constraint.path);
1047
+ if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
1036
1048
  let typeInfo: TypeInfo;
1037
1049
  if (constraint.scope === Scope.CALLDATA) {
1038
1050
  // Compatibility warnings against the limits the spec fixes (PWF-17, PV-7).
1039
- if (steps.length > Limits.MAX_PATH_DEPTH) {
1051
+ if (steps.length > PolicyFormat.MAX_PATH_DEPTH) {
1040
1052
  issues.push(
1041
1053
  ValidationIssue.pathDepthExceeded(
1042
1054
  groupIndex,
1043
1055
  constraintIndex,
1044
1056
  bigintToHex(BigInt(steps.length)),
1045
- bigintToHex(BigInt(Limits.MAX_PATH_DEPTH)),
1057
+ bigintToHex(BigInt(PolicyFormat.MAX_PATH_DEPTH)),
1046
1058
  ),
1047
1059
  );
1048
1060
  }
@@ -1051,7 +1063,7 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
1051
1063
  walk = Descriptor.walkPath(descBytes, steps);
1052
1064
  } catch (error) {
1053
1065
  // Navigability faults become issues; anything else propagates.
1054
- if (error instanceof CallciumError && NAVIGABILITY_CODES.has(error.code)) {
1066
+ if (error instanceof CallciumError && navigabilityCodes.has(error.code)) {
1055
1067
  issues.push(ValidationIssue.unnavigablePath(groupIndex, constraintIndex));
1056
1068
  continue;
1057
1069
  }
@@ -1062,20 +1074,21 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
1062
1074
  issues.push(ValidationIssue.nestedQuantifier(groupIndex, constraintIndex));
1063
1075
  continue;
1064
1076
  }
1065
- if (walk.quantifiedStaticLength > Limits.MAX_QUANTIFIED_ARRAY_LENGTH) {
1077
+ if (walk.quantifiedStaticLength > PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH) {
1066
1078
  issues.push(
1067
1079
  ValidationIssue.quantifierOverStaticLimit(
1068
1080
  groupIndex,
1069
1081
  constraintIndex,
1070
1082
  bigintToHex(BigInt(walk.quantifiedStaticLength)),
1071
- bigintToHex(BigInt(Limits.MAX_QUANTIFIED_ARRAY_LENGTH)),
1083
+ bigintToHex(BigInt(PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH)),
1072
1084
  ),
1073
1085
  );
1074
1086
  }
1075
1087
  typeInfo = walk.typeInfo;
1076
1088
  } else {
1077
1089
  const ctxId = steps[0]!;
1078
- if (ctxId > MAX_CONTEXT_PROPERTY_ID) {
1090
+ const property = findContextProperty(ctxId);
1091
+ if (!property) {
1079
1092
  // An unassigned ID declares no type, so the analysis proceeds over the widest
1080
1093
  // unsigned domain and the warning carries the ID.
1081
1094
  issues.push(
@@ -1086,24 +1099,22 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
1086
1099
  bigintToHex(BigInt(MAX_CONTEXT_PROPERTY_ID)),
1087
1100
  ),
1088
1101
  );
1089
- typeInfo = { typeCode: TypeCode.UINT_MAX, isDynamic: false, staticSize: 32 };
1090
- } else {
1091
- const typeCode = lookupContextProperty(ctxId).typeCode;
1092
- typeInfo = { typeCode, isDynamic: false, staticSize: 32 };
1093
1102
  }
1103
+ typeInfo = { typeCode: property?.typeCode ?? TypeCode.UINT_MAX, isDynamic: false, staticSize: 32 };
1094
1104
  }
1095
1105
  ctx = initContext(constraint.scope, normalizedPath, steps, typeInfo);
1096
1106
  contexts.push(ctx);
1097
1107
  }
1098
1108
 
1099
1109
  // A carried hint must equal the compilation of its own path; constraints without an encoding
1100
- // carry none, and the encoder compiles theirs from the same descriptor. Reaching here means
1101
- // the path navigates the descriptor and quantifies at most once, so only the depth PWF-17
1102
- // admits remains to be established.
1110
+ // carry none, an empty one carries nothing to compare, and the encoder compiles theirs from
1111
+ // the same descriptor. Reaching here means the path navigates the descriptor and quantifies at
1112
+ // most once, so only the depth PWF-17 admits remains to be established.
1103
1113
  if (
1104
1114
  constraint.scope === Scope.CALLDATA &&
1105
1115
  constraint.hint !== undefined &&
1106
- ctx.steps.length <= Limits.MAX_PATH_DEPTH
1116
+ constraint.hint !== "0x" &&
1117
+ ctx.steps.length <= PolicyFormat.MAX_PATH_DEPTH
1107
1118
  ) {
1108
1119
  const compiled = bytesToHex(Descriptor.compileHint(descBytes, ctx.steps));
1109
1120
  if (compiled !== constraint.hint.toLowerCase()) {
@@ -1127,6 +1138,9 @@ function validateGroup(data: PolicyData, descBytes: Uint8Array, groupIndex: numb
1127
1138
  * @returns All validation issues found, ordered by group and constraint index.
1128
1139
  * @throws {CallciumError} If the descriptor is not well-formed; semantic validation is
1129
1140
  * defined only over a well-formed descriptor.
1141
+ * @throws {CallciumError} With code `EMPTY_PATH` when a constraint carries no path step, and
1142
+ * `INVALID_HEX` or `INVALID_OPERATOR_BYTES` when an operator is not a readable encoding. Analysis
1143
+ * is defined over canonical data, so these reject the input rather than report an issue on it.
1130
1144
  */
1131
1145
  function validate(data: PolicyData): Issue[] {
1132
1146
  const descBytes = hexToBytes(data.descriptor);
package/src/signature.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { keccak_256 } from "@noble/hashes/sha3.js";
2
+ import { utf8ToBytes } from "@noble/hashes/utils.js";
2
3
 
3
4
  import { bytesToHex } from "./bytes";
4
5
  import { CallciumError } from "./errors";
@@ -18,13 +19,13 @@ export type ParsedSignature = {
18
19
  };
19
20
 
20
21
  /** Return true if the char code is an ASCII letter. */
21
- function isAlpha(c: number): boolean {
22
- return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
22
+ function isAlpha(char: number): boolean {
23
+ return (char >= 0x41 && char <= 0x5a) || (char >= 0x61 && char <= 0x7a);
23
24
  }
24
25
 
25
26
  /** Return true if the char code is an ASCII alphanumeric character. */
26
- function isAlphanum(c: number): boolean {
27
- return isAlpha(c) || (c >= 0x30 && c <= 0x39);
27
+ function isAlphanum(char: number): boolean {
28
+ return isAlpha(char) || (char >= 0x30 && char <= 0x39);
28
29
  }
29
30
 
30
31
  /**
@@ -36,29 +37,27 @@ function isAlphanum(c: number): boolean {
36
37
  * @param signature - A function signature string, e.g. `"transfer(address,uint256)"`.
37
38
  * @returns The 4-byte selector and the comma-separated types string.
38
39
  * @throws {CallciumError} with code `"MALFORMED_SIGNATURE"`, `"SIGNATURE_CONTAINS_WHITESPACE"`, or
39
- * `"INVALID_FUNCTION_NAME"` if the signature is malformed.
40
+ * `"INVALID_FUNCTION_NAME"` if the signature is malformed. The types between the parentheses are
41
+ * returned unread; whether they name types is for the type parser to say.
40
42
  */
41
43
  function parse(signature: string): ParsedSignature {
42
44
  if (signature.length < 3) {
43
45
  throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: too short, got "${signature}"`);
44
46
  }
45
47
 
46
- // Scan for whitespace, non-ASCII, and the opening parenthesis in one pass.
48
+ // Scan for whitespace and the opening parenthesis in one pass.
47
49
  let openParen = -1;
48
50
  for (let i = 0; i < signature.length; i++) {
49
- const c = signature.charCodeAt(i);
50
- if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) {
51
+ const char = signature.charCodeAt(i);
52
+ if (char === 0x20 || char === 0x09 || char === 0x0a || char === 0x0d) {
51
53
  throw new CallciumError("SIGNATURE_CONTAINS_WHITESPACE", "Signature must not contain whitespace");
52
54
  }
53
- if (c > 0x7e) {
54
- throw new CallciumError("MALFORMED_SIGNATURE", "Signature must contain only ASCII characters");
55
- }
56
- if (openParen === -1 && c === 0x28) {
55
+ if (openParen === -1 && char === 0x28) {
57
56
  openParen = i;
58
57
  }
59
58
  }
60
59
 
61
- if (openParen < 1) {
60
+ if (openParen === -1) {
62
61
  throw new CallciumError(
63
62
  "MALFORMED_SIGNATURE",
64
63
  `Invalid signature: must have a function name followed by parentheses, got "${signature}"`,
@@ -69,14 +68,19 @@ function parse(signature: string): ParsedSignature {
69
68
  throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: must end with ")", got "${signature}"`);
70
69
  }
71
70
 
72
- // Validate function name: [A-Za-z_][A-Za-z0-9_]*.
71
+ if (openParen === 0) {
72
+ throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must not be empty");
73
+ }
74
+
75
+ // Validate function name: [A-Za-z_][A-Za-z0-9_]*. Every character outside ASCII carries a code
76
+ // unit above the alphanumeric ranges, so the same test rejects it.
73
77
  const firstChar = signature.charCodeAt(0);
74
78
  if (!isAlpha(firstChar) && firstChar !== 0x5f) {
75
79
  throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must start with a letter or underscore");
76
80
  }
77
81
  for (let i = 1; i < openParen; i++) {
78
- const c = signature.charCodeAt(i);
79
- if (!isAlphanum(c) && c !== 0x5f) {
82
+ const char = signature.charCodeAt(i);
83
+ if (!isAlphanum(char) && char !== 0x5f) {
80
84
  throw new CallciumError(
81
85
  "INVALID_FUNCTION_NAME",
82
86
  "Function name must contain only alphanumeric characters or underscores",
@@ -84,9 +88,7 @@ function parse(signature: string): ParsedSignature {
84
88
  }
85
89
  }
86
90
 
87
- const encoded = new Uint8Array(signature.length);
88
- for (let i = 0; i < signature.length; i++) encoded[i] = signature.charCodeAt(i);
89
- const hash = keccak_256(encoded);
91
+ const hash = keccak_256(utf8ToBytes(signature));
90
92
 
91
93
  const selector = bytesToHex(hash.subarray(0, 4));
92
94
  const types = signature.slice(openParen + 1, signature.length - 1);
package/src/type-desc.ts CHANGED
@@ -48,7 +48,7 @@ export function function_(): Uint8Array {
48
48
  */
49
49
  export function uintN(bits: number): Uint8Array {
50
50
  if (bits < 8 || bits > 256 || bits % 8 !== 0) {
51
- throw new CallciumError("UNKNOWN_TYPE", `Invalid uintN bits: ${bits}. Must be 8–256 in steps of 8.`);
51
+ throw new CallciumError("UNKNOWN_TYPE", `Invalid uintN bits: ${bits}. Must be 8–256 in steps of 8`);
52
52
  }
53
53
  return new Uint8Array([TypeCode.UINT_MIN + (bits / 8 - 1)]);
54
54
  }
@@ -60,7 +60,7 @@ export function uintN(bits: number): Uint8Array {
60
60
  */
61
61
  export function intN(bits: number): Uint8Array {
62
62
  if (bits < 8 || bits > 256 || bits % 8 !== 0) {
63
- throw new CallciumError("UNKNOWN_TYPE", `Invalid intN bits: ${bits}. Must be 8–256 in steps of 8.`);
63
+ throw new CallciumError("UNKNOWN_TYPE", `Invalid intN bits: ${bits}. Must be 8–256 in steps of 8`);
64
64
  }
65
65
  return new Uint8Array([TypeCode.INT_MIN + (bits / 8 - 1)]);
66
66
  }
@@ -77,14 +77,14 @@ export function string_(): Uint8Array {
77
77
 
78
78
  /**
79
79
  * Build a descriptor node for `bytesN`.
80
- * @param n - Byte width, 1–32.
81
- * @throws {CallciumError} With code `UNKNOWN_TYPE` if n is out of range.
80
+ * @param length - Byte width, 1–32.
81
+ * @throws {CallciumError} With code `UNKNOWN_TYPE` if length is out of range.
82
82
  */
83
- export function bytesN(n: number): Uint8Array {
84
- if (n < 1 || n > 32) {
85
- throw new CallciumError("UNKNOWN_TYPE", `Invalid bytesN size: ${n}. Must be 1–32.`);
83
+ export function bytesN(length: number): Uint8Array {
84
+ if (length < 1 || length > 32) {
85
+ throw new CallciumError("UNKNOWN_TYPE", `Invalid bytesN size: ${length}. Must be 1–32`);
86
86
  }
87
- return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (n - 1)]);
87
+ return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (length - 1)]);
88
88
  }
89
89
 
90
90
  ///////////////////////////////////////////////////////////////////////////
@@ -108,7 +108,7 @@ export function array(elemDesc: Uint8Array, length?: number): Uint8Array {
108
108
  if (nodeLength > DF.MAX_NODE_LENGTH) {
109
109
  throw new CallciumError(
110
110
  "NODE_LENGTH_TOO_LARGE",
111
- `Dynamic array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}.`,
111
+ `Dynamic array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}`,
112
112
  );
113
113
  }
114
114
  const meta24 = (0 << DF.META_STATIC_WORDS_SHIFT) | nodeLength;
@@ -123,14 +123,14 @@ export function array(elemDesc: Uint8Array, length?: number): Uint8Array {
123
123
  if (length <= 0 || length > DF.MAX_STATIC_ARRAY_LENGTH) {
124
124
  throw new CallciumError(
125
125
  "INVALID_ARRAY_LENGTH",
126
- `Static array length ${length} out of range. Must be 1–${DF.MAX_STATIC_ARRAY_LENGTH}.`,
126
+ `Static array length ${length} out of range. Must be 1–${DF.MAX_STATIC_ARRAY_LENGTH}`,
127
127
  );
128
128
  }
129
129
  const nodeLength = DF.ARRAY_HEADER_SIZE + elemDesc.length + DF.ARRAY_LENGTH_SIZE;
130
130
  if (nodeLength > DF.MAX_NODE_LENGTH) {
131
131
  throw new CallciumError(
132
132
  "NODE_LENGTH_TOO_LARGE",
133
- `Static array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}.`,
133
+ `Static array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}`,
134
134
  );
135
135
  }
136
136
  const elemStaticWords = extractStaticWords(elemDesc);
@@ -138,7 +138,7 @@ export function array(elemDesc: Uint8Array, length?: number): Uint8Array {
138
138
  if (staticWords > DF.MAX_STATIC_WORDS) {
139
139
  throw new CallciumError(
140
140
  "STATIC_WORDS_TOO_LARGE",
141
- `Static array static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}.`,
141
+ `Static array static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}`,
142
142
  );
143
143
  }
144
144
  const meta24 = (staticWords << DF.META_STATIC_WORDS_SHIFT) | nodeLength;
@@ -162,7 +162,7 @@ export function tuple(fieldDescs: Uint8Array[]): Uint8Array {
162
162
  if (fieldDescs.length === 0 || fieldDescs.length > DF.MAX_TUPLE_FIELDS) {
163
163
  throw new CallciumError(
164
164
  "INVALID_TUPLE_FIELD_COUNT",
165
- `Tuple field count ${fieldDescs.length} out of range. Must be 1–${DF.MAX_TUPLE_FIELDS}.`,
165
+ `Tuple field count ${fieldDescs.length} out of range. Must be 1–${DF.MAX_TUPLE_FIELDS}`,
166
166
  );
167
167
  }
168
168
  const totalFieldBytes = fieldDescs.reduce((sum, f) => sum + f.length, 0);
@@ -170,7 +170,7 @@ export function tuple(fieldDescs: Uint8Array[]): Uint8Array {
170
170
  if (nodeLength > DF.MAX_NODE_LENGTH) {
171
171
  throw new CallciumError(
172
172
  "NODE_LENGTH_TOO_LARGE",
173
- `Tuple node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}.`,
173
+ `Tuple node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}`,
174
174
  );
175
175
  }
176
176
 
@@ -187,7 +187,7 @@ export function tuple(fieldDescs: Uint8Array[]): Uint8Array {
187
187
  if (staticWords > DF.MAX_STATIC_WORDS) {
188
188
  throw new CallciumError(
189
189
  "STATIC_WORDS_TOO_LARGE",
190
- `Tuple static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}.`,
190
+ `Tuple static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}`,
191
191
  );
192
192
  }
193
193
  const meta24 = (staticWords << DF.META_STATIC_WORDS_SHIFT) | nodeLength;
package/src/types.ts CHANGED
@@ -130,7 +130,7 @@ export type CalldataNavigationViolation =
130
130
  | CalldataNavigationVariant<"CALLDATA_OUT_OF_BOUNDS">
131
131
  | CalldataNavigationVariant<"ARRAY_INDEX_OUT_OF_BOUNDS">;
132
132
 
133
- /** A quantified array exceeded `Limits.MAX_QUANTIFIED_ARRAY_LENGTH`. */
133
+ /** A quantified array exceeded `PolicyFormat.MAX_QUANTIFIED_ARRAY_LENGTH`. */
134
134
  export type QuantifierLimitExceededViolation = {
135
135
  code: "QUANTIFIER_LIMIT_EXCEEDED";
136
136
  group: number;
@@ -142,7 +142,7 @@ export type QuantifierLimitExceededViolation = {
142
142
  };
143
143
 
144
144
  /**
145
- * A resolved word is not the canonical encoding of its declared type (spec §7.4).
145
+ * A resolved word is not the canonical encoding of its declared type.
146
146
  *
147
147
  * `resolvedValue` is the raw 32-byte word as it appeared in calldata, before any masking.
148
148
  */
@@ -274,13 +274,80 @@ export type IssueSeverity = "info" | "warning" | "error";
274
274
  /** Category of a validation issue. */
275
275
  export type IssueCategory = "typeMismatch" | "contradiction" | "redundancy" | "vacuity" | "compatibility";
276
276
 
277
+ /**
278
+ * Machine-readable code for a policy validation issue.
279
+ *
280
+ * Each code names one finding of the semantic validator and matches the constant of the same
281
+ * name in the Solidity `IssueCode` library.
282
+ */
283
+ export type IssueCode =
284
+ // Type mismatch.
285
+ | "VALUE_OP_ON_DYNAMIC"
286
+ | "VALUE_OP_ON_COMPOSITE"
287
+ | "NUMERIC_OP_ON_NON_NUMERIC"
288
+ | "BITMASK_ON_INVALID"
289
+ | "IN_ON_BOOL"
290
+ | "LENGTH_ON_STATIC"
291
+ | "CONTEXT_TYPE_MISMATCH"
292
+ | "UNKNOWN_OPERATOR"
293
+ | "NON_CANONICAL_OPERAND"
294
+ | "UNNAVIGABLE_PATH"
295
+ | "NESTED_QUANTIFIER"
296
+ | "HINT_MISMATCH"
297
+ // Contradiction.
298
+ | "EQ_NEQ_CONTRADICTION"
299
+ | "CONFLICTING_EQUALITY"
300
+ | "OUT_OF_PHYSICAL_BOUNDS"
301
+ | "IMPOSSIBLE_GT"
302
+ | "IMPOSSIBLE_LT"
303
+ | "BOUNDS_EXCLUDE_EQUALITY"
304
+ | "IMPOSSIBLE_RANGE"
305
+ | "SET_EXCLUDES_EQUALITY"
306
+ | "EMPTY_SET_INTERSECTION"
307
+ | "SET_FULLY_EXCLUDED"
308
+ | "LENGTH_EQ_NEQ_CONTRADICTION"
309
+ | "CONFLICTING_LENGTH"
310
+ | "BOUNDS_EXCLUDE_LENGTH"
311
+ | "IMPOSSIBLE_LENGTH_RANGE"
312
+ | "OUT_OF_PHYSICAL_LENGTH_BOUNDS"
313
+ | "IMPOSSIBLE_LENGTH_GT"
314
+ | "IMPOSSIBLE_LENGTH_LT"
315
+ | "BITMASK_CONTRADICTION"
316
+ | "BITMASK_ANY_IMPOSSIBLE"
317
+ | "UNSORTED_IN_SET"
318
+ | "EMPTY_GROUP"
319
+ // Redundancy.
320
+ | "DOMINATED_BOUND"
321
+ | "REDUNDANT_BOUND"
322
+ | "SET_REDUCTION"
323
+ | "SET_REDUNDANCY"
324
+ | "SET_PARTIALLY_EXCLUDED"
325
+ | "DOMINATED_LENGTH_BOUND"
326
+ | "REDUNDANT_LENGTH_BOUND"
327
+ | "REDUNDANT_BITMASK"
328
+ | "DUPLICATE_CONSTRAINT"
329
+ | "FUSIBLE_RANGE"
330
+ | "FUSIBLE_LENGTH_RANGE"
331
+ // Vacuity.
332
+ | "VACUOUS_GTE"
333
+ | "VACUOUS_LTE"
334
+ | "VACUOUS_LENGTH_GTE"
335
+ | "VACUOUS_LENGTH_LTE"
336
+ | "VACUOUS_NEGATED_RANGE"
337
+ | "VACUOUS_NEGATED_LENGTH_RANGE"
338
+ // Compatibility.
339
+ | "PATH_DEPTH_EXCEEDED"
340
+ | "QUANTIFIER_OVER_STATIC_LIMIT"
341
+ | "UNKNOWN_CONTEXT_PROPERTY"
342
+ | "NEGATION_UNDER_ANY";
343
+
277
344
  /** A single validation issue found during policy analysis. */
278
345
  export type Issue = {
279
346
  severity: IssueSeverity;
280
347
  category: IssueCategory;
281
348
  groupIndex: number;
282
349
  constraintIndex: number;
283
- code: string;
350
+ code: IssueCode;
284
351
  value1: Hex;
285
352
  value2: Hex;
286
353
  message: string;
@@ -1,4 +1,4 @@
1
- import type { Hex, Issue } from "./types";
1
+ import type { Hex, Issue, IssueCode } from "./types";
2
2
 
3
3
  ///////////////////////////////////////////////////////////////////////////
4
4
  // Constants
@@ -12,7 +12,7 @@ const ZERO: Hex = `0x${"0".repeat(64)}`;
12
12
 
13
13
  /** Create an issue from operator compatibility results. */
14
14
  export function fromOpRule(
15
- code: string,
15
+ code: IssueCode,
16
16
  message: string,
17
17
  groupIndex: number,
18
18
  constraintIndex: number,
@@ -304,7 +304,7 @@ export function unsortedInSet(groupIndex: number, constraintIndex: number): Issu
304
304
  code: "UNSORTED_IN_SET",
305
305
  value1: ZERO,
306
306
  value2: ZERO,
307
- message: "isIn/notIn set is not strictly sorted and deduplicated.",
307
+ message: "isIn/notIn set is not strictly sorted and deduplicated",
308
308
  };
309
309
  }
310
310
 
@@ -318,7 +318,7 @@ export function emptyGroup(groupIndex: number): Issue {
318
318
  code: "EMPTY_GROUP",
319
319
  value1: ZERO,
320
320
  value2: ZERO,
321
- message: "Group contains zero constraints.",
321
+ message: "Group contains zero constraints",
322
322
  };
323
323
  }
324
324