macula_hyparview_proto (macula v11.4.0)
View SourceHyParView protocol orchestrator (Part 3 §7.1).
Pure functional layer on top of macula_hyparview_view. Given the current view, an incoming HyParView frame, and a context (self node_id, realm, ARWL/PRWL constants and the admission trust), process/4 returns the updated view plus a list of {send, TargetPeer, Frame} actions the wrapping process should transmit.
Frames leave this module without a signature of their own: the connection that sends a frame adds its neighbour signature (D17).
Message handlers
- JOIN — receiver becomes the joiner's contact: add_active(joiner), reply NEIGHBOR(high), FORWARD_JOIN(ttl=ARWL) to every other active peer. Eviction in the active view emits DISCONNECT to the demoted peer. When
ctx()carriesrealm_key_id, admission is gated on the frame'srecordfield holding the wire form of arealm_member_endorsementthat verifies under the ctxprofile, signed by the realm key with that key id, for this exact (realm, joiner) pair (macula_hyparview_endorsement: verify_endorsement/3). A missing or invalid endorsement is dropped silently (no ack, no forward, view unchanged), never admitted. Withoutrealm_key_idinctx(), admission is unconditional (opt-in gating, matching the frame spec's own "endorsement is optional" design). - FORWARD_JOIN — if ttl=0 or active view is empty: add_active(new_member) + reply NEIGHBOR(high). Otherwise: if ttl equals this node's own PRWL, also place new_member in the passive view, within the sender's allowance; decrement ttl and forward to a random active peer that is not the sender. Carries the ORIGINAL JOIN's
record(endorsement) through the whole forward chain and re-verifies it at every admission point — trust in the endorsement is never transitively assumed just because a neighbour forwarded it. - NEIGHBOR(high) — always add_active(sender), evicting if needed.
- NEIGHBOR(low) — add_active(sender) only when the active view has room.
- DISCONNECT — demote sender to passive.
- SHUFFLE — if ttl > 0 and a forwardable neighbour exists, decrement ttl + forward; otherwise build SHUFFLE_REPLY against our own sample and send back to the origin, then place the incoming sample in the passive view, within the sender's allowance.
- SHUFFLE_REPLY: place the incoming sample in the passive view, within the sender's allowance, only while a SHUFFLE this node sent in the last 30 seconds has no reply yet (
build_shuffle/2records each one).
A neighbour places at most 20 node_ids per minute in the passive view, counting only node_ids new to it (DESIGN_PQ_DHT_SLOTS_AND_BUDGET.md, 3.1). A frame that brings more, and a SHUFFLE_REPLY that answers no SHUFFLE, return a {refused, Neighbour, Kind} action, which the wrapping process reports through macula_peering:object_refused/2. ctx() carries now, in monotonic milliseconds, for both.
Reference: plans/PLAN_MACULA_V2_PART3_DISCOVERY.md §7.1; plans/PLAN_PHASE_5_BREAKDOWN.md Session 5.2.
Summary
Functions
Build a JOIN frame to send to a contact peer.
A periodic shuffle round: a SHUFFLE to a random active neighbour, carrying a sample of this node's view, and the view recording that it was sent, so that one SHUFFLE_REPLY within 30 seconds is merged. No SHUFFLE goes out while the active view is empty.
Types
-type action() :: {send, peer(), macula_frame:frame()} | {refused, peer(), placement_allowance | unsolicited_shuffle_reply}.
-type ctx() :: #{self_id := peer(), realm := <<_:256>>, now := integer(), arwl => non_neg_integer(), prwl => non_neg_integer(), shuffle_ttl => non_neg_integer(), shuffle_active_size => non_neg_integer(), shuffle_passive_size => non_neg_integer(), realm_key_id => <<_:256>>, profile => macula_crypto_profile:profile(), self_endorsement => binary()}.
-type peer() :: <<_:256>>.
Functions
-spec build_join(ctx()) -> macula_frame:frame().
Build a JOIN frame to send to a contact peer.
-spec build_shuffle(macula_hyparview_view:view(), ctx()) -> {macula_hyparview_view:view(), [action()]}.
A periodic shuffle round: a SHUFFLE to a random active neighbour, carrying a sample of this node's view, and the view recording that it was sent, so that one SHUFFLE_REPLY within 30 seconds is merged. No SHUFFLE goes out while the active view is empty.
-spec process(macula_hyparview_view:view(), peer(), macula_frame:frame(), ctx()) -> {macula_hyparview_view:view(), [action()]}.