Files
alknet/docs/research/references/iroh/iroh-gossip/04-state-and-topic.md

6.2 KiB

iroh-gossip: Protocol State & Topic Coordination

Overview

The state module (src/proto/state.rs) provides the top-level protocol state machine that manages multiple topics. The topic module (src/proto/topic.rs) coordinates the HyParView and PlumTree state machines for a single topic.

Multi-Topic State (state::State)

pub struct State<PI, R> {
    me: PI,                                    // Our peer identity
    me_data: PeerData,                         // Our opaque peer data
    config: Config,                            // Protocol configuration
    rng: R,                                    // Random number generator
    states: HashMap<TopicId, topic::State<PI, R>>,  // Per-topic state
    outbox: Outbox<PI>,                        // Buffered output events
    peer_topics: ConnsMap<PI>,                 // Maps peer → set of shared topics
}

The State acts as a multiplexer — it routes events to the correct topic's state and collects output events. It also tracks which topics are shared with each peer (in peer_topics), which is used to determine when a peer connection can safely be closed (only when no topic still needs it).

TopicId

#[derive(Clone, Copy, Eq, PartialEq, Hash, Serialize, Ord, PartialOrd, Deserialize)]
pub struct TopicId([u8; 32]);

A 32-byte identifier for a topic. Typically created as blake3::hash(topic_name) or from raw bytes. Each topic is an independent swarm and broadcast scope.

Wire Message Format

pub struct Message<PI> {
    pub topic: TopicId,
    pub message: topic::Message<PI>,
}

Every wire message carries the TopicId prefix, allowing multiplexing of multiple topics over a single connection.

Event Routing

InEvent is mapped to either a topic-specific event or a global event:

InEvent Routing
RecvMessage(from, Message{topic, message}) → Topic-specific: topic::InEvent::RecvMessage
Command(topic, command) → Topic-specific: topic::InEvent::Command
TimerExpired(Timer{topic, timer}) → Topic-specific: topic::InEvent::TimerExpired
PeerDisconnected(peer) → Broadcast to ALL topics
UpdatePeerData(data) → Broadcast to ALL topics

Topic Lifecycle

When a Command::Join(peers) is received for a topic that doesn't yet have state, a new topic::State is automatically created. When Command::Quit is received, the topic's state is removed after processing the quit event.

Connection Management

When a topic::OutEvent::DisconnectPeer(peer) is emitted, the state module checks peer_topics to see if any other topic still needs a connection to that peer. Only when no topic needs the peer anymore is OutEvent::DisconnectPeer(peer) emitted at the top level.

Topic State (topic::State)

pub struct State<PI, R> {
    me: PI,
    pub swarm: hyparview::State<PI, R>,    // HyParView membership
    pub gossip: plumtree::State<PI>,          // PlumTree broadcast
    outbox: VecDeque<OutEvent<PI>>,
    stats: Stats,
}

The topic state composes HyParView and PlumTree, bridging them together:

Event Forwarding

When topic::State::handle() is called:

  1. HyParView events are processed first (membership layer).
  2. NeighborUp/NeighborDown events emitted by HyParView are forwarded to PlumTree:
    • NeighborUp(peer)plumtree::InEvent::NeighborUp(peer) — adds peer to eager set
    • NeighborDown(peer)plumtree::InEvent::NeighborDown(peer) — removes peer from both sets
  3. All output events from both layers are collected and returned.

Command Handling

Command Action
Join(peers) Sends RequestJoin(peer) to HyParView for each peer in the list
Broadcast(data, scope) Sends Broadcast(data, scope) to PlumTree
Quit Sends Quit to HyParView (which sends Disconnect to all active peers)

Message Routing

When a topic message is received:

match message {
    Message::Swarm(message) => hyparview.handle(RecvMessage(from, message)),
    Message::Gossip(message) => plumtree.handle(RecvMessage(from, message)),
}

Timer Routing

match timer {
    Timer::Swarm(timer) => hyparview.handle(TimerExpired(timer)),
    Timer::Gossip(timer) => plumtree.handle(TimerExpired(timer)),
}

Topic Messages (topic::Message)

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

The message kind is used for metrics tracking:

pub fn kind(&self) -> MessageKind {
    match self {
        Message::Swarm(_) => MessageKind::Control,
        Message::Gossip(message) => match message {
            plumtree::Message::Gossip(_) => MessageKind::Data,
            _ => MessageKind::Control,
        },
    }
}

Topic Events (topic::Event)

pub enum Event<PI> {
    NeighborUp(PI),           // From HyParView: new active neighbor
    NeighborDown(PI),         // From HyParView: lost active neighbor
    Received(GossipEvent<PI>), // From PlumTree: received a gossip message
}

The Received event contains:

pub struct GossipEvent<PI> {
    pub content: Bytes,              // Message payload
    pub delivered_from: PI,          // Peer that delivered the message to us
    pub scope: DeliveryScope,        // Swarm(round) or Neighbors
}

Topic Configuration

pub struct Config {
    pub membership: hyparview::Config,    // HyParView configuration
    pub broadcast: plumtree::Config,       // PlumTree configuration
    pub max_message_size: usize,           // Maximum wire message size (default: 4096)
}

The max_message_size is the total wire-level message size including headers. The actual payload capacity is computed as max_message_size - postcard_header_size, where the header size accounts for the topic ID and message envelope overhead.

Statistics

Each topic tracks:

pub struct Stats {
    pub messages_sent: usize,
    pub messages_received: usize,
}

The PlumTree layer also tracks:

pub struct Stats {
    pub payload_messages_received: u64,
    pub control_messages_received: u64,
    pub max_last_delivery_hop: u16,
}