Search documentation of macula

# macula_source_route (macula v4.4.9)

View Source

Source-route header codec (Part 6 §11).

Wire layout (big-endian throughout):

        0       1       2       3
        +-------+-------+-------+-------+
        | ver   | total | curr  | dead-
        +-------+-------+-------+-------+
        ...    deadline (8 bytes)    ...
        +-------+-------+-------+-------+
        ...    path_hash (16 bytes)  ...
        +-------+-------+-------+-------+
        ...    hops[0..total_hops-1] (each 16 bytes — NodeId prefix)

- `version` (1 byte) — currently `1`.
- `total_hops` (1 byte) — `1..8`.
- `current_hop` (1 byte) — index of the hop currently processing. `new/2,3` creates a header with `0`; each hop advances by 1 before forwarding; the destination observes `current_hop = total_hops - 1` and after one final advance sees `is_complete/1 = true`.
- `deadline` (8 bytes, unsigned) — absolute Unix ms past which the CALL is dropped (BOLT#4 `expiry_too_soon`).
- `path_hash` (16 bytes) — first 16 bytes of `SHA-256(concat(hops))`. Computed once at the origin and checked at every hop. Tampering with the hop sequence breaks the hash.
- `hops` — `total_hops × 16` bytes; each entry is the first 16 bytes of the NodeId at that hop.

Fixed overhead is 27 bytes (`1 + 1 + 1 + 8 + 16`). A path of `N` hops occupies `27 + 16*N` bytes; maximum (`N = 8`) is 155 bytes.

Reference: plans/PLAN_MACULA_V2_PART6_PROTOCOL.md §11; plans/PLAN_MACULA_V2_PART3_DISCOVERY.md §6.6; plans/PLAN_PHASE_4_BREAKDOWN.md Session 4.2.

# Summary

## Types

decode_error/0
header/0
hop_id/0

## Functions

advance(H)

Advance to the next hop. Errors if already complete.

current_hop(_)
current_hop_id(_)

The hop currently processing this frame (i.e. the receiver expected to handle this incoming CALL hop). `error` if the header is already complete.

deadline(_)
decode(Bin)

Decode a wire-format header. Verifies the path_hash so a caller can trust the structure was not tampered in flight.

encode(_)
hops(_)
is_complete(_)

`true` iff every hop has been traversed (`current_hop == total_hops`). The final receiver advances once after delivery to mark the path complete.

is_final_hop(_)

`true` iff the receiver of this frame is the destination — no further forward needed.

new(Hops, DeadlineMs)

Build a header from a list of hops + an absolute deadline (Unix ms). Each hop may be an already-truncated 16-byte ID or a full 32-byte NodeId; full IDs are truncated to their first 16 bytes per the wire format.

new(Hops, DeadlineMs, CurrentHop)
next_hop_id(_)

The hop the current receiver should forward to next. Returns `error` if the current hop is the final one (no next hop to forward to — the call is delivered locally).

path_hash(_)
total_hops(_)
truncate_hop(_)

Reduce a 32-byte NodeId (or anything ≥16 bytes) to its first 16 bytes, matching the wire layout. 16-byte inputs are returned unchanged.

verify(_)

Recompute the path_hash and check it matches the header's claim. `decode/1` already runs this check, so callers only need `verify/1` when they synthesise headers in memory or want a defensive check after a structural mutation.

version(_)

# Types

# decode_error/0

    -type decode_error() ::
              bad_header | bad_version | bad_total_hops | bad_current_hop | path_hash_mismatch | truncated.

# header/0

    -type header() ::
              #{version := non_neg_integer(),
                total_hops := pos_integer(),
                current_hop := non_neg_integer(),
                deadline := non_neg_integer(),
                path_hash := <<_:128>>,
                hops := [hop_id(), ...]}.

# hop_id/0

    -type hop_id() :: <<_:128>>.

# Functions

# advance(H)

    -spec advance(header()) -> header().

Advance to the next hop. Errors if already complete.

# current_hop(_)

    -spec current_hop(header()) -> non_neg_integer().

# current_hop_id(_)

    -spec current_hop_id(header()) -> {ok, hop_id()} | error.

The hop currently processing this frame (i.e. the receiver expected to handle this incoming CALL hop). `error` if the header is already complete.

# deadline(_)

    -spec deadline(header()) -> non_neg_integer().

# decode(Bin)

    -spec decode(binary()) -> {ok, header()} | {error, decode_error()}.

Decode a wire-format header. Verifies the path_hash so a caller can trust the structure was not tampered in flight.

# encode(_)

    -spec encode(header()) -> binary().

# hops(_)

    -spec hops(header()) -> [hop_id(), ...].

# is_complete(_)

    -spec is_complete(header()) -> boolean().

`true` iff every hop has been traversed (`current_hop == total_hops`). The final receiver advances once after delivery to mark the path complete.

# is_final_hop(_)

    -spec is_final_hop(header()) -> boolean().

`true` iff the receiver of this frame is the destination — no further forward needed.

# new(Hops, DeadlineMs)

    -spec new([hop_id() | macula_identity:pubkey()], non_neg_integer()) -> header().

Build a header from a list of hops + an absolute deadline (Unix ms). Each hop may be an already-truncated 16-byte ID or a full 32-byte NodeId; full IDs are truncated to their first 16 bytes per the wire format.

# new(Hops, DeadlineMs, CurrentHop)

    -spec new([hop_id() | macula_identity:pubkey()], non_neg_integer(), non_neg_integer()) -> header().

# next_hop_id(_)

    -spec next_hop_id(header()) -> {ok, hop_id()} | error.

The hop the current receiver should forward to next. Returns `error` if the current hop is the final one (no next hop to forward to — the call is delivered locally).

# path_hash(_)

    -spec path_hash(header()) -> <<_:128>>.

# total_hops(_)

    -spec total_hops(header()) -> pos_integer().

# truncate_hop(_)

    -spec truncate_hop(binary()) -> hop_id().

Reduce a 32-byte NodeId (or anything ≥16 bytes) to its first 16 bytes, matching the wire layout. 16-byte inputs are returned unchanged.

# verify(_)

    -spec verify(header()) -> ok | {error, path_hash_mismatch}.

Recompute the path_hash and check it matches the header's claim. `decode/1` already runs this check, so callers only need `verify/1` when they synthesise headers in memory or want a defensive check after a structural mutation.

# version(_)

    -spec version(header()) -> non_neg_integer().