macula_frame (macula v11.4.0)
View SourceCBOR-encoded wire frames for Macula V2 (Part 6 §3 canonical wire).
A wire frame is a length-prefixed deterministic CBOR map:
<<Length:32/big, Cbor/binary>>
where Cbor is the RFC 8949 §4.2.1 deterministic encoding of a single map. The map carries the common header fields (Part 6 §3) plus type-specific fields and an Ed25519 signature.
PLAN_WIRE_CBOR.md migrated this codec from BERT to CBOR so hecate-station and the macula 3.x SDK share a wire format. Frame schemas (atom-keyed maps in process memory) are unchanged. Atoms go out as text; on decode a frame type's own fields come back through a fixed table, and peer-supplied maps keep the one key form (D26).
Phase 1 covers CONNECT / HELLO / GOODBYE. Phase 2 adds SWIM. Phase 3 (Session 3.4) adds the DHT operation frames from Part 6 §7: PING / PONG, FIND_NODE / NODES, FIND_VALUE / VALUE and STORE / STORE_ACK. CALL and STREAM_OPEN carry a request signed by the caller, RESULT and ERROR a reply signed by the provider, and ERROR and STREAM_ERROR from a station a relay error signed by that station (D25). PUBLISH frames land later.
Signatures are Ed25519 over "macula-v2-frame\0" ++ canonical_cbor(unsigned) where canonical_cbor is macula_cbor_nif:pack_deterministic/1 (RFC 8949 §4.2.1 deterministic — same rules macula_record_cbor:encode/1 implements in pure Erlang, kept as the differentially-tested reference; see test/macula_cbor_deterministic_diff_tests.erl).
Summary
Functions
Check the addresses of a NODES entry: at most 4, each exactly a host, a port from 1 to 65535 and the quic transport. A host is 1 to 253 bytes: an IP literal without a zone, or a host name of labels of 1 to 63 letters, digits and hyphens that neither start nor end with a hyphen, with no trailing dot. The builder and the receive rule use this function, and a station can run it on the addresses it stores before it lists them.
Sign a CALL with the caller's identity key.
Sign a caller's stream frame for a verified STREAM_OPEN with the caller's identity key. A caller sends no STREAM_REPLY, and no STREAM_DATA in a server_stream.
Whether a refusal of a signed object is charged to the connection that carried it (DESIGN_PQ_DHT_SLOTS_AND_BUDGET.md, 3.1). A refusal every verifier reaches from the same bytes is charged: a malformed shape, key, field or alg, a signature that does not verify, or a signer that is not its key. One that depends on what the receiver holds is not: a sequence number, a stream that has ended, a match with the request or its target, or a subscriber on this node for the frame's realm. A neighbour's HyParView placements past its allowance, and a SHUFFLE_REPLY that answers no SHUFFLE, are charged. A freshness refusal is charged only when it is more than 10 minutes past the moment its rule starts refusing. A Plumtree neighbour's IHAVE past its open entries, a GRAFT it leaves unanswered, and a publication for another realm are charged. A Plumtree frame from a sender outside the receiver's push sets is not: through a relay that sender is the frame's origin, not the connection peer, and it may be no neighbour during view churn through no fault of the relay.
Is this whole frame sendable? Used by macula_peering:send_frame/2, which is the single seam every producer passes through. Records travel as their wire bytes, so a record-bearing frame is judged like any other.
Is this term admissible as a frame payload?
The fields a PUBLISH, EVENT or GOSSIP's publication names, read without verifying its signature: the same frame-field and signed-object shape checks, the same strict tbs decode and the same field table verify_publication/3 applies, so the fields come back in the verified shape or not at all. For a lenient subscriber that wants the fact even when its signature does not check out. Anything else is no_publication.
The realm a GOSSIP's publication names, read without verifying it. A GOSSIP names its realm only inside its signed publication, so this is the key a station link routes one to its realm's overlay subscribers by, and nothing more: verify_publication/3 decides whether the publication is genuine and for that realm. The frame's fields and the signed object's shape are checked as verify_publication/3 checks them, and the tbs is read with the same strict decoding, within the decoder's element budget, and the same field table, so a realm of another length never comes back. Anything else is no_realm: a tbs that does not decode or names no realm, and any frame that is not a GOSSIP.
The request_id and request_hash a received reply names, read without verifying it: a RESULT or ERROR carrying reply, or an ERROR or STREAM_ERROR carrying relay_error. The ids are a key for finding the pending request, and nothing more: verify_reply/3 or verify_relay_error/4 against that request decides whether the frame answers it. The frame's fields and the signed object's shape are checked as the verifiers check them, and the tbs is read with the same strict decoding and field table, so ids of another length or shape never come back. It checks neither the signed object's key nor that key's size for the profile: nothing it returns is trusted, and the verifiers check both. Anything else is malformed_frame.
Whether a frame type belongs on a connection's control stream, in either profile: the frames pq_hybrid neighbour-signs (D17). One arriving on a dedicated stream is malformed_frame, and the connection closes.
Decode a single length-prefixed frame received from a peer, from the head of a buffer, and check its shape with validate_received/1. Returns {ok, Frame, RestBuffer}, {more, BytesNeeded} if the buffer is short, or {error, Reason}: frame_too_large or bad_frame if the framing is malformed, bad_frame as well for a frame without a frame_type, of a type this node does not know, or with a field its type's table cannot read, and {invalid_frame, Type, Field} if the frame decodes but validate_received/1 refuses its shape. There is no decode without that check: every frame decode/1 returns has passed validate_received/1. A frame with more CBOR items than the decoder's element budget is refused as too_many_elements.
Prefix frame CBOR bytes with their length, leaving the bytes as they are. The handshake keeps the bytes it passes here, because the connection proof hashes a frame's bytes without the prefix.
An EVENT for a verified publication, carrying its bytes unchanged and how it was delivered.
Render a rejection as a sentence, with the remedy where there is one. The operator reading a log at 03:00 is not reading edoc.
Whether a profile neighbour-signs a frame type.
The state a verifier starts a stream with: nothing seen from either side yet.
Parse bytes a relay received on a stream, for it to pass on. Each whole frame that passes the checks of parse_received/2 comes with a unit holding a copy of exactly the bytes received for it, its length header included, so a unit keeps nothing else of the buffer alive. Only such a unit is written, by macula_peering:relay_on_stream/2 and async_relay_on_stream/2,3, through relayed_bytes/1. A frame whose fields its type refuses comes back as {refused, {invalid_frame, Type, Field}}, in its place and with no unit. A length header above MaxFrameBytes, or a whole frame that does not decode, ends the parse as in parse_received/2, and nothing after it yields a unit.
Drain all complete frames a peer sent from a buffer.
parse_received/1 with a frame cap of MaxFrameBytes, which may not exceed the 16 MiB frame cap. A length header above MaxFrameBytes ends the parse as frame_too_large from its four bytes, so a caller that keeps Tail holds at most MaxFrameBytes plus the 4-byte header. A connection's handshake reads with a small cap.
Drain the complete frames in a buffer, in the {Frames, Tail} shape of 10.x. Deprecated, and removed in 11.0.0: use parse_received/1, which reports invalid frames and bytes that do not decode.
Drain every complete frame from a buffer as its CBOR bytes, exactly as received and without the length prefix, and return the incomplete rest. The handshake reads frames this way, because the connection proof hashes the challenge bytes as received. A length over the 16 MiB frame cap is refused.
parse_stream_bytes/1 with a frame cap of MaxFrameBytes, at most the 16 MiB frame cap. A length header above it is refused from its four bytes, before the frame's bytes are buffered, so a caller that keeps the rest holds at most MaxFrameBytes plus the header and one read. The handshake reads with a 64 KiB cap.
Sign a provider's ERROR for a verified request: a code of at most 64 bytes and an optional detail of at most 256, both UTF-8. Other text raises a badmatch on {error, {text_too_long, Field}} or {error, {invalid_text, Field}}.
Sign a provider's stream frame for a verified STREAM_OPEN with the provider's identity key. The first frame, seq 0, carries the key; later frames do not.
Sign a publication with the publisher's identity key, as a PUBLISH. Its tbs holds no frame_type, because the same bytes ride in every EVENT and GOSSIP made from it.
Read a frame from its decoded CBOR value, the way decode/1 reads one after decoding its bytes. A connection decodes each frame once to route it by its frame_type, and passes the value here. A frame whose fields its type refuses is bad_frame here too.
charged_refusal/1 over any term, for a caller that must not raise on a kind it was handed: charged or uncharged for a kind charged_refusal/1 classifies, and unclassified for any other term.
Sign a station's relay error, an ERROR or STREAM_ERROR for a pending request, with the station's identity key. It carries a code from the closed set and no free text, so a spec with a detail raises function_clause.
The bytes a relay received for a frame parse_for_relay/2 accepted, to write. Anything else is refused with function_clause, in the caller.
Whether a relayed frame of this type is taken without a frame signature: every HyParView frame, JOIN, FORWARD_JOIN, NEIGHBOR, DISCONNECT, SHUFFLE and SHUFFLE_REPLY, and the Plumtree IHAVE, GRAFT and PRUNE, which D17 leaves unsigned in pq_pure, and the Plumtree GOSSIP, whose publication is signed end to end. A station relays them from a connection it authenticated, so a receiver takes the relay's origin as their sender. A GOSSIP's publication stays unverified until verify_publication/3 accepts it, and its subscriber verifies it before acting on it. Every other relayed type keeps its own verification.
Sign a RESULT for a verified request with the provider's identity key.
Neighbour-sign a control frame with the sender's identity key, for one connection and one seq.
The bytes of a frame built here for a dedicated stream: the one step every such frame passes through before it is written. Build names the frame (see stream_build()); it is signed with the identity key Key, under that key's profile, and encoded. A build that its receiver would refuse, or that the wire cannot carry, returns an error and leaves nothing to write. These checks run in order, before anything is signed
Sign a STREAM_OPEN, which carries its stream mode, with the caller's identity key.
Check text a frame builder bounds, before anything is built: a topic or a procedure of at most 512 bytes, a code of at most 64, and a detail or a message of at most 256, each valid UTF-8. Returns the builder's own refusal, so a caller refuses local text by name instead of letting a builder raise.
Does a frame decoded from a peer's bytes have the shape its type requires, the post-quantum shape its builder writes (D26)? Each required field must be there with a value its builder accepts: a 16-byte id, a 32-byte key, a 48-byte hash, a binary, an integer in range, an atom of a closed set, a signed object, or a list of well-formed entries. A type that carries one of several signed objects holds exactly one of them. An optional field may be missing or undefined, a CBOR null. A field the type does not have refuses the frame, and so do a version other than the protocol's and a FORWARD_JOIN whose prwl is above its arwl, the one rule between fields. A control frame signed for its neighbour holds version, frame_type and neighbour and nothing else, and the frame verify_neighbour/2 opens from it is checked the same way. A handler of a received frame then never meets a missing key or a value it cannot match. The rules sit beside the field table and macula_frame_received_tests holds the two to each other.
Verify a caller's stream frame against the stream's state with the STREAM_OPEN's key, and return its fields and the next state. The signer must be the STREAM_OPEN's caller, and in a server_stream a caller sends no STREAM_DATA.
Read a received frame under the connection's profile. A frame type the profile signs must be exactly {version, frame_type, neighbour}, signed by the peer's identity key for this connection and the next seq, and comes back as the frame its tbs holds. Any other frame must not carry neighbour and comes back as it is.
Verify a provider's stream frame against the stream's state, and return its fields and the next state. Before the provider's first frame the verifier holds no provider key, so a frame without one is out of order. The first frame's key must be the key id of its signer and the STREAM_OPEN's target; later frames verify with that key. Each side's seq is the previous one plus one, and nothing follows its STREAM_END.
Verify the publication a PUBLISH, EVENT or GOSSIP carries, under the connection's profile and a clock in milliseconds: its signature and fields, a ttl_ms of at most one hour, publisher as the key id of its key, a published_at no more than 5 minutes ahead, and not past published_at plus its ttl_ms, or 10 minutes without one, plus 5 minutes. The origin station checks this before fan-out, and every subscriber before delivery. A refusal for its time says how far outside its window the publication is, in milliseconds: {not_yet_valid, AheadMs} past the 5 minutes ahead it may be, and {expired, PastMs} past its expiry.
Verify a received relay error for the pending request it names, from the station the connection authenticated: its signature and fields, reported_by as the key id of its key, the request's request_id and request_hash, and reported_by as ExpectedReporter, that station's node_id. A relay error another station reports is refused as not_the_connection.
Verify a received RESULT or provider ERROR for the request it answers: its signature and fields, responded_by as the key id of its key, the request's request_id and request_hash, and responded_by as the request's target.
Verify a received CALL or STREAM_OPEN under the connection's profile: the request's signature and fields, and caller as the key id of its key. A station checks this before it routes, a provider before its own checks.
The bytes stream_bytes/2 built, to write. Anything else is refused with function_clause, in the caller.
Types
-type address() :: #{host := binary(), port := 1..65535, transport := quic}.
-type advertise_spec() :: #{advertisement := binary()}.
-type call_id() :: <<_:128>>.
-type cancel_spec() :: #{blocks := [mcid()]}.
-type connect_spec() :: #{node_id := macula_identity:pubkey(), station_id := macula_identity:pubkey(), realms := [macula_identity:pubkey()], capabilities := non_neg_integer(), puzzle_evidence := <<_:256>>, addresses => [map()], site => map() | undefined, endorsements => [map()]}.
-type country() :: <<_:16>>.
-type delivery_channel() :: plumtree | direct.
-type event_spec() :: #{publication := macula_signed_object:object(), delivered_via := delivery_channel()}.
-type find_node_spec() :: #{key := id256(), origin := id256(), depth := non_neg_integer()}.
-type frame() :: map().
-type frame_type() ::
connect | hello | goodbye | swim_ping | swim_ack | swim_suspect | swim_confirm | ping | pong |
find_node | nodes | find_value | value | store | store_ack | call | result | error |
hyparview_join | hyparview_forward_join | hyparview_neighbor | hyparview_disconnect |
hyparview_shuffle | hyparview_shuffle_reply | plumtree_gossip | plumtree_ihave |
plumtree_graft | plumtree_prune | overlay_relay | publish | subscribe | unsubscribe | event |
advertise | unadvertise | stream_open | stream_data | stream_end | stream_error |
stream_reply | want | have | block | manifest_req | manifest_res | cancel.
-type have_entry() :: #{mcid := mcid(), size := non_neg_integer()}.
-type have_spec() :: #{blocks := [have_entry()]}.
-type hello_spec() :: #{node_id := macula_identity:pubkey(), station_id := macula_identity:pubkey(), realms := [macula_identity:pubkey()], capabilities := non_neg_integer(), accepted := boolean(), negotiated_capabilities := non_neg_integer(), addresses => [map()], site => map() | undefined, refusal_code => non_neg_integer() | undefined}.
-type hyparview_disconnect_spec() :: #{realm := id256()}.
-type hyparview_forward_join_spec() :: #{realm := id256(), new_member := id256(), ttl := non_neg_integer(), arwl := non_neg_integer(), prwl := non_neg_integer(), record => binary()}.
-type hyparview_neighbor_spec() :: #{realm := id256(), priority := neighbor_priority(), record => binary()}.
-type hyparview_shuffle_spec() :: #{realm := id256(), origin := id256(), ttl := non_neg_integer(), peer_sample := [id256()]}.
-type id256() :: <<_:256>>.
-type item() :: frame() | {invalid_frame, frame_type() | unknown, atom()}.
-type manifest_req_spec() :: #{mcid := mcid()}.
-type mcid() :: <<_:400>>.
-type member_state() :: alive | suspect | confirmed_failed.
-type msg_id() :: <<_:384>>.
-type neighbor_priority() :: high | low.
-type nodes_spec() :: #{key := id256(), nodes := [station_ref()]}.
-type nonce128() :: <<_:128>>.
-type ping_spec() :: #{nonce := nonce128()}.
-type plumtree_gossip_spec() :: #{publication := macula_signed_object:object(), round := non_neg_integer()}.
-type plumtree_graft_spec() :: #{realm := id256(), msg_id := msg_id(), round := non_neg_integer()}.
-type plumtree_ihave_spec() :: #{realm := id256(), msg_id := msg_id(), round := non_neg_integer()}.
-type plumtree_prune_spec() :: #{realm := id256()}.
-type pong_spec() :: #{nonce := nonce128()}.
-type publish_spec() :: #{realm := id256(), topic := binary(), seq := non_neg_integer(), published_at := non_neg_integer(), payload := term(), ttl_ms => 0..3600000}.
-opaque received_frame()
-type relay_item() :: {frame(), received_frame()} | {refused, {invalid_frame, frame_type() | unknown, atom()}}.
-type request_spec() :: #{request_id := <<_:128>>, realm := id256(), procedure := binary(), target := id256(), deadline := non_neg_integer(), payload := term(), mode => stream_mode(), token => binary(), source_route => binary(), retry_budget => non_neg_integer()}.
-type station_ref() :: #{node_id := id256(), station_id := id256(), addresses := [address()], tier := tier(), asn := non_neg_integer() | undefined, country := country(), last_seen_at := pos_integer()}.
-type station_ref_spec() :: #{node_id := id256(), station_id := id256(), addresses => [address()], tier := tier(), asn => non_neg_integer() | undefined, country := country(), last_seen_at := pos_integer()}.
-type store_spec() :: #{record := binary()}.
-type stream_build() :: {call | stream_open, request_spec()} | {result, #{request := verified_request(), payload := term(), source_route_reverse => binary()}} | {provider_error, #{request := verified_request(), code := binary(), detail => binary(), source_route_reverse => binary()}} | {relay_error, #{frame_type := error | stream_error, request := verified_request(), code := unknown_next_peer, offending_hop => <<_:256>>, source_route_partial => binary()}} | {provider_stream | caller_stream, stream_spec(), verified_request() | undefined}.
-opaque stream_bytes()
-type stream_encoding() :: raw | msgpack.
-type stream_id() :: <<_:128>>.
-type stream_mode() :: server_stream | client_stream | bidi.
-type stream_role() :: send | both.
-type stream_spec() :: #{frame_type := stream_data | stream_end | stream_error | stream_reply, seq := non_neg_integer(), encoding => stream_encoding(), body => term(), role => stream_role(), code => binary(), message => binary(), payload => term()}.
-type stream_state() :: #{request := verified_request(), provider := #{next := non_neg_integer(), ended := boolean(), key => binary(), signer => id256()}, caller := #{next := non_neg_integer(), ended := boolean()}}.
-type swim_ack_spec() :: #{round := non_neg_integer(), responder := id256(), incarnation := non_neg_integer(), piggyback => [swim_update()]}.
-type swim_ping_spec() :: #{round := non_neg_integer(), incarnation := non_neg_integer(), piggyback => [swim_update()]}.
-type swim_suspect_spec() :: #{target := id256(), target_incarnation := non_neg_integer(), suspected_by := id256(), ttl := non_neg_integer()}.
-type swim_update() :: #{target := id256(), state := member_state(), incarnation := non_neg_integer(), observed_at := pos_integer(), by := id256()}.
-type swim_update_spec() :: #{target := id256(), state := member_state(), incarnation := non_neg_integer(), observed_at := pos_integer(), by := id256()}.
-type tier() :: 0..4.
-type unadvertise_spec() :: #{withdrawal := binary()}.
-type verified_publication() :: #{publisher := id256(), realm := id256(), topic := binary(), seq := non_neg_integer(), published_at := non_neg_integer(), ttl_ms => 0..3600000, payload := term(), key := binary(), publication_hash := msg_id(), expires_at := non_neg_integer()}.
-type verified_request() :: #{frame_type := call | stream_open, key := binary(), request_hash := <<_:384>>, caller := id256(), request_id := <<_:128>>, realm := id256(), procedure := binary(), target := id256(), deadline := non_neg_integer(), payload := term(), mode => stream_mode(), token => binary()}.
-type want_entry() :: #{mcid := mcid(), priority => want_priority()}.
-type want_priority() :: 0..255.
-type want_spec() :: #{blocks := [want_entry()]}.
Functions
-spec addresses_checked(term()) -> ok | {error, invalid_addresses}.
Check the addresses of a NODES entry: at most 4, each exactly a host, a port from 1 to 65535 and the quic transport. A host is 1 to 253 bytes: an IP literal without a zone, or a host name of labels of 1 to 63 letters, digits and hyphens that neither start nor end with a hyphen, with no trailing dot. The builder and the receive rule use this function, and a station can run it on the addresses it stores before it lists them.
-spec advertise(advertise_spec()) -> frame().
-spec block(block_spec()) -> frame().
-spec call(request_spec(), macula_node_keys:node_key()) -> frame().
Sign a CALL with the caller's identity key.
-spec caller_stream(stream_spec(), macula_node_keys:node_key(), verified_request()) -> frame().
Sign a caller's stream frame for a verified STREAM_OPEN with the caller's identity key. A caller sends no STREAM_REPLY, and no STREAM_DATA in a server_stream.
-spec cancel(cancel_spec()) -> frame().
-spec charged_refusal(malformed_frame | signature_invalid | key_id_mismatch | seq_mismatch | stream_ended | request_mismatch | not_the_target | placement_allowance | unsolicited_shuffle_reply | {expired, pos_integer()} | {not_yet_valid, pos_integer()} | ihave_allowance | graft_unanswered | wrong_realm | not_a_peer | no_subscriber) -> boolean().
Whether a refusal of a signed object is charged to the connection that carried it (DESIGN_PQ_DHT_SLOTS_AND_BUDGET.md, 3.1). A refusal every verifier reaches from the same bytes is charged: a malformed shape, key, field or alg, a signature that does not verify, or a signer that is not its key. One that depends on what the receiver holds is not: a sequence number, a stream that has ended, a match with the request or its target, or a subscriber on this node for the frame's realm. A neighbour's HyParView placements past its allowance, and a SHUFFLE_REPLY that answers no SHUFFLE, are charged. A freshness refusal is charged only when it is more than 10 minutes past the moment its rule starts refusing. A Plumtree neighbour's IHAVE past its open entries, a GRAFT it leaves unanswered, and a publication for another realm are charged. A Plumtree frame from a sender outside the receiver's push sets is not: through a relay that sender is the frame's origin, not the connection peer, and it may be no neighbour during view churn through no fault of the relay.
Is this whole frame sendable? Used by macula_peering:send_frame/2, which is the single seam every producer passes through. Records travel as their wire bytes, so a record-bearing frame is judged like any other.
Is this term admissible as a frame payload?
Returns ok, or {error, {unsupported_payload_type, Type, Path}} where Path locates the offending value inside the term (map keys and zero-based list indices, outermost first).
WHY THIS EXISTS. macula_peering:send_frame/2 is a cast, so the frame is encoded later, in the shared peering connection process, with no try/catch around it. A term the codec cannot represent therefore does not fail the publisher — it kills the connection, taking every other producer's in-flight traffic with it, while the publisher was told ok. Checking here, in the caller's process before the cast, is what makes that ok mean something.
This function must agree exactly with to_wire/1 followed by macula_cbor_nif:pack_deterministic/1. That is why it lives beside them rather than in a validation module: the two cannot drift apart without the agreement property test in macula_frame_tests going red.
FLOATS ARE CARRIED, as of the float support in macula_record_cbor. They used to be rejected here, and before that to_wire/1 silently rewrote them as six-decimal text. Both were workarounds for the canonical encoder lacking a float clause, which was never a CBOR limitation: RFC 8949 major type 7 is floats. The encoder now emits binary64, so callers no longer scale to integers to get a number across.
The fields a PUBLISH, EVENT or GOSSIP's publication names, read without verifying its signature: the same frame-field and signed-object shape checks, the same strict tbs decode and the same field table verify_publication/3 applies, so the fields come back in the verified shape or not at all. For a lenient subscriber that wants the fact even when its signature does not check out. Anything else is no_publication.
-spec claimed_publication_realm(frame()) -> {ok, <<_:256>>} | {error, no_realm}.
The realm a GOSSIP's publication names, read without verifying it. A GOSSIP names its realm only inside its signed publication, so this is the key a station link routes one to its realm's overlay subscribers by, and nothing more: verify_publication/3 decides whether the publication is genuine and for that realm. The frame's fields and the signed object's shape are checked as verify_publication/3 checks them, and the tbs is read with the same strict decoding, within the decoder's element budget, and the same field table, so a realm of another length never comes back. Anything else is no_realm: a tbs that does not decode or names no realm, and any frame that is not a GOSSIP.
-spec claimed_reply_ids(frame()) -> {ok, #{request_id := <<_:128>>, request_hash := <<_:384>>}} | {error, malformed_frame}.
The request_id and request_hash a received reply names, read without verifying it: a RESULT or ERROR carrying reply, or an ERROR or STREAM_ERROR carrying relay_error. The ids are a key for finding the pending request, and nothing more: verify_reply/3 or verify_relay_error/4 against that request decides whether the frame answers it. The frame's fields and the signed object's shape are checked as the verifiers check them, and the tbs is read with the same strict decoding and field table, so ids of another length or shape never come back. It checks neither the signed object's key nor that key's size for the profile: nothing it returns is trusted, and the verifiers check both. Anything else is malformed_frame.
-spec connect(connect_spec()) -> frame().
-spec control_frame(frame_type()) -> boolean().
Whether a frame type belongs on a connection's control stream, in either profile: the frames pq_hybrid neighbour-signs (D17). One arriving on a dedicated stream is malformed_frame, and the connection closes.
-spec decode(binary()) -> {ok, frame(), binary()} | {more, pos_integer()} | {error, frame_too_large | bad_frame | too_many_elements | {invalid_frame, frame_type() | unknown, atom()}}.
Decode a single length-prefixed frame received from a peer, from the head of a buffer, and check its shape with validate_received/1. Returns {ok, Frame, RestBuffer}, {more, BytesNeeded} if the buffer is short, or {error, Reason}: frame_too_large or bad_frame if the framing is malformed, bad_frame as well for a frame without a frame_type, of a type this node does not know, or with a field its type's table cannot read, and {invalid_frame, Type, Field} if the frame decodes but validate_received/1 refuses its shape. There is no decode without that check: every frame decode/1 returns has passed validate_received/1. A frame with more CBOR items than the decoder's element budget is refused as too_many_elements.
Prefix frame CBOR bytes with their length, leaving the bytes as they are. The handshake keeps the bytes it passes here, because the connection proof hashes a frame's bytes without the prefix.
-spec event(event_spec()) -> frame().
An EVENT for a verified publication, carrying its bytes unchanged and how it was delivered.
-spec explain(term()) -> unicode:chardata().
Render a rejection as a sentence, with the remedy where there is one. The operator reading a log at 03:00 is not reading edoc.
-spec find_node(find_node_spec()) -> frame().
-spec find_value(find_value_spec()) -> frame().
-spec goodbye(atom(), binary() | undefined, non_neg_integer()) -> frame().
-spec hello(hello_spec()) -> frame().
-spec hyparview_disconnect(hyparview_disconnect_spec()) -> frame().
-spec hyparview_forward_join(hyparview_forward_join_spec()) -> frame().
-spec hyparview_join(hyparview_join_spec()) -> frame().
-spec hyparview_neighbor(hyparview_neighbor_spec()) -> frame().
-spec hyparview_shuffle(hyparview_shuffle_spec()) -> frame().
-spec hyparview_shuffle_reply(hyparview_shuffle_reply_spec()) -> frame().
-spec manifest_req(manifest_req_spec()) -> frame().
-spec manifest_res(manifest_res_spec()) -> frame().
-spec neighbour_signed(macula_crypto_profile:profile(), frame_type()) -> boolean().
Whether a profile neighbour-signs a frame type.
-spec nodes(nodes_spec()) -> frame().
-spec open_stream(verified_request()) -> stream_state().
The state a verifier starts a stream with: nothing seen from either side yet.
-spec overlay_relay(overlay_relay_spec()) -> frame().
-spec parse_for_relay(binary(), pos_integer()) -> {ok, [relay_item()], binary()} | {malformed, [relay_item()], frame_too_large | bad_frame | too_many_elements}.
Parse bytes a relay received on a stream, for it to pass on. Each whole frame that passes the checks of parse_received/2 comes with a unit holding a copy of exactly the bytes received for it, its length header included, so a unit keeps nothing else of the buffer alive. Only such a unit is written, by macula_peering:relay_on_stream/2 and async_relay_on_stream/2,3, through relayed_bytes/1. A frame whose fields its type refuses comes back as {refused, {invalid_frame, Type, Field}}, in its place and with no unit. A length header above MaxFrameBytes, or a whole frame that does not decode, ends the parse as in parse_received/2, and nothing after it yields a unit.
-spec parse_received(binary()) -> {ok, [item()], binary()} | {malformed, [item()], frame_too_large | bad_frame | too_many_elements}.
Drain all complete frames a peer sent from a buffer.
Returns {ok, Items, Tail} with an item per complete frame, in order, and Tail holding at most one incomplete frame, so a caller that keeps Tail for the next chunk never holds more than the frame cap plus its 4-byte header. An item is the frame, or {invalid_frame, Type, Field} for a frame that decodes but whose fields validate_received/1 refuses. Its bytes were read in full, so parsing goes on after it, and each caller decides what an invalid frame means on its stream. The first frame that does not decode ends the parse with {malformed, ItemsBefore, Reason}: frame_too_large for a length header above the cap, decided from the header alone, bad_frame for a complete frame that is not CBOR, and too_many_elements for one with more CBOR items than the decoder's element budget. Nothing after that frame can be read, so the caller ends the stream.
-spec parse_received(binary(), pos_integer()) -> {ok, [item()], binary()} | {malformed, [item()], frame_too_large | bad_frame | too_many_elements}.
parse_received/1 with a frame cap of MaxFrameBytes, which may not exceed the 16 MiB frame cap. A length header above MaxFrameBytes ends the parse as frame_too_large from its four bytes, so a caller that keeps Tail holds at most MaxFrameBytes plus the 4-byte header. A connection's handshake reads with a small cap.
Drain the complete frames in a buffer, in the {Frames, Tail} shape of 10.x. Deprecated, and removed in 11.0.0: use parse_received/1, which reports invalid frames and bytes that do not decode.
Frames holds only the frames that pass validate_received/1: a frame whose fields are refused is dropped without a warning. Bytes that do not decode end the parse: the frames before them come back with an empty Tail, and the rest of the buffer is dropped. A caller that keeps Tail for the next chunk never holds more than the frame cap plus its 4-byte header.
Drain every complete frame from a buffer as its CBOR bytes, exactly as received and without the length prefix, and return the incomplete rest. The handshake reads frames this way, because the connection proof hashes the challenge bytes as received. A length over the 16 MiB frame cap is refused.
-spec parse_stream_bytes(binary(), pos_integer()) -> {ok, [binary()], binary()} | {error, frame_too_large}.
parse_stream_bytes/1 with a frame cap of MaxFrameBytes, at most the 16 MiB frame cap. A length header above it is refused from its four bytes, before the frame's bytes are buffered, so a caller that keeps the rest holds at most MaxFrameBytes plus the header and one read. The handshake reads with a 64 KiB cap.
-spec plumtree_gossip(plumtree_gossip_spec()) -> frame().
-spec plumtree_graft(plumtree_graft_spec()) -> frame().
-spec plumtree_ihave(plumtree_ihave_spec()) -> frame().
-spec plumtree_prune(plumtree_prune_spec()) -> frame().
-spec provider_error(#{request := verified_request(), code := binary(), detail => binary(), source_route_reverse => binary()}, macula_node_keys:node_key()) -> frame().
Sign a provider's ERROR for a verified request: a code of at most 64 bytes and an optional detail of at most 256, both UTF-8. Other text raises a badmatch on {error, {text_too_long, Field}} or {error, {invalid_text, Field}}.
-spec provider_stream(stream_spec(), macula_node_keys:node_key(), verified_request()) -> frame().
Sign a provider's stream frame for a verified STREAM_OPEN with the provider's identity key. The first frame, seq 0, carries the key; later frames do not.
-spec publish(publish_spec(), macula_node_keys:node_key()) -> frame().
Sign a publication with the publisher's identity key, as a PUBLISH. Its tbs holds no frame_type, because the same bytes ride in every EVENT and GOSSIP made from it.
Read a frame from its decoded CBOR value, the way decode/1 reads one after decoding its bytes. A connection decodes each frame once to route it by its frame_type, and passes the value here. A frame whose fields its type refuses is bad_frame here too.
-spec refusal_charge(term()) -> charged | uncharged | unclassified.
charged_refusal/1 over any term, for a caller that must not raise on a kind it was handed: charged or uncharged for a kind charged_refusal/1 classifies, and unclassified for any other term.
-spec relay_error(#{frame_type := error | stream_error, request := verified_request(), code := unknown_next_peer, offending_hop => binary(), source_route_partial => binary()}, macula_node_keys:node_key()) -> frame().
Sign a station's relay error, an ERROR or STREAM_ERROR for a pending request, with the station's identity key. It carries a code from the closed set and no free text, so a spec with a detail raises function_clause.
-spec relayed_bytes(received_frame()) -> binary().
The bytes a relay received for a frame parse_for_relay/2 accepted, to write. Anything else is refused with function_clause, in the caller.
Whether a relayed frame of this type is taken without a frame signature: every HyParView frame, JOIN, FORWARD_JOIN, NEIGHBOR, DISCONNECT, SHUFFLE and SHUFFLE_REPLY, and the Plumtree IHAVE, GRAFT and PRUNE, which D17 leaves unsigned in pq_pure, and the Plumtree GOSSIP, whose publication is signed end to end. A station relays them from a connection it authenticated, so a receiver takes the relay's origin as their sender. A GOSSIP's publication stays unverified until verify_publication/3 accepts it, and its subscriber verifies it before acting on it. Every other relayed type keeps its own verification.
-spec result(#{request := verified_request(), payload := term(), source_route_reverse => binary()}, macula_node_keys:node_key()) -> frame().
Sign a RESULT for a verified request with the provider's identity key.
-spec sign(frame(), macula_identity:key_pair() | macula_identity:privkey()) -> frame().
-spec sign_neighbour(frame(), macula_node_keys:node_key(), #{connection := binary(), seq := non_neg_integer()}) -> frame().
Neighbour-sign a control frame with the sender's identity key, for one connection and one seq.
-spec station_ref(station_ref_spec()) -> station_ref().
-spec store(store_spec()) -> frame().
-spec store_ack(store_ack_spec()) -> frame().
-spec stream_bytes(stream_build(), macula_node_keys:node_key() | undefined) -> {ok, stream_bytes()} | {error, {unknown_build_key, term()} | unsignable | {not_allowed, stream_reply | stream_data} | {invalid_text, atom()} | {text_too_long, procedure | code | detail | message} | relay_code_outside_its_set | {unsupported_payload_type, atom(), [term()]} | frame_too_large}.
The bytes of a frame built here for a dedicated stream: the one step every such frame passes through before it is written. Build names the frame (see stream_build()); it is signed with the identity key Key, under that key's profile, and encoded. A build that its receiver would refuse, or that the wire cannot carry, returns an error and leaves nothing to write. These checks run in order, before anything is signed:
- {unknown_build_key, Key} for a field its frame does not have; - unsignable for a key that is not an identity key or not the sender the receiver verifies (the verified request's target for a reply or a provider's stream frame, its caller for a caller's stream frame; a key id holds its profile, so a key of the other profile is not the sender either), or a stream frame without its verified STREAM_OPEN; - {not_allowed, Type} for a stream frame its side does not send: a caller's STREAM_REPLY, or a caller's STREAM_DATA in a server_stream; - {text_too_long, Field} for a procedure over 512 bytes, a code over 64 bytes, or a provider error's detail or a STREAM_ERROR message over 256 bytes, judged before the text itself; - {invalid_text, Field} for a procedure, code, detail or message that is not a binary of valid UTF-8; - relay_code_outside_its_set for a relay error code outside the closed set; - {unsupported_payload_type, Type, Path} for a payload, stream body or reply the wire cannot carry.
After encoding, frame_too_large is returned for a frame whose encoding is over the 16 MiB frame cap.
None of these is for the peer. A call site sends a fixed code, never a rendering of a local error, and an error frame carries only the code its build names; a local error term given as text is {invalid_text, Field}.
A build that leaves out a field its frame requires (a result without its payload, a STREAM_ERROR without its message), an id, realm or target of the wrong size, a relay error whose offending hop is not 32 bytes, a stream frame type other than STREAM_DATA, STREAM_END, STREAM_ERROR and STREAM_REPLY, or a seq outside the protocol's range is a programming error and raises function_clause. Any other field outside its type, range or set is a programming error too, and raises: a deadline or retry budget out of range, a token or source route that is not a binary, a mode, role, encoding or relay frame type outside its set, or a raw body that is not a binary. A stream frame's seq is its side's own next sequence number, from 0. macula_peering:send_on_stream/2 and async_send_on_stream/2,3 write the result, through written_bytes/1.
-spec stream_open(request_spec(), macula_node_keys:node_key()) -> frame().
Sign a STREAM_OPEN, which carries its stream mode, with the caller's identity key.
-spec subscribe(subscribe_spec()) -> frame().
-spec swim_ack(swim_ack_spec()) -> frame().
-spec swim_confirm(swim_suspect_spec()) -> frame().
-spec swim_ping(swim_ping_spec()) -> frame().
-spec swim_suspect(swim_suspect_spec()) -> frame().
-spec swim_update(swim_update_spec()) -> swim_update().
-spec text_checked(topic | procedure | code | detail | message, term()) -> ok | {error, {text_too_long | invalid_text, topic | procedure | code | detail | message}}.
Check text a frame builder bounds, before anything is built: a topic or a procedure of at most 512 bytes, a code of at most 64, and a detail or a message of at most 256, each valid UTF-8. Returns the builder's own refusal, so a caller refuses local text by name instead of letting a builder raise.
-spec unadvertise(unadvertise_spec()) -> frame().
-spec unsubscribe(unsubscribe_spec()) -> frame().
-spec validate_received(frame()) -> ok | {error, {invalid_frame, frame_type() | unknown, atom()}}.
Does a frame decoded from a peer's bytes have the shape its type requires, the post-quantum shape its builder writes (D26)? Each required field must be there with a value its builder accepts: a 16-byte id, a 32-byte key, a 48-byte hash, a binary, an integer in range, an atom of a closed set, a signed object, or a list of well-formed entries. A type that carries one of several signed objects holds exactly one of them. An optional field may be missing or undefined, a CBOR null. A field the type does not have refuses the frame, and so do a version other than the protocol's and a FORWARD_JOIN whose prwl is above its arwl, the one rule between fields. A control frame signed for its neighbour holds version, frame_type and neighbour and nothing else, and the frame verify_neighbour/2 opens from it is checked the same way. A handler of a received frame then never meets a missing key or a value it cannot match. The rules sit beside the field table and macula_frame_received_tests holds the two to each other.
The refusal names the frame type and the first field refused, both this node's own atoms. A frame without a frame_type, or of a type this node does not know, is refused as {invalid_frame, unknown, frame_type}. Signatures are not checked here.
-spec value(value_spec()) -> frame().
-spec verify(frame(), macula_identity:pubkey()) -> {ok, frame()} | {error, term()}.
-spec verify_caller_stream(frame(), stream_state(), macula_crypto_profile:profile()) -> {ok, map(), stream_state()} | {error, malformed_frame | signature_invalid | key_id_mismatch | request_mismatch | seq_mismatch | stream_ended}.
Verify a caller's stream frame against the stream's state with the STREAM_OPEN's key, and return its fields and the next state. The signer must be the STREAM_OPEN's caller, and in a server_stream a caller sends no STREAM_DATA.
-spec verify_neighbour(frame(), #{profile := macula_crypto_profile:profile(), peer_key := binary(), connection := binary(), seq := non_neg_integer()}) -> {ok, frame()} | {error, malformed_frame | signature_invalid}.
Read a received frame under the connection's profile. A frame type the profile signs must be exactly {version, frame_type, neighbour}, signed by the peer's identity key for this connection and the next seq, and comes back as the frame its tbs holds. Any other frame must not carry neighbour and comes back as it is.
-spec verify_provider_stream(frame(), stream_state(), macula_crypto_profile:profile()) -> {ok, map(), stream_state()} | {error, malformed_frame | signature_invalid | key_id_mismatch | not_the_target | request_mismatch | seq_mismatch | stream_ended}.
Verify a provider's stream frame against the stream's state, and return its fields and the next state. Before the provider's first frame the verifier holds no provider key, so a frame without one is out of order. The first frame's key must be the key id of its signer and the STREAM_OPEN's target; later frames verify with that key. Each side's seq is the previous one plus one, and nothing follows its STREAM_END.
-spec verify_publication(frame(), macula_crypto_profile:profile(), integer()) -> {ok, verified_publication()} | {error, malformed_frame | signature_invalid | key_id_mismatch | {not_yet_valid, pos_integer()} | {expired, pos_integer()}}.
Verify the publication a PUBLISH, EVENT or GOSSIP carries, under the connection's profile and a clock in milliseconds: its signature and fields, a ttl_ms of at most one hour, publisher as the key id of its key, a published_at no more than 5 minutes ahead, and not past published_at plus its ttl_ms, or 10 minutes without one, plus 5 minutes. The origin station checks this before fan-out, and every subscriber before delivery. A refusal for its time says how far outside its window the publication is, in milliseconds: {not_yet_valid, AheadMs} past the 5 minutes ahead it may be, and {expired, PastMs} past its expiry.
-spec verify_relay_error(frame(), verified_request(), macula_crypto_profile:profile(), <<_:256>>) -> {ok, map()} | {error, malformed_frame | signature_invalid | key_id_mismatch | request_mismatch | not_the_connection}.
Verify a received relay error for the pending request it names, from the station the connection authenticated: its signature and fields, reported_by as the key id of its key, the request's request_id and request_hash, and reported_by as ExpectedReporter, that station's node_id. A relay error another station reports is refused as not_the_connection.
-spec verify_reply(frame(), verified_request(), macula_crypto_profile:profile()) -> {ok, map()} | {error, malformed_frame | signature_invalid | key_id_mismatch | request_mismatch | not_the_target}.
Verify a received RESULT or provider ERROR for the request it answers: its signature and fields, responded_by as the key id of its key, the request's request_id and request_hash, and responded_by as the request's target.
-spec verify_request(frame(), macula_crypto_profile:profile()) -> {ok, verified_request()} | {error, malformed_frame | signature_invalid | key_id_mismatch}.
Verify a received CALL or STREAM_OPEN under the connection's profile: the request's signature and fields, and caller as the key id of its key. A station checks this before it routes, a provider before its own checks.
-spec written_bytes(stream_bytes()) -> binary().
The bytes stream_bytes/2 built, to write. Anything else is refused with function_clause, in the caller.