Policy
Binary encoding format for onchain calldata policies.
1. Document Control
- Version: 2.0
- Status: Normative
2. Purpose, Scope, and Exclusions
This document specifies the canonical binary format for Policies in Callcium, a policy engine for ABI-encoded data. A Policy defines constraints that ABI-encoded data must satisfy to be considered compliant.
Scope
- Binary format and encoding rules.
- Validation semantics.
- Canonicalization requirements.
Exclusions
This document does not define:
- Builder API design or usage patterns.
- Implementation strategies, gas optimization techniques, or evaluation order heuristics.
- Application-specific policy templates.
- Descriptor format (see Callcium Descriptor Spec).
3. Terminology and Conformance
- The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY in this document are to be interpreted as described in RFC 2119.
- "Validator" refers to any component that checks a policy blob against the invariants of Section 8.
- "Builder" refers to any component that constructs canonical policy blobs from higher-level definitions.
- "Enforcer" refers to any component that evaluates a policy against calldata and execution context.
An implementation is conformant if and only if it meets all MUST/REQUIRED obligations in Sections 4–9.
4. Wire Format
4.1 Policy Structure
+--------+----------+------------+----------+------------+----------+
| header | selector | descLength | desc | groupCount | groups |
| 1 byte | 4 bytes | 2 bytes | variable | 1 byte | variable |
+--------+----------+------------+----------+------------+----------+| Offset | Field | Size | Description |
|---|---|---|---|
| 0 | header | 1 | Composite header byte (see below) |
| 1 | selector | 4 | Function selector (must be 0x00000000 if selectorless) |
| 5 | descLength | 2 | Descriptor length in bytes (big-endian) |
| 7 | desc | descLength | Embedded function descriptor |
| 7+descLength | groupCount | 1 | Number of rule groups [1, 255] |
| 8+descLength | groups | var | Concatenated group records |
Header byte layout:
+----------+------------------+----------+
| reserved | FLAG_NO_SELECTOR | version |
| bits 7-5 | bit 4 | bits 3-0 |
+----------+------------------+----------+- Bits 3-0 (version): Format version (
VERSION, Section 5.1). - Bit 4 (FLAG_NO_SELECTOR): If set, the policy targets raw ABI-encoded calldata without a 4-byte function selector. The selector slot is ignored by the enforcer, which uses
baseOffset = 0instead of4. - Bits 7-5 (reserved): MUST be zero. Validators MUST reject non-zero reserved bits.
Invariants:
(header & VERSION_MASK) == VERSION. The policy format version is independent of the descriptor format version.(header & RESERVED_MASK) == 0x00. Reserved bits must be zero.- If
(header & FLAG_NO_SELECTOR) != 0:selector == 0x00000000(canonical encoding). descLength >= 2(the minimum descriptor header: version + paramCount).descis a well-formed descriptor blob (Callcium Descriptor Spec, Section 7.1).groupCount >= 1(empty policy is invalid).- Groups are stored contiguously, group-major order.
4.2 Group Structure
+-----------+-----------+----------+
| ruleCount | groupSize | rules |
| 2 bytes | 4 bytes | variable |
+-----------+-----------+----------+| Offset | Field | Size | Description |
|---|---|---|---|
| 0 | ruleCount | 2 | Number of rules in group (big-endian) |
| 2 | groupSize | 4 | Total bytes of rules payload (big-endian) |
| 6 | rules | var | Concatenated rule records |
Invariants:
- Group identity = position in blob (0-indexed).
ruleCount >= 1(empty group is invalid).groupSize == sum of all rule bytes in this group.- Rules within a group are sorted by
(scope, pathDepth, pathBytes, operatorBytes)ascending.
Semantics:
- Rules within a group have AND semantics (all must pass).
- Groups have OR semantics (first passing group succeeds).
- This structure is Disjunctive Normal Form (DNF): an OR of ANDs.
4.3 Rule Structure
+----------+--------+-----------+---------------+----------------+--------+------------+------------------+
| ruleSize | scope | pathDepth | path | hint | opCode | dataLength | data |
| 2 bytes | 1 byte | 1 byte | 2*depth bytes | hintSize bytes | 1 byte | 2 bytes | dataLength bytes |
+----------+--------+-----------+---------------+----------------+--------+------------+------------------+| Offset | Field | Size | Description |
|---|---|---|---|
| 0 | ruleSize | 2 | Total size of this rule in bytes, including this field (big-endian) |
| 2 | scope | 1 | Rule scope (0=context, 1=calldata) |
| 3 | pathDepth | 1 | Number of path steps |
| 4 | path | 2*depth | Path steps (big-endian uint16 each) |
| 4+2*depth | hint | hintSize | Compiled hint block (calldata rules only; see below) |
| 4+2*depth+hintSize | opCode | 1 | Comparison operator |
| 5+2*depth+hintSize | dataLength | 2 | Length of data section (big-endian) |
| 7+2*depth+hintSize | data | dataLength | Operator-specific data |
Invariants:
ruleSize == 4 + pathDepth*2 + hintSize + 3 + dataLength.- When
scope == 0:pathDepth == 1,path[0]is a reserved context property ID, andhintSize == 0(context rules carry no hint block). - When
scope == 1:pathDepth >= 1, the path navigates calldata structure, and the rule carries a hint block.
Compiled Hint Block:
The hint block encodes how to reach the rule's target from baseOffset (Section 7.1), compiled from the rule's path against the embedded descriptor. A node is indirected when ABI encodes it behind an offset word: composites with zero staticWords, and bytes/string. Addressing is a chain of hops of two kinds: a plain hop enters an indirected node's payload through its offset word; an element hop resolves one element of an array the chain has already reached. Every path a valid policy admits (PV-1–PV-3) compiles to this form; there is no fallback form. Hint constants are defined in Section 5.8.
+--------+------------------+-----------------------------+---------------------------------+
| header | hops | frame (quantified only) | target |
| 1 byte | 8*hopCount bytes | 9 + 8*suffixHopCount bytes | 7 bytes |
+--------+------------------+-----------------------------+---------------------------------+| Field | Size | Description |
|---|---|---|
| header | 1 | Bits 7–6: kind — HINT_KIND_NONE, HINT_KIND_ALL, or HINT_KIND_ANY; value 3 is reserved. Bits 5–0: hopCount. |
| hops | 8 × hopCount | Hop entries of the main chain. |
| arrayDelta | 4 | Quantified only. Byte offset, relative to the base after the main chain, of the quantified array's data: the length word for a dynamic array, the element region otherwise. |
| count | 2 | Quantified only. Declared element count for a static array; 0 for a dynamic array, whose count is read from calldata. Unambiguous: a static array's declared length is at least 1 (Callcium Descriptor Spec, DWF-6). |
| meta | 2 | Quantified only. Meta word of the quantified array. |
| suffixHeader | 1 | Quantified only. Bits 7–6: reserved. Bits 5–0: suffixHopCount. |
| suffix hops | 8 × suffixHopCount | Quantified only. Hop entries of the element chain, relative to each element's base. |
| targetDelta | 4 | Byte offset of the target, relative to the final base. |
| targetMeta | 2 | Meta word of the target when typeCode is the dynamic array code; zero otherwise. Consumed by the payload-extent check (Section 7.2). |
| typeCode | 1 | Descriptor type code of the target (Callcium Descriptor Spec, Section 5). |
The frame and suffix hops are present if and only if kind is not HINT_KIND_NONE, so hintSize is 8 + 8*hopCount for an unquantified rule and 17 + 8*(hopCount + suffixHopCount) for a quantified rule — a function of the header and suffixHeader bytes alone.
Hop entry — [delta:4][index:2][meta:2]:
- Plain hop (
index == HINT_NO_INDEX):deltais the byte offset of the entered node's offset word, relative to the current base;metais zero. - Element hop (
index != HINT_NO_INDEX):indexis the concrete element index andmetathe meta word of the array the chain has reached;deltais zero. The value 0xFFFE never appears inindex.
Meta word — big-endian uint16 describing an array node:
- Bit 15 (
HINT_META_ELEM_DYNAMIC): set when the element type is dynamic. - Bit 14 (
HINT_META_DYNAMIC_ARRAY): set when the node is a dynamic array. - Bits 13–12: reserved.
- Bits 11–0 (
strideWords): element head size in words —elemStaticWordsfor static elements (Callcium Descriptor Spec, Section 4.4), 1 for dynamic elements (one offset word per element). The element stride in bytes isstrideWords * 32.
All multi-byte hint fields are big-endian. Delta fields are uint32, relative to the chain's current base; the chain starts at baseOffset, so hint bytes are identical for selector and selectorless policies.
Compilation. The hint is a deterministic function of the rule's path and the embedded descriptor. Compilation walks the path with a byte accumulator delta, initially zero. Entering an indirected node emits plain hop [delta, HINT_NO_INDEX, 0] and resets delta to zero — unless the chain already ends inside it (the node was the dynamic element resolved by a preceding element hop or by the frame's element step), in which case nothing is emitted.
- A step through a static container adds the child's head offset to
delta. A node's head slot spans(staticWords == 0 ? 1 : staticWords) * 32bytes — an indirected node's slot is its offset word. For a tuple or the parameter list, the head offset is the sum of the head slot spans of preceding fields or parameters; for a static array of static elements, it isindex * elemStaticWords * 32. - A step into a field of a dynamic tuple enters the tuple; the field's head offset then accumulates per rule 1.
- A concrete-index step into a dynamic array, or into a static array of dynamic elements, enters the array, then emits element hop
[0, index, meta(array)]. - A quantifier step enters the array if it is indirected, closes the main chain with
arrayDelta = delta,countandmetafrom the array node, and resetsdeltato zero; the remaining steps compile into the suffix chain by these same rules. - After the last step: if the target node is indirected, enter it and set
targetDelta = 0; otherwisetargetDelta = delta. typeCodeis the target node's code;targetMetaismeta(target)when the target is a dynamic array, zero otherwise;kindand the hop counts fill the header and suffixHeader.
Type Resolution:
- For calldata rules: the target type is the hint's
typeCode; for valid policies it equals the code resolved by navigating the descriptor using the rule's path (PV-6). - For context rules: type is implicit from the context property ID.
4.4 Data Encoding
All multi-byte integers are big-endian. All values in the data section are encoded as fixed 32-byte fields.
Operator Data Formats:
| Operator | dataLength | Format | Description |
|---|---|---|---|
| EQ | 32 | [value:32] | Single comparison value |
| GT | 32 | [bound:32] | Lower bound (exclusive) |
| LT | 32 | [bound:32] | Upper bound (exclusive) |
| GTE | 32 | [bound:32] | Lower bound (inclusive) |
| LTE | 32 | [bound:32] | Upper bound (inclusive) |
| BETWEEN | 64 | [min:32][max:32] | Range bounds (inclusive) |
| IN | 32*n | [v1:32][v2:32]... | Set members (n = dataLength/32), sorted and deduped |
| BITMASK_ALL | 32 | [mask:32] | Required bits (all must be set) |
| BITMASK_ANY | 32 | [mask:32] | Any-of bits (at least one set) |
| BITMASK_NONE | 32 | [mask:32] | Forbidden bits (none may be set) |
| LENGTH_EQ | 32 | [length:32] | Exact length |
| LENGTH_GT | 32 | [bound:32] | Minimum length (exclusive) |
| LENGTH_LT | 32 | [bound:32] | Maximum length (exclusive) |
| LENGTH_GTE | 32 | [bound:32] | Minimum length (inclusive) |
| LENGTH_LTE | 32 | [bound:32] | Maximum length (inclusive) |
| LENGTH_BETWEEN | 64 | [min:32][max:32] | Length range (inclusive) |
Length operators apply to dynamic arrays (element count) and bytes/string (byte length). Static arrays are forbidden.
4.5 Type-Specific Encoding
The encodings in this section define the canonical 32-byte form for operator operands and the form required of resolved calldata values (§7.4).
Address (typeCode = 0x41):
[0x000000000000000000000000][address:20]Left-padded with 12 zero bytes.
Unsigned Integers (typeCode = 0x01-0x20):
[padding][value]Left-padded to 32 bytes. Value occupies rightmost N bytes where N = typeCode.
Signed Integers (typeCode = 0x21-0x40):
[sign-extension][value]Sign-extended to 32 bytes (two's complement). Comparison operators (GT, LT, GTE, LTE, BETWEEN) MUST use signed arithmetic (EVM slt/sgt). The EQ and IN operators use bitwise equality and are sign-agnostic.
Boolean (typeCode = 0x42):
[0x00...00][0x00 or 0x01]0x00 = false, 0x01 = true. Only EQ and its negation are valid for booleans.
Function (typeCode = 0x43):
[address:20][selector:4][0x0000000000000000]External function pointer: a 20-byte address followed by a 4-byte selector (24 bytes total). Encoded identical to bytes24 — left-aligned in the high 24 bytes and padded with 8 trailing zero bytes.
Fixed Bytes (typeCode = 0x50-0x6F):
[value:N][0x00...00]Right-padded with zeros. N = typeCode - 0x4F.
5. Constants
5.1 Version and Header
VERSION = 0x02 // format version (lower nibble of header)
VERSION_MASK = 0x0F // mask to extract version from header
FLAG_NO_SELECTOR = 0x10 // bit 4: selectorless policy
RESERVED_MASK = 0xE0 // bits 7-5: must be zero5.2 Header Sizes
POLICY_HEADER_PREFIX = 7 // header(1) + selector(4) + descLength(2)
GROUP_HEADER_SIZE = 6 // ruleCount(2) + groupSize(4)
RULE_MIN_SIZE = 9 // ruleSize(2) + scope(1) + pathDepth(1) + path(2) + opCode(1) + dataLength(2)5.3 Scope Values
SCOPE_CONTEXT = 0x00
SCOPE_CALLDATA = 0x015.4 Context Property IDs
When scope == SCOPE_CONTEXT, the path contains exactly one step identifying the context property:
CTX_MSG_SENDER = 0x0000 // msg.sender (address)
CTX_MSG_VALUE = 0x0001 // msg.value (uint256)
CTX_BLOCK_TIMESTAMP = 0x0002 // block.timestamp (uint256)
CTX_BLOCK_NUMBER = 0x0003 // block.number (uint256)
CTX_CHAIN_ID = 0x0004 // chain.id (uint256)
CTX_TX_ORIGIN = 0x0005 // tx.origin (address)
CTX_BASE_FEE = 0x0006 // block.basefee (uint256)
CTX_GAS_PRICE = 0x0007 // tx.gasprice (uint256)Builders MUST validate operator-type compatibility for context rules using the declared types above (address or uint256). Enforcers MUST treat all context values as raw 32-byte words at evaluation time; runtime type checking is not required.
5.5 Path Quantifiers
ALL = 0xFFFF // Universal quantifier (∀): passes for ALL elements; empty arrays yield true (vacuous)
ANY = 0xFFFE // Existential quantifier (∃): passes for AT LEAST ONE element; empty arrays yield falseReserved index range: indices i >= 0xFFFE are reserved for quantifiers. Valid concrete indices are 0..0xFFFD.
Strict universality — every element passes and the array is non-empty — is expressed by composing a LENGTH_GT 0 rule with an ALL rule in the same group.
5.6 Operator Codes
Encoding: [NOT:1 bit][OPERATOR:7 bits]
- Bit 7 (0x80): NOT flag — inverts the operator result.
- Bits 0-6: Operator code.
Operator code 0x00 is unassigned and MUST be rejected.
Base Operators (0x01–0x7F):
OP_EQ = 0x01 // value == operand
OP_GT = 0x02 // value > operand
OP_LT = 0x03 // value < operand
OP_GTE = 0x04 // value >= operand
OP_LTE = 0x05 // value <= operand
OP_BETWEEN = 0x06 // min <= value <= max (inclusive)
OP_IN = 0x07 // value in {v1, v2, ...}
OP_BITMASK_ALL = 0x10 // (value & mask) == mask
OP_BITMASK_ANY = 0x11 // (value & mask) != 0
OP_BITMASK_NONE = 0x12 // (value & mask) == 0
OP_LENGTH_EQ = 0x20 // length(value) == operand
OP_LENGTH_GT = 0x21 // length(value) > operand
OP_LENGTH_LT = 0x22 // length(value) < operand
OP_LENGTH_GTE = 0x23 // length(value) >= operand
OP_LENGTH_LTE = 0x24 // length(value) <= operand
OP_LENGTH_BETWEEN = 0x25 // min <= length(value) <= maxNegation:
NOT_FLAG = 0x80Negated forms follow the same type restrictions as their base operators.
5.7 Operator-Type Compatibility Matrix
| Operator | Valid Types |
|---|---|
EQ | All 32-byte static elementary types. |
GT, LT, GTE, LTE, BETWEEN | Numeric types (UINT*, INT*) only. |
IN | All 32-byte static elementary types except BOOL. |
BITMASK_* | Unsigned integer types (UINT*) and BYTES32 only. |
LENGTH_* | BYTES, STRING, DYNAMIC_ARRAY only. Static arrays forbidden. |
Value operators (EQ, GT, LT, GTE, LTE, BETWEEN, IN, BITMASK_*) require 32-byte static elementary types; dynamic and composite types are incompatible.
5.8 Compiled Hint Constants
HINT_HOP_SIZE = 8 // hop entry: delta(4) + index(2) + meta(2)
HINT_KIND_NONE = 0x0 // header kind: unquantified rule
HINT_KIND_ALL = 0x1 // header kind: universal quantifier
HINT_KIND_ANY = 0x2 // header kind: existential quantifier
HINT_NO_INDEX = 0xFFFF // hop index: the crossing selects no element
HINT_META_ELEM_DYNAMIC = 0x8000 // meta bit 15: element type is dynamic
HINT_META_DYNAMIC_ARRAY = 0x4000 // meta bit 14: the node is a dynamic array
HINT_META_STRIDE_MASK = 0x0FFF // meta bits 11-0: element head size in words6. Path Encoding and Rule Ordering
6.1 Path Format
Path is encoded as a sequence of big-endian uint16 values:
[step0:2][step1:2]...[stepN:2]6.2 Path Interpretation
path[0]: Top-level parameter index (0-based).path[1..n]: Navigation into nested structures.- For tuples: field index.
- For arrays: element index,
ALL(0xFFFF), orANY(0xFFFE).
6.3 Quantifier Constraints
ALLandANYsteps are only valid immediately after array nodes.- A path MUST contain at most one quantifier step. Nested quantifiers are forbidden in this format version.
- Valid concrete indices are
0..0xFFFD.
6.4 Examples
// Function: foo(address recipient, uint256 amount)
// Rule: amount >= 100
path = [0x0001] // parameter index 1
// Function: bar((address token, uint256 amount) payment)
// Rule: payment.amount <= 1000
path = [0x0000, 0x0001] // parameter 0, field 1
// Function: baz(address[] recipients)
// Rule: all recipients in allowlist (universal, vacuous on empty)
path = [0x0000, 0xFFFF] // parameter 0, ALL elements
// Rule: at least one recipient in allowlist (existential)
path = [0x0000, 0xFFFE] // parameter 0, ANY element6.5 Canonical Rule Sort Key
Rules within each group MUST be sorted by (scope, pathDepth, pathBytes, operatorBytes) in ascending order.
Sort priority:
scope: 0 (context) before 1 (calldata).pathDepth: shorter paths before longer.pathBytes: lexicographic comparison of path bytes.operatorBytes: lexicographic comparison ofopCode || data(tie-breaker for multiple rules on the same path).
The hint block does not participate in the sort key: it is a deterministic function of the path and the descriptor (PV-6).
6.6 Comparison Algorithm
function compareRules(a, b):
// Primary: scope
if a.scope != b.scope:
return a.scope - b.scope
// Secondary: pathDepth
if a.pathDepth != b.pathDepth:
return a.pathDepth - b.pathDepth
// Tertiary: pathBytes (lexicographic)
for i in 0 ..< a.pathDepth: // exclusive upper bound
if a.path[i] < b.path[i]: return -1
if a.path[i] > b.path[i]: return +1
// Quaternary: operatorBytes (lexicographic over opCode || data)
return lexicographicCompare(a.operatorBytes, b.operatorBytes)Lexicographic comparison of byte arrays: compare byte-by-byte from index 0. At the first differing byte, the array with the smaller byte value sorts first. If all bytes of the shorter array match the corresponding prefix of the longer array, the shorter array sorts first.
6.7 Sort Invariants
scope == 0⇒pathDepth == 1with reserved context property ID.scope == 1⇒pathDepth >= 1with BE16 path steps.
7. Evaluation
7.1 Evaluation Algorithm
- Extract version from header byte (
header & VERSION_MASK); verify== VERSION. - If
FLAG_NO_SELECTORis not set: verify selector in calldata matches policy selector; setbaseOffset = 4. - If
FLAG_NO_SELECTORis set: skip selector validation; setbaseOffset = 0. - Locate the groups region past the embedded descriptor (
descLength). - Evaluate groups in order; first passing group succeeds (OR semantics).
- Within each group, all rules must pass (AND semantics).
7.2 Rule Evaluation
- Context rules (
scope == 0) resolve the value from the execution environment using the context property ID inpath[0]. - Calldata rules (
scope == 1) resolve the target location through the hint block exclusively; path bytes are not consulted at evaluation time. Everyword(x)below is a bounds-checked 32-byte calldata load: a read past the end of calldata is aCALLDATA_OUT_OF_BOUNDSviolation. Offset and extent arithmetic MUST NOT wrap: a sum or product that overflows is likewise aCALLDATA_OUT_OF_BOUNDSviolation. The main chain resolves as:
chain(base, hops):
for each hop [delta, index, meta]:
if index == HINT_NO_INDEX:
base = base + word(base + delta)
else:
elems = base
if meta.DYNAMIC_ARRAY:
if index >= word(elems): violation ARRAY_INDEX_OUT_OF_BOUNDS
elems = elems + 32
slot = elems + index * meta.strideWords * 32
base = meta.ELEM_DYNAMIC ? elems + word(slot) : slot
return base- Unquantified rule:
target = chain(baseOffset, hops) + targetDelta. For a static target the word attargetis the value. For a dynamic target the chain ends at the target's payload, so the word attargetis the declared length (LENGTH_*rules only, Section 5.7). - Quantified rule: element addressing per Section 7.3.
- A static target's resolved value is checked against the canonical encoding of its declared type per §7.4, then the value and type code are checked against the operator and data.
- For
LENGTH_*operators, the resolved value is the declared length read at the target's payload base: the element count for dynamic arrays, the byte length forbytesandstring. Enforcers MUST verify that the declared data extent —length * stridebytes starting attarget + 32, where the stride is 1 forbytes/stringandtargetMeta.strideWords * 32for dynamic arrays — lies within calldata bounds before applying the operator; an overrun is aCALLDATA_OUT_OF_BOUNDSviolation. Length values are compared as raw unsigned words; the canonical-encoding check of §7.4 does not apply to them.
7.3 Quantifier Handling
A rule whose hint kind is HINT_KIND_ALL or HINT_KIND_ANY evaluates against concrete elements. The frame resolves the array and its elements:
base = chain(baseOffset, hops)
elems = base + arrayDelta
if count == 0: count = word(elems); elems = elems + 32
for index in 0 .. count-1:
slot = elems + index * meta.strideWords * 32
elem = meta.ELEM_DYNAMIC ? elems + word(slot) : slot
target(index) = chain(elem, suffix hops) + targetDeltaBefore iterating: a count read from calldata above MAX_QUANTIFIED_ARRAY_LENGTH is a QUANTIFIER_LIMIT_EXCEEDED violation (a declared count above it cannot occur in a valid policy, PV-7); a count of zero yields the empty-array semantics of Section 5.5 — HINT_KIND_ALL passes, HINT_KIND_ANY is a QUANTIFIER_EMPTY_ARRAY violation. The hint kind alone selects these semantics.
Each element's resolved value is evaluated per Section 7.2. Universal quantification fails on the first failing element; existential quantification passes on the first passing element.
7.4 Canonical Value Encoding
Before applying an operator to a static target's resolved value, an enforcer MUST verify that the value carries the encoding defined in §4.5 for its declared type. The declared type is the rule's target type code (Section 4.3, Type Resolution). Length values are exempt (§7.2). For the raw 32-byte word loaded at the resolved location, that encoding requires:
- Unsigned integers (
0x01–0x20),address,bool: all bits above the type's value width are zero —N * 8bits foruintN, 160 foraddress, 1 forbool. - Signed integers (
0x21–0x40): the word is sign-extended from the type's most-significant byte (EVMSIGNEXTEND). - Fixed bytes (
0x50–0x6F) andfunction(0x43): the low(32 − N)padding bytes are zero (the value is left-aligned in the highNbytes;N = 24forfunction, encoded identical tobytes24). uint256,int256,bytes32: unconstrained; the value occupies the full word.
A word that does not meet the requirement for its declared type is a NON_CANONICAL_VALUE violation.
Context values (scope == 0) are exempt: per §5.4 they are evaluated as raw 32-byte words.
7.5 Conformance Boundary
This specification defines evaluation semantics for well-formed policies (Section 8.1). Enforcers are not required to verify validity (Section 8.2) or canonical form (Section 8.3). Evaluating a well-formed but invalid policy may surface implementation-defined integrity errors (Section 9.2). When such a policy is evaluated rather than rejected, a calldata rule's stored hint remains authoritative even where it diverges from the compilation of its path: resolution follows the hint (Section 7.2). Hint–path agreement is a validity property (PV-6), not an evaluation-time check. Canonical form affects byte identity only, not the verdict.
8. Validation Rules
8.1 Well-Formedness
A policy is well-formed if it satisfies all of the following invariants. Validators MUST reject a policy that is not well-formed before evaluating it. Where that rejection happens — decoding, storage, or a standalone validation pass — is implementation-defined.
- PWF-1: The policy is at least 8 bytes (minimum fixed header).
- PWF-2:
(header & VERSION_MASK) == VERSION. - PWF-3:
(header & RESERVED_MASK) == 0x00. - PWF-4: If
FLAG_NO_SELECTORis set,selector == 0x00000000. - PWF-5:
descLength >= 2. - PWF-6:
7 + descLength + 1does not exceed the policy blob size. - PWF-7:
descis a well-formed descriptor (Callcium Descriptor Spec, Section 7.1). - PWF-8:
groupCount >= 1. - PWF-9: Every group has
ruleCount >= 1. - PWF-10: Every group has
groupSize >= ruleCount * RULE_MIN_SIZE. - PWF-11: The rules of every group exactly fill its declared
groupSize. - PWF-12: No trailing bytes remain after the last group.
- PWF-13: Every rule's
ruleSizeequals the computed size (4 + pathDepth*2 + hintSize + 3 + dataLength, wherehintSizeis 0 for context rules and, for calldata rules, follows from the hint's header and suffixHeader bytes per Section 4.3). - PWF-14: Every rule's
scopeis 0 or 1. - PWF-15: Every context rule (
scope == 0) haspathDepth == 1. - PWF-16: Every context rule's
path[0]is a defined context property ID (Section 5.4). - PWF-17: Every rule has
pathDepth <= MAX_PATH_DEPTH(Section 8.4). - PWF-18: Every rule has
pathDepth >= 1. - PWF-19: Every rule's
opCode(masked with0x7F) is a defined operator. - PWF-20: Every rule's
dataLengthmatches its operator's data format (Section 4.4). - PWF-21: Every
INoperator's operands are strictly ascending by lexicographic comparison of their 32-byte encodings. Strict ascent implies deduplication. - PWF-22: No calldata rule's hint block carries a reserved or unused state: the header
kindis not 3, reserved bits of the suffixHeader and of every meta word are zero, no hopindexis 0xFFFE, a plain hop'smetaand an element hop'sdeltaare zero, andtargetMetais zero unlesstypeCodeis the dynamic array code. - PWF-23: Every calldata rule's hint
typeCodeis an elementary type code or the dynamic array code (Callcium Descriptor Spec, Section 5). - PWF-24: Every calldata rule's
opCode(masked with0x7F) is aLENGTH_*operator when its hinttypeCodeisbytes,string, or the dynamic array code, and a value operator otherwise (Sections 5.6–5.7).
8.2 Validity
A policy is valid if it is well-formed and satisfies the following invariants. Builders MUST NOT emit an invalid policy. Enforcers are not required to verify these invariants (Section 7.5).
- PV-1: Every calldata rule's path navigates the descriptor without stepping into an elementary type, past a tuple's field count, or past a static array's declared length.
- PV-2: Every operator is compatible with its target's declared type per the compatibility matrix (Section 5.7).
- PV-3: Quantifier steps (
ALL/ANY) appear only immediately after array nodes, and reserved indices (>= 0xFFFE) do not appear as explicit indices. - PV-4: No group contains two byte-identical rules. Rules sharing a path are otherwise permitted: a single rule definition may compile to multiple binary rules on the same path — range composition (e.g.,
gte(5)+lte(10)) produces two rules, or may be optimized into a singleBETWEEN. Definition-level uniqueness — at most one definition per(scope, pathBytes)pair within a group — is a builder obligation, not observable in the encoded policy: a multi-operator definition and multiple single-operator definitions on the same path encode identically. - PV-5: Every group is satisfiable. Builders MUST detect at least: bound contradictions (conflicting equalities, values outside type range, impossible ranges), set contradictions (empty intersection, all values excluded), and bitmask contradictions (conflicting
bitmaskAll/bitmaskNonebits). Builders MAY detect more. - PV-6: Every calldata rule's hint block equals the deterministic compilation of its path against the embedded descriptor (Section 4.3).
- PV-7: No quantifier applies to a static array whose declared length exceeds
MAX_QUANTIFIED_ARRAY_LENGTH— the failure is descriptor-fixed, so it is a validity defect rather than a runtime violation.
8.3 Canonical Form
A policy is canonical if it is valid and its encoding satisfies the following invariants. Builders MUST emit canonical policies: two canonical encodings of the same policy are byte-identical, so keccak256(policy) identifies the policy. Canonical form does not affect verdicts (Section 7.5).
- PC-1: Operator operands use the canonical 32-byte encodings of Section 4.5.
- PC-2: Rules within each group are sorted by
(scope, pathDepth, pathBytes, operatorBytes)ascending (Sections 6.5–6.6). - PC-3: Groups are sorted ascending by group hash, where
groupHash = keccak256(ruleBytes)andruleBytesis the concatenation of the group's rule byte sequences in their already-sorted order (PC-2). The group hash is not serialized; it is derived from the on-wire rule bytes for sorting purposes only.
8.4 Normative Limits
| Constant | Value | Category | Invariant | Derivation |
|---|---|---|---|---|
MAX_PATH_DEPTH | 32 steps | Design | PWF-17 | The 1-byte pathDepth field allows 255; capped to bound per-rule path bytes and compilation work. |
| Hops per chain | 63 | Format | — | 6-bit hopCount fields in the hint header and suffixHeader. Compilation emits at most pathDepth + 1 hops per chain, so under PWF-17 the field encodes every chain a well-formed policy compiles. |
MAX_QUANTIFIED_ARRAY_LENGTH | 256 elements | Design | PV-7 | Bound on ALL/ANY iteration: a calldata length above it is a QUANTIFIER_LIMIT_EXCEEDED violation (Section 9.1); a declared static length above it is a validity defect (PV-7). |
MAX_POLICY_SIZE | 24,575 bytes | Design | — | Storage bound enforced by the onchain registry at store time; not a wire-format invariant. |
RULE_MIN_SIZE | 9 bytes | Derived | PWF-10 | Minimal parseable rule record: a context rule (no hint block) with one path step and an empty data section (Section 5.2). Not attainable by a well-formed rule — every operator payload is at least 32 bytes (PWF-20); this is a structural lower bound for PWF-10 only. |
IN set cardinality | [1, 2,047] | Derived | PWF-20 | Lower bound: variadic data is a positive multiple of 32 (Section 4.4); upper bound: ⌊65,535 / 32⌋ from the 2-byte dataLength. |
| Groups per policy | 255 | Format | — | 1-byte groupCount. |
| Rules per group | 65,535 | Format | — | 2-byte ruleCount. |
| Rule size | 65,535 bytes | Format | — | 2-byte ruleSize. |
| Group size | 4,294,967,295 bytes | Format | — | 4-byte groupSize. |
| Operator payload | 65,535 bytes | Format | — | 2-byte dataLength. |
| Path depth (encoded) | 255 | Format | — | 1-byte pathDepth; the effective cap is MAX_PATH_DEPTH. |
| Descriptor length | 65,535 bytes | Format | — | 2-byte descLength. |
Limits without an invariant reference (—) are either bounds of the encoding itself (no byte string can exceed them) or enforced outside policy validation, as noted.
Limit categories:
- Format: Structural constraint from the binary encoding field width. Cannot change without a format version bump.
- Derived: Mechanically follows from other limits.
- Design: Operational cap chosen for evaluation cost, storage, or usability. Normative and fixed for this format version: all conformant implementations enforce the same value.
Descriptor format limits are defined in the Callcium Descriptor Spec, Section 7.3. Conformant policy encoders MUST respect those limits when constructing embedded descriptors.
9. Enforcement Outcome Semantics
9.1 Enforcement Violations
Violations are calldata-dependent failures: different calldata could change the outcome. Conformant implementations SHOULD use these codes when exposing machine-readable enforcement outcomes. Implementations MAY map them to reverts, return values, exceptions, or other diagnostics as appropriate for the execution environment.
| Code | Description |
|---|---|
VALUE_MISMATCH | Logical operator not satisfied on the resolved value. |
NON_CANONICAL_VALUE | Resolved word is not the canonical encoding of its declared type (§7.4). |
SELECTOR_MISMATCH | Calldata selector does not match the policy header. |
MISSING_SELECTOR | Calldata too short to contain a 4-byte selector. |
CALLDATA_OUT_OF_BOUNDS | Runtime calldata read failure: calldata truncated or offset points beyond available bytes. |
ARRAY_INDEX_OUT_OF_BOUNDS | Dynamic array in calldata is shorter than the index required by the rule. |
MISSING_CONTEXT | Recognized context property not provided at runtime. |
QUANTIFIER_LIMIT_EXCEEDED | Array length exceeds the enforcer iteration limit. |
QUANTIFIER_EMPTY_ARRAY | ANY quantifier evaluated over an empty array. |
9.2 Integrity Errors
Integrity errors are descriptor-fixed or policy-fixed failures: no calldata can resolve them. They correspond to violations of validity invariants (Section 8.2), such as a path targeting a non-existent tuple field: builders MUST NOT emit policies containing them, and enforcers MAY retain checks for them as defense-in-depth. Names and granularity are implementation-specific.
9.3 Violation Effects and Control Flow
Each violation code has a normative effect on evaluation:
| Code | Effect |
|---|---|
VALUE_MISMATCH | Group-local |
NON_CANONICAL_VALUE | Abort |
SELECTOR_MISMATCH | Abort |
MISSING_SELECTOR | Abort |
CALLDATA_OUT_OF_BOUNDS | Abort |
ARRAY_INDEX_OUT_OF_BOUNDS | Abort |
MISSING_CONTEXT | Group-local |
QUANTIFIER_LIMIT_EXCEEDED | Abort |
QUANTIFIER_EMPTY_ARRAY | Group-local |
- Group-local: the containing group fails; evaluation continues with the next group.
- Abort: evaluation stops; the policy rejects without evaluating further groups.
Enforcers MUST produce the same accept/reject verdict for identical policy, calldata, and context. Control flow and reporting MAY differ: a fail-fast enforcer that stops at the first failure and a collect-all enforcer that reports every violation of the evaluated groups are both conformant, provided the verdict is identical.
10. References
- ABI Specification (Solidity documentation, applicable to all EVM languages).
- RFC 2119 — Key words for use in RFCs to Indicate Requirement Levels.
- Callcium Descriptor Spec.
- Callcium reference implementation (non-normative).
Appendix A. Changelog
- v2.0 (2026-08-05): Initial specification.