Signatures

Method Identity

Every method has a unique 64-bit identifier computed from its service name and method name only. The signature is deliberately excluded — schema exchange (see r[schema.method-id]) handles type evolution without changing method identity.

method.identity.computation

The method ID MUST be computed as:

method_id = blake3(kebab(ServiceName) + "." + kebab(methodName))[0..8]

Where:

  • kebab() converts to kebab-case (e.g. TemplateHosttemplate-host)
  • [0..8] takes the first 8 bytes as a u64

The signature hash (sig_bytes) is NOT included. Only the service name and method name contribute to the method ID.

This means:

  • Renaming a service or method changes the ID (breaking change)
  • Case variations normalize to the same ID (loadTemplate = load_template)
  • Changing argument or return types does NOT change the method ID — schema translation handles type evolution (see the schema exchange specification)

Signature Hash

The signature hash is no longer part of method identity (see r[method.identity.computation]), but the canonical encoding below is still used for schema extraction and compatibility tooling.

signature.hash.algorithm

The signature hash is computed by hashing a canonical byte representation of the method signature using BLAKE3.

signature.varint

Variable-length integers (varint) in signature encoding use the same format as [POSTCARD]: unsigned LEB128. Each byte contains 7 data bits; the high bit indicates continuation (1 = more bytes).

signature.endianness

All fixed-width integers in signature encoding are little-endian.

The canonical representation encodes the method signature as a tuple (see r[signature.method] below). Each type within is encoded recursively:

Primitive Types

signature.primitive

Primitive types MUST be encoded as a single byte tag:

TypeTag
bool0x01
u80x02
u160x03
u320x04
u640x05
u1280x06
i80x07
i160x08
i320x09
i640x0A
i1280x0B
f320x0C
f640x0D
char0x0E
String0x0F
() (unit)0x10
bytes0x11

Container Types

signature.container

Container types MUST be encoded as a tag byte followed by their element type(s):

TypeTagEncoding
List0x20tag + encode(element)
Option0x21tag + encode(inner)
Array0x22tag + varint(len) + encode(element)
Map0x23tag + encode(key) + encode(value)
Set0x24tag + encode(element)
Tuple0x25tag + varint(len) + encode(T1) + encode(T2) + ...
Stream0x26tag + encode(element)

Note: These are wire-format types, not Rust types. Vec, VecDeque, and LinkedList all encode as List. HashMap and BTreeMap both encode as Map.

signature.bytes.equivalence

Any "bytes" type MUST use the bytes tag (0x11) in signature encoding. This includes the dedicated bytes wire-format type and a list of u8. As a result, bytes and List<u8> MUST produce identical signature hashes.

Struct Types

signature.struct

Struct types MUST be encoded as:

0x30 + varint(field_count) + (field_name + field_type)*

Where each field_name is encoded as varint(len) + utf8_bytes. Fields MUST be encoded in declaration order.

Note: The struct's name is NOT included — only field names and types. This allows renaming types without breaking compatibility.

Enum Types

signature.enum

Enum types MUST be encoded as:

0x31 + varint(variant_count) + (variant_name + variant_payload)*

Where each variant_name is encoded as varint(len) + utf8_bytes. variant_payload is:

  • 0x00 for unit variants
  • 0x01 + encode(T) for newtype variants
  • 0x02 + struct encoding (without the 0x30 tag) for struct variants

Variants MUST be encoded in declaration order.

Recursive Types

signature.recursive

When encoding types that reference themselves (directly or indirectly), implementations MUST detect cycles and emit a back-reference instead of infinitely recursing. Cycles can occur through any chain of type references: containers, struct fields, enum variants, or combinations thereof.

signature.recursive.encoding

A back-reference MUST be encoded as the tag byte 0x32 followed by a varint(depth) indicating how many levels up the type stack the reference points to. Depth 0 means the immediately enclosing type (direct self-recursion), depth 1 means the type one level above (mutual recursion through one intermediate type), and so on.

This disambiguates mutually recursive structures. Without a depth index, types like A -> Option<B> -> Option<A> and A -> Option<A> could produce colliding encodings when their field layouts happen to align.

signature.recursive.stack

Implementations MUST maintain a stack of types currently being encoded. When a type is encountered that is already on the stack, the encoder emits 0x32 + varint(distance) where distance is the number of entries between the current position and the matching stack entry (0-indexed from the top). After encoding a type's body, it is popped from the stack.

This ensures:

  • No stack overflow during encoding
  • Deterministic output (same type always produces same bytes)
  • Finite signature size for recursive types
  • Unambiguous back-references in mutually recursive type graphs

Method Signature Encoding

signature.method

A method signature MUST be encoded as the args tuple type followed by the return type:

encode(ArgTuple) + encode(ReturnType)

Where ArgTuple is the tuple of argument types (A1, A2, ..., AN), encoded as a regular tuple (tag 0x25 + varint(N) + each element).

Since ArgTuple is a tuple, a zero-argument method uses () (unit, tag 0x10). This structure ensures unambiguous parsing — the arg count is implicit in the tuple length.

Example

For a method:

rust
async fn add(&self, a: i32, b: i32) -> i64;

The canonical bytes would be:

0x25          // Tuple tag for (i32, i32)
0x02          // 2 arguments
0x09          // a: i32
0x09          // b: i32
0x0A          // return: i64

BLAKE3 hash of these bytes gives sig_bytes.