Callcium

Descriptor

Binary format for describing ABI types in Callcium.

1. Document Control

  • Version: 2.0
  • Status: Normative

2. Purpose, Scope, and Exclusions

This document specifies the Descriptor binary format for Callcium, a policy engine for ABI-encoded data. A descriptor lets a reader traverse ABI-encoded data deterministically at runtime, to validate, parse and extract values. The reader does not need the full ABI JSON.

Exclusions

This document does not define:

  • Validation rule languages or constraints on values (see Callcium Policy Spec).
  • Contract- or application-specific policies.
  • Non-ABI encodings.
  • Implementation strategies, gas optimization techniques, or API design.

3. Terminology and Conformance

  • This document uses the key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED and MAY. RFC 2119 defines their meaning.
  • "Validator" refers to any component that checks a descriptor blob against the invariants of Section 7.
  • "Builder" refers to any component that constructs descriptor blobs from higher-level type definitions.
  • "Reader" refers to any component that traverses ABI-encoded calldata using a descriptor.
  • "ABI word" means a 32-byte slot in ABI encoding.

An implementation is conformant if and only if it meets all MUST/REQUIRED obligations in Sections 4–7.


4. Wire Format

4.1 Top-Level Structure

[version:1][paramCount:1][param_0][param_1]…[param_(paramCount-1)]

version is a single byte that identifies the descriptor format version. This document describes format version VERSION = 0x02. The descriptor format version is independent of the policy format version (see Callcium Policy Spec). Validators MUST reject any descriptor with version != VERSION.

paramCount is a single byte that declares the number of top-level parameters (0–255). The descriptor body MUST contain exactly paramCount parameter descriptors, in sequence, with no padding between them. Validators MUST reject a descriptor if the parsed count is not equal to paramCount, or if bytes remain after the last parameter.

4.2 Elementary Types (Leaf Nodes)

In this document, "elementary" refers to descriptor leaf nodes: types that a single type code byte encodes, with no nested children. Elementary types include bytes (0x70) and string (0x71). These two types are dynamic in ABI encoding but elementary in the descriptor tree.

  • Encoding: [typeCode].
  • Node length is exactly one byte.
  • ABI head contribution is one word (32 bytes) for all elementary types except bytes and string, which are dynamic.
  • For traversal, elementary types have an implied staticWords: 1 for static elementary types (all except bytes and string), 0 for dynamic elementary types (bytes, string). Composite types carry explicit staticWords in their metadata.

4.3 Composite Types (Common Structure)

All composite types start with [typeCode][meta].

meta is a big-endian 24-bit (3-byte) field split into two 12-bit components:

  • staticWords (high 12 bits, bits 23–12): number of 32-byte words in the ABI head for this node when the node is static. Zero means that the node is dynamic.
  • nodeLength (low 12 bits, bits 11–0): total descriptor node length in bytes for this node.

The following constants define the composite header layout:

  • COMPOSITE_META_SIZE = 3.
  • TUPLE_HEADER_SIZE = 6 (typeCode + meta + fieldCount).
  • ARRAY_HEADER_SIZE = 4 (typeCode + meta). This size applies to static arrays and to dynamic arrays.
  • ARRAY_LENGTH_SIZE = 2 (big-endian uint16).

4.4 Static Arrays

Layout: [STATIC_ARRAY][meta][elemDesc][length].

  • length is big-endian uint16.
  • staticWords encodes length * elemStaticWords, or zero if the element type is dynamic. In this document, elemStaticWords is the staticWords metadata value of the element node (or the implied value 1 for static elementary types, 0 for dynamic elementary types).

4.5 Dynamic Arrays

Layout: [DYNAMIC_ARRAY][meta][elemDesc].

Element descriptors follow immediately after the common header.

4.6 Tuples

Layout: [TUPLE][meta][fieldCount:be16][field_0][field_1]…[field_(fieldCount-1)].

  • fieldCount is big-endian uint16.
  • If any field is dynamic, staticWords is zero. Otherwise, staticWords is the sum of the static words of all fields.

5. Type Codes

5.1 Type Code Ranges

RangeCategoryDescription
0x00Reserved ZeroMUST NOT be used. Validators MUST reject a descriptor that contains this code
0x01–0x20Unsigned Integers32 variants: uint8 … uint256
0x21–0x40Signed Integers32 variants: int8 … int256
0x41–0x4FFixed Typesaddress, bool, function (external function pointer)
0x50–0x6FFixed Bytes32 variants: bytes1 … bytes32
0x70–0x7FDynamic Elementarybytes, string
0x80–0x8FArraysstatic array, dynamic array
0x90–0x9FTuplesaggregate of fields
0xA0–0xFFReservedMUST NOT be used. Validators MUST reject a descriptor that contains one of these codes

5.2 Derivation Formulas

The following formulas are normative and MUST produce the same code points as Appendix A:

  • uint<N>: code = N / 8 → 0x01–0x20.
  • int<N>: code = 0x20 + (N / 8) → 0x21–0x40.
  • bytes<N> (N ∈ {1, ..., 32}): code = 0x4F + N → 0x50–0x6F.

5.3 Reserved Codes

Code 0x00 is permanently reserved. Code 0x00 is not a valid type code in this format version or in any future format version. Validators MUST reject 0x00 at every type code position.

In the ranges 0x41–0x4F, 0x70–0x7F, 0x80–0x8F and 0x90–0x9F, the codes that Appendix A does not list are reserved for future use in their range. Validators MUST reject these codes in this format version.


6. Calldata Traversal

When a reader navigates a path through ABI-encoded calldata, the reader MUST keep a state triple (head, base, descOffset).

6.1 State Triple

VariableDescription
headByte offset in calldata for the head slot of the current node.
baseStart offset of a composite. Readers resolve relative ABI offsets from this offset.
descOffsetByte offset into the descriptor for the current node's type.

6.2 Initial State

For a baseOffset (the byte offset where the ABI-encoded parameters start):

  • head = baseOffset, base = baseOffset, descOffset = HEADER_SIZE (2).
  • To find the target parameter, the reader steps through the parameters before the target. For each of these parameters, the reader advances head by the parameter's head contribution and descOffset by the parameter's nodeLength.

The calling layer determines baseOffset. Standard calldata uses baseOffset = 4, after the 4-byte selector. Raw ABI payloads use baseOffset = 0.

6.3 Tuple Descent

To navigate to field childIndex of a tuple at (head, base, descOffset):

  1. Read isDynamic from the tuple's staticWords metadata (0 = dynamic).
  2. Compute the tuple's data region:
    • Dynamic: tupleBase = base + calldataload(head), cursor = tupleBase.
    • Static: tupleBase = base, cursor = head.
  3. Skip fields 0..childIndex-1. For each skipped field, advance cursor by (staticWords == 0 ? 1 : staticWords) * 32 and descOffset by the field's nodeLength (1 for elementary types).
  4. The result is (head = cursor, base = tupleBase, descOffset = fieldDescOffset).

6.4 Array Descent

To navigate to element childIndex of an array at (head, base, descOffset):

In both cases below, elementStaticSize = elemStaticWords * 32. This value is the ABI head size of the element in bytes, from the descriptor metadata of the element (Section 4.4 defines elemStaticWords).

Dynamic array:

  1. arrayBase = base + calldataload(head).
  2. length = calldataload(arrayBase). The reader MUST check childIndex < length.
  3. headsSection = arrayBase + 32 (skip the length word).
  4. If the elements are dynamic: newHead = headsSection + (childIndex * 32), newBase = headsSection.
  5. If the elements are static: newHead = headsSection + (childIndex * elementStaticSize), newBase = arrayBase.

Static array:

  1. If the elements are dynamic: arrayBase = base + calldataload(head), newHead = arrayBase + (childIndex * 32), newBase = arrayBase.
  2. If the elements are static: newHead = head + (childIndex * elementStaticSize), newBase = base (unchanged).

Key invariant: For dynamic elements, the reader MUST set base to the start of the heads section, not to the start of the array data. Per-element offsets are relative to the start of the heads section.

6.5 Bounds Checks

Readers MUST verify that each ABI offset from calldata points within the calldata bounds. If an offset causes a read past calldatasize(), traversal MUST fail instead of a silent mis-parse.

  • Bounds: For every 32-byte read at offset offset, readers MUST check offset + 32 <= calldatasize(). For a dynamic-length read (for example, a bytes or string payload), readers MUST check that the declared length does not extend past the end of calldata.
  • Arithmetic overflow: When readers compute a resolved offset as base + offset, readers MUST make sure that the addition does not overflow.
  • Word alignment: Readers are not required to check that ABI offsets are 32-byte aligned. An implementation MAY reject a non-aligned offset as a strictness option. Conformance does not require this check.
  • Overlapping regions: Readers are not required to detect overlapping data regions. An implementation MAY detect overlaps as an optional strictness check.

7. Validation Rules

7.1 Well-Formedness

A descriptor is well-formed if it satisfies all of the following invariants. Validators MUST reject a descriptor that is not well-formed before a reader uses the descriptor for calldata traversal (Section 6). The implementation defines where the rejection occurs: at decoding, at storage, or in a standalone validation pass.

  • DWF-1: The descriptor is at least 2 bytes (version and paramCount are present).
  • DWF-2: version == VERSION.
  • DWF-3: Every type code has an assignment in Appendix A. Reserved and unassigned codes (Section 5.3) do not appear.
  • DWF-4: Every composite node's metadata lies within the descriptor, and its nodeLength is at least the node's header size. The span of each composite node (nodeLength bytes from its type code) does not extend past the end of the descriptor.
  • DWF-5: Every tuple has fieldCount in [1, MAX_TUPLE_FIELDS].
  • DWF-6: Every static array has length in [1, MAX_STATIC_ARRAY_LENGTH].
  • DWF-7: No composite node has a nesting depth greater than MAX_NESTING_DEPTH. The nesting depth of a node is the number of nodes on the path from its top-level parameter node to the node, both ends included. The limit does not apply to leaf nodes.
  • DWF-8: The descriptor body contains exactly paramCount complete parameter nodes with no trailing bytes.

7.2 Validity

A descriptor is valid if it is well-formed and satisfies the following invariants. Builders MUST NOT emit an invalid descriptor. Validators are not required to verify these invariants. Section 6 defines calldata traversal only for valid descriptors.

  • DV-1: nodeLength of every composite node equals the actual byte span of that node, and a tuple's fieldCount field descriptors exactly fill its nodeLength.
  • DV-2: staticWords is zero if and only if the node contains a dynamic sub-value. Otherwise, staticWords equals the node's ABI head size in words (Sections 4.3–4.6).

7.3 Normative Limits

ConstantValueCategoryInvariantDerivation
MAX_NODE_LENGTH4,095 bytesFormat—12-bit nodeLength field in composite metadata (0x0FFF).
MAX_STATIC_WORDS4,095 words (~128 KB)Format—12-bit staticWords field in composite metadata.
MAX_STATIC_ARRAY_LENGTH4,095 elementsFormatDWF-6Same 12-bit field width as the other composite metadata limits.
MAX_TUPLE_FIELDS4,089 fieldsDerivedDWF-5MAX_NODE_LENGTH - TUPLE_HEADER_SIZE (4,095 - 6).
MAX_NESTING_DEPTH64 levelsDesignDWF-7Operational limit on composite nesting. Section 7.1 gives the counting rule.
MAX_PARAMS255Format—1-byte paramCount in descriptor header.

A limit without an invariant reference (—) is a bound of the encoding itself. No byte string can exceed such a limit, so a validator has nothing to check.

Limit categories:

  • Format: Structural constraint from the width of an encoding field. A change to the limit needs a new format version.
  • Derived: Follows from other limits.
  • Design: Operational limit for implementation safety. The limit is normative and does not change within this format version: all conformant implementations enforce the same value.

8. References

  • ABI Specification (Solidity documentation, applicable to all EVM languages).
  • RFC 2119 — Key words for use in RFCs to Indicate Requirement Levels.
  • Callcium reference implementation (non-normative).
  • Conformance test vector suite: spec/vectors/descriptors.json in the reference implementation repository.

Appendix A. Complete Type Code Assignments

Unsigned Integers (0x01–0x20)

ABI TypeCodeABI TypeCodeABI TypeCodeABI TypeCode
uint80x01uint720x09uint1360x11uint2000x19
uint160x02uint800x0Auint1440x12uint2080x1A
uint240x03uint880x0Buint1520x13uint2160x1B
uint320x04uint960x0Cuint1600x14uint2240x1C
uint400x05uint1040x0Duint1680x15uint2320x1D
uint480x06uint1120x0Euint1760x16uint2400x1E
uint560x07uint1200x0Fuint1840x17uint2480x1F
uint640x08uint1280x10uint1920x18uint2560x20

Signed Integers (0x21–0x40)

ABI TypeCodeABI TypeCodeABI TypeCodeABI TypeCode
int80x21int720x29int1360x31int2000x39
int160x22int800x2Aint1440x32int2080x3A
int240x23int880x2Bint1520x33int2160x3B
int320x24int960x2Cint1600x34int2240x3C
int400x25int1040x2Dint1680x35int2320x3D
int480x26int1120x2Eint1760x36int2400x3E
int560x27int1200x2Fint1840x37int2480x3F
int640x28int1280x30int1920x38int2560x40

Fixed Types (0x41–0x4F)

ABI TypeCode
address0x41
bool0x42
function0x43

function is the ABI type of an external function pointer: a 20-byte address and then a 4-byte selector. The encoding of function is identical to the encoding of bytes24.

Codes 0x44–0x4F are reserved. Validators MUST reject these codes in this format version.

Fixed Bytes (0x50–0x6F)

ABI TypeCodeABI TypeCodeABI TypeCodeABI TypeCode
bytes10x50bytes90x58bytes170x60bytes250x68
bytes20x51bytes100x59bytes180x61bytes260x69
bytes30x52bytes110x5Abytes190x62bytes270x6A
bytes40x53bytes120x5Bbytes200x63bytes280x6B
bytes50x54bytes130x5Cbytes210x64bytes290x6C
bytes60x55bytes140x5Dbytes220x65bytes300x6D
bytes70x56bytes150x5Ebytes230x66bytes310x6E
bytes80x57bytes160x5Fbytes240x67bytes320x6F

Dynamic Elementary (0x70–0x7F)

ABI TypeCode
bytes0x70
string0x71

Codes 0x72–0x7F are reserved. Validators MUST reject these codes in this format version.

Arrays (0x80–0x8F)

ABI TypeCode
static array0x80
dynamic array0x81

Codes 0x82–0x8F are reserved. Validators MUST reject these codes in this format version.

Tuples (0x90–0x9F)

ABI TypeCode
tuple0x90

Codes 0x91–0x9F are reserved. Validators MUST reject these codes in this format version.


Appendix B. Changelog

  • v2.0 (2026-08-05): Initial specification.

On this page