Direct Messaging

Peer-to-peer direct messaging

The messaging protocol provides request-response semantics for targeted peer-to-peer communication. Unlike GossipSub which broadcasts to all peers, messaging sends a request to a specific peer and waits for a response.

Protocol ID: /xe/msg/1.0.0

#Wire Types

#MsgRequest

go
type MsgRequest struct {    Type    string          `json:"type"`    Payload json.RawMessage `json:"payload"`}

#MsgResponse

go
type MsgResponse struct {    Type    string          `json:"type"`    Error   string          `json:"error,omitempty"`    Payload json.RawMessage `json:"payload"`}

#MsgHandler

go
type MsgHandler func(from peer.ID, payload json.RawMessage) (json.RawMessage, error)

Handlers receive the sender's peer ID and the raw JSON payload. They return a raw JSON response or an error.

#Constants

ConstantValueDescription
MsgProtocol/xe/msg/1.0.0Stream protocol identifier
MsgStreamDeadline30 secondsRead/write timeout per stream
MsgMaxRequestSize64 KB (65,536 bytes)Maximum incoming request size
MsgMaxResponseSize64 KB (65,536 bytes)Maximum incoming response size

#Messenger

The Messenger struct manages handler registration and request dispatching:

go
type Messenger struct {    host     host.Host    dht      *dht.IpfsDHT    handlers map[string]MsgHandler    mu       sync.RWMutex}

#Construction

go
func NewMessenger(h host.Host, d *dht.IpfsDHT) *Messenger

NewMessenger creates a Messenger and registers the /xe/msg/1.0.0 stream handler on the host. The DHT is used for peer discovery when the target peer is not in the peerstore.

#Registering Handlers

go
func (m *Messenger) Handle(msgType string, h MsgHandler)

Registers a handler for a specific message type. Handlers are stored in a thread-safe map and dispatched by the Type field of incoming requests.

#Sending Requests

go
func (m *Messenger) Request(ctx context.Context, target peer.ID, msgType string, payload any) (json.RawMessage, error)

Sends a typed request to a specific peer and waits for the response. The flow:

  1. Marshal payload -- The payload (any type) is JSON-marshaled into json.RawMessage
  2. Find peer -- If the target is not in the peerstore and a DHT is available, FindPeer() is called to locate the peer and Connect() establishes a connection
  3. Open stream -- A new stream is opened to the target on protocol /xe/msg/1.0.0
  4. Set deadline -- Uses the context deadline if set, otherwise defaults to 30 seconds
  5. Send request -- JSON-encodes the MsgRequest and calls CloseWrite() to signal completion
  6. Read response -- JSON-decodes the MsgResponse from a size-limited reader
  7. Check error -- If the response contains an Error field, returns it as a Go error

#Request-Response Flow

diagram
Client                                    Server──────                                    ──────  │                                          │  │  Request(ctx, target, type, payload)     │  │                                          │  │  1. Find peer via DHT (if needed)        │  │  2. Connect (if needed)                  │  │  3. Open stream                          │  │                                          │  │  MsgRequest{Type, Payload} ────────────▶ │  │  CloseWrite() ─────────────────────────▶ │  │                                          │  Lookup handler by Type  │                                          │  Call handler(from, payload)  │                                          │  │  ◀──────────── MsgResponse{Type, Payload}│  │                                          │  │  Close stream                            │  │                                          │

#Peer Discovery via DHT

When Request() is called for a peer not in the peerstore, the Messenger uses the Kademlia DHT to find the peer:

go
if len(m.host.Peerstore().Addrs(target)) == 0 && m.dht != nil {    pi, err := m.dht.FindPeer(ctx, target)    // ... connect to discovered peer}

This makes messaging work even when the sender has never directly connected to the recipient, as long as both are part of the DHT.

#Message Types

The messaging protocol is generic -- the Type field determines which handler processes the request. The following message types are registered as point-to-point handlers:

TypeDirectionPurpose
vm_credentialsProvider → ConsumerDelivers VM SSH connection details after provisioning
vm_statusConsumer → ProviderQueries current VM status for a lease
account_chatAny → AnySends a chat message between accounts
attest_timestampProvider → TimekeeperRequests a signed timestamp attestation for a lease

#Payload Types

#ResourceAdvertisement

go
type ResourceAdvertisement struct {    Provider  string `json:"provider"`    VCPUs     uint64 `json:"vcpus"`    MemoryMB  uint64 `json:"memory_mb"`    DiskGB    uint64 `json:"disk_gb"`    Timestamp int64  `json:"timestamp"`    Signature string `json:"signature"`}

#ResourceRequest

go
type ResourceRequest struct {    Consumer  string `json:"consumer"`    RequestID string `json:"request_id"`    VCPUs     uint64 `json:"vcpus"`    MemoryMB  uint64 `json:"memory_mb"`    DiskGB    uint64 `json:"disk_gb"`    Duration  uint64 `json:"duration"`  // seconds    Timestamp int64  `json:"timestamp"`    Signature string `json:"signature"`}

#ResourceOffer

go
type ResourceOffer struct {    Provider  string `json:"provider"`    RequestID string `json:"request_id"`    VCPUs     uint64 `json:"vcpus"`    MemoryMB  uint64 `json:"memory_mb"`    DiskGB    uint64 `json:"disk_gb"`    Duration  uint64 `json:"duration"`    TotalCost uint64 `json:"total_cost"`    Timestamp int64  `json:"timestamp"`    Signature string `json:"signature"`}

#StateChainSyncRequest / Response

go
type StateChainSyncRequest struct {    TipIndex int64 `json:"tip_index"` // -1 if empty chain} type StateChainSyncResponse struct {    Blocks  []*statechain.Block `json:"blocks"`    HasMore bool                `json:"has_more"`}

#MarketplaceMsg

go
type MarketplaceMsg struct {    Type    string                 `json:"type"` // "advertisement", "request", "offer"    Ad      *ResourceAdvertisement `json:"ad,omitempty"`    Request *ResourceRequest       `json:"request,omitempty"`    Offer   *ResourceOffer         `json:"offer,omitempty"`}

#Error Handling

Errors can occur at multiple levels:

LevelHandling
Peer not found (DHT)Request() returns error
Connection failureRequest() returns error
Stream open failureRequest() returns error
Timeout (30s deadline)Stream read/write fails
Unknown message typeServer responds with error in MsgResponse.Error
Handler returns errorServer responds with error in MsgResponse.Error
Decode failureServer logs error, no response sent

#Comparison with GossipSub

MessagingGossipSub
TargetSpecific peerAll peers
PatternRequest-responsePublish-subscribe
DeliveryReliable (or error)Best-effort
Use caseMarketplace negotiation, targeted syncBlock/vote broadcast
Size limit64 KB256 KB
Timeout30 secondsNone (async)

Use messaging when you need a response from a specific peer. Use gossip when you need to broadcast to the entire network.