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
bytesandstring, which are dynamic. - For traversal, elementary types have an implied
staticWords: 1 for static elementary types (all exceptbytesandstring), 0 for dynamic elementary types (bytes,string). Composite types carry explicitstaticWordsin 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].
lengthis big-endian uint16.staticWordsencodeslength * elemStaticWords, or zero if the element type is dynamic. In this document,elemStaticWordsis thestaticWordsmetadata 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)].
fieldCountis big-endian uint16.- If any field is dynamic,
staticWordsis zero. Otherwise,staticWordsis the sum of the static words of all fields.
5. Type Codes
5.1 Type Code Ranges
| Range | Category | Description |
|---|---|---|
0x00 | Reserved Zero | MUST NOT be used. Validators MUST reject a descriptor that contains this code |
0x01–0x20 | Unsigned Integers | 32 variants: uint8 … uint256 |
0x21–0x40 | Signed Integers | 32 variants: int8 … int256 |
0x41–0x4F | Fixed Types | address, bool, function (external function pointer) |
0x50–0x6F | Fixed Bytes | 32 variants: bytes1 … bytes32 |
0x70–0x7F | Dynamic Elementary | bytes, string |
0x80–0x8F | Arrays | static array, dynamic array |
0x90–0x9F | Tuples | aggregate of fields |
0xA0–0xFF | Reserved | MUST 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
| Variable | Description |
|---|---|
head | Byte offset in calldata for the head slot of the current node. |
base | Start offset of a composite. Readers resolve relative ABI offsets from this offset. |
descOffset | Byte 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
headby the parameter's head contribution anddescOffsetby the parameter'snodeLength.
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):
- Read
isDynamicfrom the tuple'sstaticWordsmetadata (0 = dynamic). - Compute the tuple's data region:
- Dynamic:
tupleBase = base + calldataload(head),cursor = tupleBase. - Static:
tupleBase = base,cursor = head.
- Dynamic:
- Skip fields
0..childIndex-1. For each skipped field, advancecursorby(staticWords == 0 ? 1 : staticWords) * 32anddescOffsetby the field'snodeLength(1 for elementary types). - 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:
arrayBase = base + calldataload(head).length = calldataload(arrayBase). The reader MUST checkchildIndex < length.headsSection = arrayBase + 32(skip the length word).- If the elements are dynamic:
newHead = headsSection + (childIndex * 32),newBase = headsSection. - If the elements are static:
newHead = headsSection + (childIndex * elementStaticSize),newBase = arrayBase.
Static array:
- If the elements are dynamic:
arrayBase = base + calldataload(head),newHead = arrayBase + (childIndex * 32),newBase = arrayBase. - 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 checkoffset + 32 <= calldatasize(). For a dynamic-length read (for example, abytesorstringpayload), 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
paramCountare 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
nodeLengthis at least the node's header size. The span of each composite node (nodeLengthbytes from its type code) does not extend past the end of the descriptor. - DWF-5: Every tuple has
fieldCountin[1, MAX_TUPLE_FIELDS]. - DWF-6: Every static array has
lengthin[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
paramCountcomplete 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:
nodeLengthof every composite node equals the actual byte span of that node, and a tuple'sfieldCountfield descriptors exactly fill itsnodeLength. - DV-2:
staticWordsis zero if and only if the node contains a dynamic sub-value. Otherwise,staticWordsequals the node's ABI head size in words (Sections 4.3–4.6).
7.3 Normative Limits
| Constant | Value | Category | Invariant | Derivation |
|---|---|---|---|---|
MAX_NODE_LENGTH | 4,095 bytes | Format | — | 12-bit nodeLength field in composite metadata (0x0FFF). |
MAX_STATIC_WORDS | 4,095 words (~128 KB) | Format | — | 12-bit staticWords field in composite metadata. |
MAX_STATIC_ARRAY_LENGTH | 4,095 elements | Format | DWF-6 | Same 12-bit field width as the other composite metadata limits. |
MAX_TUPLE_FIELDS | 4,089 fields | Derived | DWF-5 | MAX_NODE_LENGTH - TUPLE_HEADER_SIZE (4,095 - 6). |
MAX_NESTING_DEPTH | 64 levels | Design | DWF-7 | Operational limit on composite nesting. Section 7.1 gives the counting rule. |
MAX_PARAMS | 255 | Format | — | 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.jsonin the reference implementation repository.
Appendix A. Complete Type Code Assignments
Unsigned Integers (0x01–0x20)
| ABI Type | Code | ABI Type | Code | ABI Type | Code | ABI Type | Code |
|---|---|---|---|---|---|---|---|
uint8 | 0x01 | uint72 | 0x09 | uint136 | 0x11 | uint200 | 0x19 |
uint16 | 0x02 | uint80 | 0x0A | uint144 | 0x12 | uint208 | 0x1A |
uint24 | 0x03 | uint88 | 0x0B | uint152 | 0x13 | uint216 | 0x1B |
uint32 | 0x04 | uint96 | 0x0C | uint160 | 0x14 | uint224 | 0x1C |
uint40 | 0x05 | uint104 | 0x0D | uint168 | 0x15 | uint232 | 0x1D |
uint48 | 0x06 | uint112 | 0x0E | uint176 | 0x16 | uint240 | 0x1E |
uint56 | 0x07 | uint120 | 0x0F | uint184 | 0x17 | uint248 | 0x1F |
uint64 | 0x08 | uint128 | 0x10 | uint192 | 0x18 | uint256 | 0x20 |
Signed Integers (0x21–0x40)
| ABI Type | Code | ABI Type | Code | ABI Type | Code | ABI Type | Code |
|---|---|---|---|---|---|---|---|
int8 | 0x21 | int72 | 0x29 | int136 | 0x31 | int200 | 0x39 |
int16 | 0x22 | int80 | 0x2A | int144 | 0x32 | int208 | 0x3A |
int24 | 0x23 | int88 | 0x2B | int152 | 0x33 | int216 | 0x3B |
int32 | 0x24 | int96 | 0x2C | int160 | 0x34 | int224 | 0x3C |
int40 | 0x25 | int104 | 0x2D | int168 | 0x35 | int232 | 0x3D |
int48 | 0x26 | int112 | 0x2E | int176 | 0x36 | int240 | 0x3E |
int56 | 0x27 | int120 | 0x2F | int184 | 0x37 | int248 | 0x3F |
int64 | 0x28 | int128 | 0x30 | int192 | 0x38 | int256 | 0x40 |
Fixed Types (0x41–0x4F)
| ABI Type | Code |
|---|---|
address | 0x41 |
bool | 0x42 |
function | 0x43 |
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 Type | Code | ABI Type | Code | ABI Type | Code | ABI Type | Code |
|---|---|---|---|---|---|---|---|
bytes1 | 0x50 | bytes9 | 0x58 | bytes17 | 0x60 | bytes25 | 0x68 |
bytes2 | 0x51 | bytes10 | 0x59 | bytes18 | 0x61 | bytes26 | 0x69 |
bytes3 | 0x52 | bytes11 | 0x5A | bytes19 | 0x62 | bytes27 | 0x6A |
bytes4 | 0x53 | bytes12 | 0x5B | bytes20 | 0x63 | bytes28 | 0x6B |
bytes5 | 0x54 | bytes13 | 0x5C | bytes21 | 0x64 | bytes29 | 0x6C |
bytes6 | 0x55 | bytes14 | 0x5D | bytes22 | 0x65 | bytes30 | 0x6D |
bytes7 | 0x56 | bytes15 | 0x5E | bytes23 | 0x66 | bytes31 | 0x6E |
bytes8 | 0x57 | bytes16 | 0x5F | bytes24 | 0x67 | bytes32 | 0x6F |
Dynamic Elementary (0x70–0x7F)
| ABI Type | Code |
|---|---|
bytes | 0x70 |
string | 0x71 |
Codes 0x72–0x7F are reserved. Validators MUST reject these codes in this format version.
Arrays (0x80–0x8F)
| ABI Type | Code |
|---|---|
static array | 0x80 |
dynamic array | 0x81 |
Codes 0x82–0x8F are reserved. Validators MUST reject these codes in this format version.
Tuples (0x90–0x9F)
| ABI Type | Code |
|---|---|
tuple | 0x90 |
Codes 0x91–0x9F are reserved. Validators MUST reject these codes in this format version.
Appendix B. Changelog
- v2.0 (2026-08-05): Initial specification.