Packages

macula

3.10.2
7.0.0 6.0.0 5.2.2 5.2.1 5.2.0 5.1.0 5.0.0 4.8.0 4.7.1 4.7.0 4.6.0 4.5.0 4.4.10 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.1 4.3.0 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.1 4.1.0 4.0.0 3.16.0 3.15.3 3.15.2 3.15.1 3.14.0 3.13.0 3.12.1 3.12.0 3.11.1 3.11.0 3.10.3 3.10.2 3.10.1 3.9.0 3.8.0 3.7.0 3.5.0 3.4.0 3.3.0 3.2.0 3.1.0 3.0.0 2.1.1 2.1.0 2.0.0 1.5.2 1.5.1 1.4.30 1.4.29 1.4.28 1.4.27 1.4.26 1.4.25 1.4.24 1.4.23 1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.1 1.3.0 1.2.0 1.1.0 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.48.6 0.48.5 0.48.4 0.48.3 0.48.2 0.48.1 0.48.0 0.47.1 0.47.0 0.46.3 0.46.1 0.46.0 0.45.3 0.45.2 0.45.1 0.45.0 0.44.2 0.44.1 0.44.0 0.43.3 0.43.2 0.43.1 0.43.0 0.42.9 0.42.8 0.42.7 0.42.6 0.42.5 0.42.4 0.42.3 0.42.2 0.42.1 0.42.0 0.41.1 0.41.0 0.40.1 0.40.0 0.39.9 0.39.8 0.39.7 0.39.6 0.39.5 0.39.4 0.39.3 0.39.2 0.39.1 0.39.0 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.6 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.4 0.35.3 0.35.2 0.35.1 0.35.0 0.34.1 0.34.0 0.33.1 0.33.0 0.32.5 0.32.4 0.32.3 0.32.2 0.32.1 0.32.0 0.31.9 0.31.8 0.31.7 0.31.6 0.31.5 0.31.4 0.31.3 0.31.2 0.31.1 0.31.0 0.30.10 0.30.9 0.30.8 0.30.7 0.30.6 0.30.5 0.30.4 0.30.3 0.30.2 0.30.1 0.30.0 0.29.0 0.28.3 0.28.2 0.28.1 0.28.0 0.27.1 0.27.0 0.26.1 0.26.0 0.25.6 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.6 0.24.5 0.24.4 0.24.3 0.24.2 0.24.1 0.24.0 0.23.3 0.23.2 0.23.1 0.23.0 0.22.12 0.22.11 0.22.10 0.22.9 0.22.8 0.22.7 0.22.6 0.22.5 0.22.4 0.22.3 0.22.2 0.22.1 0.22.0 0.21.7 0.21.6 0.21.5 0.21.4 0.21.2 0.21.1 0.21.0 0.20.25 0.20.24 0.20.23 0.20.22 0.20.21 0.20.20 0.20.19 0.20.18 0.20.17 0.20.16 0.20.15 0.20.14 0.20.13 0.20.12 0.20.11 0.20.10 0.20.9 0.20.8 0.20.7 0.20.6 0.20.5 0.20.3 0.20.2 0.20.1 0.20.0 0.19.2 0.19.1 0.19.0 0.18.1 0.18.0 0.17.4 0.17.3 0.17.2 0.17.1 0.17.0 0.16.6 0.16.5 0.16.4 0.16.3 0.16.2 0.16.1 0.16.0 0.15.1 0.15.0 0.14.3 0.14.2 0.14.1 0.14.0 0.12.6 0.12.5 0.12.3 0.11.3 0.10.2 0.10.1 0.10.0 0.9.2 0.9.1 0.9.0 0.8.25 0.8.24 0.8.23 0.8.22 0.8.21 0.8.20 0.8.19 0.8.18 0.8.17 0.8.16 0.8.15 0.8.14 0.8.13 0.8.12 0.8.11 0.8.10 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.3 0.8.2 0.8.1 0.8.0 0.7.30 0.7.29 0.7.28 0.7.27 0.7.26 0.7.25 0.7.24 0.7.23 0.7.22 0.7.21 0.7.20 0.7.19 0.7.18 0.7.17 0.7.16 0.7.15 0.7.14 0.7.13 0.7.12 0.7.11 0.7.10 0.7.9 0.7.8 0.7.7 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.7 0.6.6 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.0 0.4.4 0.4.3 0.4.2 0.4.1 0.4.0 0.3.4 0.3.3 0.3.2 0.3.1

Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise

Current section

Files

Jump to
macula src macula_source_route.erl
Raw

src/macula_source_route.erl

%% @doc Source-route header codec (Part 6 §11).
%%
%% Wire layout (big-endian throughout):
%%
%% <pre>
%% 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)
%% </pre>
%%
%% <ul>
%% <li><code>version</code> (1 byte) — currently `1'.</li>
%% <li><code>total_hops</code> (1 byte) — `1..8'.</li>
%% <li><code>current_hop</code> (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'.</li>
%% <li><code>deadline</code> (8 bytes, unsigned) — absolute Unix ms
%% past which the CALL is dropped (BOLT#4 `expiry_too_soon').</li>
%% <li><code>path_hash</code> (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.</li>
%% <li><code>hops</code> — `total_hops × 16' bytes; each entry is
%% the first 16 bytes of the NodeId at that hop.</li>
%% </ul>
%%
%% 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.
-module(macula_source_route).
-export([
new/2, new/3,
encode/1,
decode/1,
verify/1,
advance/1,
deadline/1,
version/1,
total_hops/1,
current_hop/1,
hops/1,
path_hash/1,
current_hop_id/1,
next_hop_id/1,
is_complete/1,
is_final_hop/1,
truncate_hop/1
]).
-export_type([header/0, hop_id/0, decode_error/0]).
-define(VERSION, 1).
-define(MAX_HOPS, 8).
-define(HOP_BYTES, 16).
-define(HASH_BYTES, 16).
-define(FIXED_OVERHEAD, 27).
-type hop_id() :: <<_:128>>.
-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(), ...]
}.
-type decode_error() ::
bad_header
| bad_version
| bad_total_hops
| bad_current_hop
| path_hash_mismatch
| truncated.
%%=====================================================================
%% Construction
%%=====================================================================
%% @doc 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.
-spec new([hop_id() | macula_identity:pubkey()],
non_neg_integer()) -> header().
new(Hops, DeadlineMs) ->
new(Hops, DeadlineMs, 0).
-spec new([hop_id() | macula_identity:pubkey()],
non_neg_integer(), non_neg_integer()) -> header().
new(Hops, DeadlineMs, CurrentHop)
when is_list(Hops),
is_integer(DeadlineMs), DeadlineMs >= 0,
is_integer(CurrentHop), CurrentHop >= 0 ->
Truncated = [truncate_hop(H) || H <- Hops],
Total = length(Truncated),
valid_total(Total),
valid_current(CurrentHop, Total),
#{
version => ?VERSION,
total_hops => Total,
current_hop => CurrentHop,
deadline => DeadlineMs,
path_hash => compute_hash(Truncated),
hops => Truncated
}.
%%=====================================================================
%% Wire codec
%%=====================================================================
-spec encode(header()) -> binary().
encode(#{version := V, total_hops := T, current_hop := C,
deadline := D, path_hash := H, hops := Hops})
when is_integer(V), V >= 0, V =< 255,
is_integer(T), T >= 1, T =< ?MAX_HOPS,
is_integer(C), C >= 0, C =< T,
is_integer(D), D >= 0,
is_binary(H), byte_size(H) =:= ?HASH_BYTES,
length(Hops) =:= T ->
HopsBin = << <<Hop:?HOP_BYTES/binary>> || Hop <- Hops >>,
<<V:8, T:8, C:8, D:64/big-unsigned, H/binary, HopsBin/binary>>.
%% @doc Decode a wire-format header. Verifies the path_hash so a
%% caller can trust the structure was not tampered in flight.
-spec decode(binary()) -> {ok, header()} | {error, decode_error()}.
decode(Bin) when is_binary(Bin), byte_size(Bin) >= ?FIXED_OVERHEAD ->
<<V:8, T:8, C:8, D:64/big-unsigned, H:?HASH_BYTES/binary,
Rest/binary>> = Bin,
parse_decoded(V, T, C, D, H, Rest);
decode(_) ->
{error, truncated}.
-spec parse_decoded(byte(), byte(), byte(), non_neg_integer(),
binary(), binary()) ->
{ok, header()} | {error, decode_error()}.
parse_decoded(V, _T, _C, _D, _H, _Rest) when V =/= ?VERSION ->
{error, bad_version};
parse_decoded(_V, T, _C, _D, _H, _Rest) when T < 1; T > ?MAX_HOPS ->
{error, bad_total_hops};
parse_decoded(_V, T, C, _D, _H, _Rest) when C > T ->
{error, bad_current_hop};
parse_decoded(V, T, C, D, H, Rest) ->
parse_hops(V, T, C, D, H, Rest).
-spec parse_hops(byte(), byte(), byte(), non_neg_integer(),
binary(), binary()) ->
{ok, header()} | {error, decode_error()}.
parse_hops(V, T, C, D, H, Rest) when byte_size(Rest) >= T * ?HOP_BYTES ->
HopsByteCount = T * ?HOP_BYTES,
<<HopsBin:HopsByteCount/binary, _Tail/binary>> = Rest,
Hops = [Hop || <<Hop:?HOP_BYTES/binary>> <= HopsBin],
verify_hash(compute_hash(Hops) =:= H, V, T, C, D, H, Hops);
parse_hops(_V, _T, _C, _D, _H, _Rest) ->
{error, truncated}.
-spec verify_hash(boolean(), byte(), byte(), byte(),
non_neg_integer(), binary(), [hop_id()]) ->
{ok, header()} | {error, decode_error()}.
verify_hash(false, _V, _T, _C, _D, _H, _Hops) ->
{error, path_hash_mismatch};
verify_hash(true, V, T, C, D, H, Hops) ->
{ok, #{version => V,
total_hops => T,
current_hop => C,
deadline => D,
path_hash => H,
hops => Hops}}.
%%=====================================================================
%% Verification + position management
%%=====================================================================
%% @doc 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.
-spec verify(header()) -> ok | {error, path_hash_mismatch}.
verify(#{path_hash := H, hops := Hops}) ->
verify_compare(compute_hash(Hops) =:= H).
verify_compare(true) -> ok;
verify_compare(false) -> {error, path_hash_mismatch}.
%% @doc Advance to the next hop. Errors if already complete.
-spec advance(header()) -> header().
advance(#{current_hop := C, total_hops := T} = H) when C < T ->
H#{current_hop := C + 1};
advance(_) ->
error(path_already_complete).
%%=====================================================================
%% Accessors
%%=====================================================================
-spec version(header()) -> non_neg_integer().
version(#{version := V}) -> V.
-spec total_hops(header()) -> pos_integer().
total_hops(#{total_hops := T}) -> T.
-spec current_hop(header()) -> non_neg_integer().
current_hop(#{current_hop := C}) -> C.
-spec deadline(header()) -> non_neg_integer().
deadline(#{deadline := D}) -> D.
-spec path_hash(header()) -> <<_:128>>.
path_hash(#{path_hash := H}) -> H.
-spec hops(header()) -> [hop_id(), ...].
hops(#{hops := Hops}) -> Hops.
%% @doc The hop currently processing this frame (i.e. the receiver
%% expected to handle this incoming CALL hop). `error' if the
%% header is already complete.
-spec current_hop_id(header()) -> {ok, hop_id()} | error.
current_hop_id(#{current_hop := C, total_hops := T}) when C >= T ->
error;
current_hop_id(#{current_hop := C, hops := Hops}) ->
{ok, lists:nth(C + 1, Hops)}.
%% @doc 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).
-spec next_hop_id(header()) -> {ok, hop_id()} | error.
next_hop_id(#{current_hop := C, total_hops := T}) when C + 1 >= T ->
error;
next_hop_id(#{current_hop := C, hops := Hops}) ->
{ok, lists:nth(C + 2, Hops)}.
%% @doc `true' iff every hop has been traversed (`current_hop ==
%% total_hops'). The final receiver advances once after delivery
%% to mark the path complete.
-spec is_complete(header()) -> boolean().
is_complete(#{current_hop := C, total_hops := T}) -> C >= T.
%% @doc `true' iff the receiver of this frame is the destination —
%% no further forward needed.
-spec is_final_hop(header()) -> boolean().
is_final_hop(#{current_hop := C, total_hops := T}) -> C + 1 =:= T.
%%=====================================================================
%% Hop-ID truncation
%%=====================================================================
%% @doc Reduce a 32-byte NodeId (or anything ≥16 bytes) to its
%% first 16 bytes, matching the wire layout. 16-byte inputs are
%% returned unchanged.
-spec truncate_hop(binary()) -> hop_id().
truncate_hop(<<H:?HOP_BYTES/binary>>) ->
H;
truncate_hop(<<H:?HOP_BYTES/binary, _Tail/binary>>) ->
H.
%%=====================================================================
%% Internals
%%=====================================================================
-spec valid_total(integer()) -> ok.
valid_total(N) when N >= 1, N =< ?MAX_HOPS -> ok.
-spec valid_current(integer(), integer()) -> ok.
valid_current(C, T) when C >= 0, C =< T -> ok.
-spec compute_hash([binary()]) -> <<_:128>>.
compute_hash(Hops) ->
Concat = iolist_to_binary(Hops),
<<Trunc:?HASH_BYTES/binary, _/binary>> = crypto:hash(sha256, Concat),
Trunc.