Packages
macula
3.2.0
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
Current section
Files
src/macula_record.erl
%%%-------------------------------------------------------------------
%%% @doc Signed, content-addressed records.
%%%
%%% Records are the SDK's primitive for typed, signed, application
%%% data put into the mesh's distributed storage. They are
%%% content-addressed: the `key' is the BLAKE3 hash of the
%%% deterministic CBOR encoding of the signed body, so two clients
%%% computing the same record arrive at the same key.
%%%
%%% == Anatomy ==
%%%
%%% A record has six fields plus an optional `expires_at':
%%%
%%% <ul>
%%% <li>`type' — application-defined `0..255' tag (e.g. `16#02'
%%% for `station_record', `16#11' for `content_announcement').
%%% The SDK does not interpret payloads — type is just routing
%%% metadata for `find_records_by_type/2'.</li>
%%% <li>`payload' — opaque application bytes. Conventionally a
%%% deterministic CBOR encoding via `macula_record_cbor', but
%%% the SDK stores it verbatim.</li>
%%% <li>`pubkey' — 32-byte Ed25519 public key of the publisher.</li>
%%% <li>`ttl_ms' — record lifetime hint in milliseconds. The relay
%%% expires the record after this elapses.</li>
%%% <li>`sig' — 64-byte Ed25519 signature over the canonical body
%%% (type + payload + pubkey + ttl_ms, deterministic CBOR).</li>
%%% <li>`expires_at' — absolute epoch milliseconds, set by the
%%% relay on store. Optional in records built locally.</li>
%%% </ul>
%%%
%%% == Signing scheme ==
%%%
%%% The signed bytes are the deterministic CBOR encoding (RFC 8949
%%% §4.2.1) of:
%%%
%%% ```
%%% [type, payload, pubkey, ttl_ms]
%%% '''
%%%
%%% Verifiers reconstruct this same byte string from the record's
%%% fields and check the Ed25519 signature against `pubkey'.
%%%
%%% == Content addressing ==
%%%
%%% The `key' is `blake3(canonical_body || sig)'. Including the
%%% signature in the hash prevents two distinct signers from
%%% producing the same key for the same logical content — each
%%% signed record has a unique address.
%%%
%%% @end
%%%-------------------------------------------------------------------
-module(macula_record).
-export([build/4,
key_of/1,
canonical_body/1,
verify/1]).
-export_type([record/0, record_type/0, record_key/0]).
-type record_type() :: 0..255.
-type record_key() :: <<_:256>>. %% 32-byte BLAKE3 of (body || sig)
-type record() :: #{
type := record_type(),
payload := binary(),
pubkey := <<_:256>>,
ttl_ms := non_neg_integer(),
sig := <<_:512>>,
expires_at => non_neg_integer()
}.
%%%===================================================================
%%% Build + sign
%%%===================================================================
%% @doc Build a signed record. Computes the canonical body, signs
%% with the keypair's private half, and returns the record map. The
%% caller passes the result to `macula:put_record/2'.
-spec build(record_type(), binary(), macula_identity:key_pair(),
non_neg_integer()) -> record().
build(Type, Payload, KeyPair, TtlMs)
when is_integer(Type), Type >= 0, Type =< 255,
is_binary(Payload),
is_map(KeyPair),
is_integer(TtlMs), TtlMs >= 0 ->
Pub = macula_identity:public(KeyPair),
Body = canonical_body(Type, Payload, Pub, TtlMs),
Sig = macula_identity:sign(Body, KeyPair),
#{type => Type,
payload => Payload,
pubkey => Pub,
ttl_ms => TtlMs,
sig => Sig}.
%%%===================================================================
%%% Key derivation
%%%===================================================================
%% @doc The 32-byte content-address key for a record.
%% Computed as BLAKE3 of the canonical body concatenated with the
%% signature.
-spec key_of(record()) -> record_key().
key_of(#{type := T, payload := P, pubkey := Pub,
ttl_ms := TTL, sig := Sig}) ->
Body = canonical_body(T, P, Pub, TTL),
macula_blake3_nif:hash(<<Body/binary, Sig/binary>>).
%%%===================================================================
%%% Canonical signing body
%%%===================================================================
%% @doc Returns the canonical byte string the signature covers.
%% Exposed so callers / verifiers can inspect what was signed
%% without re-deriving the encoding rules.
-spec canonical_body(record()) -> binary().
canonical_body(#{type := T, payload := P, pubkey := Pub, ttl_ms := TTL}) ->
canonical_body(T, P, Pub, TTL).
canonical_body(Type, Payload, Pub, TtlMs) ->
macula_record_cbor:encode([Type, Payload, Pub, TtlMs]).
%%%===================================================================
%%% Verify
%%%===================================================================
%% @doc Verify a record's signature. Returns `true' if `sig' is a
%% valid Ed25519 signature over `canonical_body/1' under `pubkey'.
-spec verify(record()) -> boolean().
verify(#{pubkey := Pub, sig := Sig} = Record)
when is_binary(Pub), byte_size(Pub) =:= 32,
is_binary(Sig), byte_size(Sig) =:= 64 ->
Body = canonical_body(Record),
macula_identity:verify(Body, Sig, Pub);
verify(_) ->
false.