macula_source_route (macula v3.13.0)

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.
  • hopstotal_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

Functions

Advance to the next hop. Errors if already complete.

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

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

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

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

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.

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).

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

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.

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)

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().