GossipSub

Gossip-based message propagation

XE uses GossipSub for broadcasting messages to all peers in the network. Each category of data has its own topic, and each topic has a dedicated gossip type with Publish() and Subscribe() methods.

Topics

TopicConstantData typeChannel buffer
xe/blocksBlockTopicBlockMsg (wraps core.Block)256
xe/votesVoteTopicVoteMsg (wraps core.Vote)1024
xe/marketplaceMarketplaceTopicMarketplaceMsg256
xe/statechainStateChainTopicStateChainMsg (wraps statechain.Block)16
xe/directoryDirectoryTopicdirectory.Registration256
xe/certificatesCertificateTopicperf.Certificate64

Message Size Limit

const MaxGossipMessageSize = 262144 // 256 KB

The 256 KB limit accommodates state chain blocks which can carry large key-value data. Block lattice messages are much smaller but share the same PubSub instance.

Gossip Types

All gossip types share a common PubSub instance created with NewPubSub():

func NewPubSub(ctx context.Context, h host.Host) (*pubsub.PubSub, error)

Each gossip type joins its respective topic and provides Publish() and Subscribe() methods.

Block Gossip (Gossip)

Broadcasts and receives block lattice blocks.

type Gossip struct { /* ... */ }

func NewGossip(ctx context.Context, h host.Host, ps ...*pubsub.PubSub) (*Gossip, error)
func (g *Gossip) Publish(ctx context.Context, b *core.Block) error
func (g *Gossip) Subscribe(ctx context.Context) <-chan *core.Block
func (g *Gossip) PubSub() *pubsub.PubSub

The PubSub() accessor returns the underlying PubSub instance for sharing with other gossip types.

Vote Gossip (VoteGossip)

Broadcasts and receives finalization votes — both converge and final votes.

type VoteGossip struct { /* ... */ }

func NewVoteGossip(ps *pubsub.PubSub) (*VoteGossip, error)
func (vg *VoteGossip) Publish(ctx context.Context, v *core.Vote) error
func (vg *VoteGossip) Subscribe(ctx context.Context) <-chan *core.Vote

The vote receive path is the one place gossip does more than decode-and-forward, because a lost final vote is not recoverable the way a lost block is:

  1. Length pre-validationRepPubKey and BlockHash must be 64 hex characters, Signature 64 raw bytes.
  2. Signature verification, before dedup — a forged vote is dropped before it can touch dedup state. Otherwise an attacker flooding Final = true votes for a victim representative with random signature bytes could evict that representative's genuine, finality-critical vote from the dedup window.
  3. Dedup — exact repeats (same representative, position, block hash and Final flag) seen within 200 ms are dropped as carrying no new information. Final is part of the key, so a converge vote and the representative's later final vote are distinct and both pass.
  4. Priority send — a final vote takes a blocking, context-guarded send and is never dropped on a full buffer. A converge vote takes a non-blocking send and may be dropped, because the 15-second re-emit sweep will re-deliver it.

Certificate Gossip (CertificateGossip)

Broadcasts and receives provider performance certificates.

type CertificateGossip struct { /* ... */ }

func NewCertificateGossip(ps *pubsub.PubSub) (*CertificateGossip, error)
func (cg *CertificateGossip) Publish(ctx context.Context, cert *perf.Certificate) error
func (cg *CertificateGossip) Subscribe(ctx context.Context) <-chan *perf.Certificate

State Chain Gossip (StateChainGossip)

Broadcasts and receives state chain blocks (governance operations).

type StateChainGossip struct { /* ... */ }

func NewStateChainGossip(ps *pubsub.PubSub) (*StateChainGossip, error)
func (sg *StateChainGossip) Publish(ctx context.Context, b *statechain.Block) error
func (sg *StateChainGossip) Subscribe(ctx context.Context) <-chan *statechain.Block

Directory Gossip (DirectoryGossip)

Broadcasts and receives account directory registrations (human-readable name mappings).

type DirectoryGossip struct { /* ... */ }

func NewDirectoryGossip(ps *pubsub.PubSub) (*DirectoryGossip, error)
func (dg *DirectoryGossip) Publish(ctx context.Context, reg *directory.Registration) error
func (dg *DirectoryGossip) Subscribe(ctx context.Context) <-chan *directory.Registration

Marketplace Gossip (MarketplaceGossip)

Broadcasts and receives marketplace messages (resource advertisements, requests, and offers).

type MarketplaceGossip struct { /* ... */ }

func NewMarketplaceGossip(ps *pubsub.PubSub) (*MarketplaceGossip, error)
func (mg *MarketplaceGossip) Publish(ctx context.Context, msg *MarketplaceMsg) error
func (mg *MarketplaceGossip) Subscribe(ctx context.Context) <-chan *MarketplaceMsg

Pre-Validation

Messages are validated before being passed to consumers. This rejects malformed data before expensive cryptographic verification.

Block Validation

// Hash and Account are 64 hex chars (32 bytes), Signature is 128 hex chars (64-byte ed25519)
if len(b.Hash) != 64 || len(b.Signature) != 128 || len(b.Account) != 64 {
    continue // drop
}
FieldExpected lengthEncoding
Hash64 charsHex (32 bytes)
Signature128 charsHex (64 bytes)
Account64 charsHex (32 bytes)

Vote Validation

// RepPubKey and BlockHash are 64 hex chars, Signature is 64 raw bytes (ed25519)
if len(v.RepPubKey) != 64 || len(v.BlockHash) != 64 || len(v.Signature) != 64 {
    continue // drop
}

State Chain Block Validation

// Hash must be 64 hex chars, at least one signature required
if len(scm.Block.Hash) != 64 || len(scm.Block.Signatures) == 0 {
    continue // drop
}

Directory Validation

// Account and Signature must be non-empty
if reg.Account == "" || reg.Signature == "" {
    continue // drop
}

Marketplace Validation

// Type field must be non-empty
if mm.Type == "" {
    continue // drop
}

Certificate Validation

// Provider, Hash and Signature must all be non-empty
if cert.Provider == "" || cert.Hash == "" || cert.Signature == "" {
    continue // drop
}

Message Flow

Publisher Node                          Subscriber Node
─────────────                          ───────────────
     │                                       │
     │  Publish(ctx, block)                  │
     │     │                                 │
     │     ▼                                 │
     │  JSON marshal                         │
     │     │                                 │
     │     ▼                                 │
     │  topic.Publish() ──── GossipSub ────▶ sub.Next()
     │                                       │
     │                                       ▼
     │                                  JSON decode
     │                                       │
     │                                       ▼
     │                                  Pre-validate
     │                                       │
     │                                  ┌────┴────┐
     │                                  │ Valid?  │
     │                                  └────┬────┘
     │                                   yes │ no
     │                                       │  └─▶ drop
     │                                       ▼
     │                                  ch <- block
     │                                  (or drop if full)

Backpressure

Each Subscribe method creates a buffered channel. If the consumer is slow and the channel fills up, incoming messages are dropped with a log warning:

gossip: block channel full, dropping block a1b2c3d4e5f6... (total dropped: 17)

Dropped blocks are counted and the running total is exposed as dropped_blocks on GET /node. Non-zero and rising means the node is shedding gossiped blocks under sustained load and falling behind.

[!WARNING] Dropped Messages Dropped blocks are recovered by the sync protocol, the backstop for anything gossip loses. Dropped converge votes are recovered by the finalization sweep, which re-emits a representative's current preference once per 15-second window; final votes are never dropped at all (see the priority-send rule above). A position that still cannot resolve is recovered by a targeted vote pull. Marketplace messages rely on re-advertisement by peers.

JSON Encoding

All gossip messages use JSON encoding with encoding/json. Block and vote decoders use DisallowUnknownFields() for strict parsing. State chain, directory, certificate and marketplace decoders use standard json.Unmarshal / a plain decoder.

Shared PubSub Instance

All gossip types share a single PubSub instance. The typical initialization order is:

// 1. Create shared PubSub
ps, err := xenet.NewPubSub(ctx, host)

// 2. Create block gossip (can also create PubSub internally)
blockGossip, err := xenet.NewGossip(ctx, host, ps)

// 3. Create other gossip types sharing the same PubSub
voteGossip, err := xenet.NewVoteGossip(ps)
stateChainGossip, err := xenet.NewStateChainGossip(ps)
directoryGossip, err := xenet.NewDirectoryGossip(ps)
marketplaceGossip, err := xenet.NewMarketplaceGossip(ps)
certGossip, err := xenet.NewCertificateGossip(ps)

[!INFO] Why Share PubSub? A single PubSub instance maintains one set of peer connections and mesh overlays. Sharing it across topics avoids duplicate connection overhead and ensures consistent peer scoring.