Files
alknet/docs/research/references/iroh/iroh-gossip/07-utilities-wire-format.md

6.4 KiB

iroh-gossip: Utility Data Structures & Wire Format

IndexSet (proto::util::IndexSet)

A wrapper around indexmap::IndexSet that provides random selection capabilities needed by HyParView:

pub(crate) struct IndexSet<T> {
    inner: indexmap::IndexSet<T>,
}

Key Operations

Method Purpose
insert(value) Add element (returns false if already present)
remove(value) Remove by value (swap-remove, O(1))
remove_index(index) Remove by index (swap-remove)
remove_random(rng) Remove a random element
pick_random(rng) Get reference to random element
pick_random_without(exclude, rng) Random element excluding certain elements
pick_random_index(rng) Random index
shuffled(rng) All elements in random order
shuffled_and_capped(len, rng) First len elements after shuffle
shuffled_without(exclude, rng) Random order excluding certain elements
shuffled_without_and_capped(exclude, len, rng) Capped shuffle excluding elements
iter_without(value) Iterator skipping a specific element

These operations are critical for HyParView's random walks, shuffle exchanges, and passive view management.

TimerMap (proto::util::TimerMap)

A priority queue of timer entries sorted by Instant, with stable ordering via a sequence counter:

pub struct TimerMap<T> {
    heap: BinaryHeap<TimerMapEntry<T>>,
    seq: u64,
}

Used by the protocol state machine for scheduling future events (shuffles, graft timeouts, cache eviction). The networking layer wraps this in an async-friendly Timers type that can wait_next().

Key Operations

Method Purpose
insert(instant, item) Schedule a timer
pop_before(limit) Pop the earliest entry if it's before limit
drain_until(from) Drain all entries up to a time
first() Get reference to earliest entry

TimeBoundCache (proto::util::TimeBoundCache)

A HashMap where entries expire after a specified Instant:

pub struct TimeBoundCache<K, V> {
    map: HashMap<K, (Instant, V)>,
    expiry: TimerMap<K>,
}

Used by PlumTree for:

  • received_messages: TimeBoundCache<MessageId, ()> — deduplication
  • cache: TimeBoundCache<MessageId, Gossip> — message payload storage for Graft replies

Key Operations

Method Purpose
insert(key, value, expires) Insert with expiration
contains_key(key) Check existence
get(key) Get value
expires(key) Get expiration time
expire_until(instant) Remove all expired entries, returns count
len() / is_empty() Size queries

The expire_until method correctly handles re-insertions: if a key is re-inserted with a later expiration time after being added to the expiry queue, the old expiry entry is ignored (not removed from the map).

Wire Format

Frame Encoding

Messages are encoded using postcard (a no_std-friendly, serde-compatible format) and sent as length-prefixed frames:

┌──────────────┬──────────────┬─────────────────┐
│ Length (u32) │ TopicHeader  │ Message Payload  │
│   big-endian │  postcard    │   postcard       │
└──────────────┴──────────────┴─────────────────┘

Stream Protocol

Each QUIC unidirectional stream is dedicated to a single topic. The stream begins with a StreamHeader:

pub(crate) struct StreamHeader {
    pub(crate) topic_id: TopicId,
}

All subsequent frames on that stream carry messages for that topic. When a Disconnect message is sent, the stream is closed (via finish()).

Message Types on Wire

pub enum Message<PI> {
    Swarm(hyparview::Message<PI>),   // Membership messages
    Gossip(plumtree::Message),        // Broadcast messages
}

Where PI is PublicKey (32-byte ed25519 public key) in the networking layer.

The MessageKind classification is used for metrics:

Kind Message Types
Data Gossip messages (actual content)
Control All Swarm messages, plus Prune, Graft, IHave

Message Size Limits

  • Default max message size: 4096 bytes (minimum: 512)
  • The header size is computed at compile time via postcard::experimental::serialized_size
  • Actual payload capacity = max_message_size - header_size

The SendLoop checks message size before writing and returns WriteError::TooLarge if exceeded.

PeerData & Address Propagation

The PeerData type is an opaque Bytes wrapper used in HyParView messages. In the net layer, it carries addressing information:

struct AddrInfo {
    relay_url: Option<RelayUrl>,
    direct_addresses: BTreeSet<SocketAddr>,
}

This is serialized with postcard and passed as PeerData in Join, ForwardJoin, and Neighbor messages. When received, the AddrInfo is decoded and fed into GossipAddressLookup, which implements iroh's AddressLookup trait, allowing gossip-discovered addresses to be used for future connections.

GossipAddressLookup

pub(crate) struct GossipAddressLookup {
    endpoints: NodeMap,                       // Arc<RwLock<BTreeMap<EndpointId, StoredEndpointInfo>>>
    _task_handle: Arc<AbortOnDropHandle<()>>, // Background eviction task
}

Key behaviors:

  • Merging: When adding addresses for an already-known endpoint, new addresses are merged (union of direct addresses, relay URL is overwritten)
  • Expiration: Entries expire after 5 minutes, with eviction checks every 30 seconds
  • Integration: Implements iroh::address_lookup::AddressLookup, returning data with provenance "gossip"

Dialer

struct Dialer {
    endpoint: Endpoint,
    pending: JoinSet<(EndpointId, Option<Result<Connection, ConnectError>>)>,
    pending_dials: HashMap<EndpointId, CancellationToken>,
}

The Dialer manages outgoing connection attempts:

  • Queues a dial via queue_dial(endpoint_id, alpn)
  • Checks for pending dials to avoid duplicate connections
  • Supports cancellation of in-progress dials
  • Returns completed connections via next_conn()

When a dial succeeds, the connection is passed to handle_connection(). When a dial fails and the peer is not already active, a PeerDisconnected event is injected into the protocol state.