Message Routing Differences Between Broadcast and Direct Peer-to-Peer in mpcium

mpcium implements two distinct NATS-based communication paths: broadcast routing uses pub/sub with cryptographic signatures on global topics for all participants, while direct peer-to-peer routing uses encrypted point-to-point channels with recipient-specific subjects.

The mpcium library separates MPC session communication into broadcast and direct messaging layers to optimize for both global coordination and private node-to-node exchanges. Understanding these message routing differences between broadcast and direct peer-to-peer mechanisms is critical for debugging distributed threshold signature operations and ensuring secure message delivery across the network.

Transport Layer Architecture

Broadcast Pub/Sub Mechanics

Broadcast communication leverages NATS publish/subscribe on a shared broadcast subject that every participant in the MPC session subscribes to. This path is selected when routing.IsBroadcast evaluates to true and the recipient list is empty, indicating a message intended for all parties. The PubSub interface in pkg/messaging/pubsub.go handles these transmissions, publishing signed messages through pubSub.Publish without encryption since the content is meant for global visibility.

Direct Point-to-Point Channels

Direct peer-to-peer messaging utilizes NATS request/reply (or plain publish) on direct subjects that encode both sender and receiver IDs. When routing.IsBroadcast is false or specific recipients are listed in routing.To, the session iterates through target party IDs and composes unique topics using TopicComposer.ComposeDirectTopic(fromID, toID). This approach creates isolated communication channels such as mpc.direct:<fromID>:<toID>:<walletID>, ensuring messages reach only intended recipients.

Security Models: Authentication vs Confidentiality

Broadcast Message Signatures

In the broadcast path, the TssMessage is signed with the node’s identity key via identityStore.SignMessage and transmitted as-is. Receiving nodes validate authenticity through identityStore.VerifyMessage in the broadcast handler. No encryption is applied because the message content must remain readable by all session participants, relying solely on signature verification to prevent tampering.

Direct Message Encryption

For direct routing, the TssMessage is encrypted for the specific target node using identityStore.EncryptMessage, and no digital signature is attached. According to the mpcium source code, confidentiality already guarantees authenticity in this model—only the holder of the correct private key can decrypt the payload. This eliminates the overhead of additional signatures while ensuring privacy for sensitive MPC protocol messages exchanged between specific parties.

Routing Logic and Implementation

Broadcast Pathway

The handleTssMessage function in pkg/mpc/session.go (lines 121‑135) processes broadcast sends by attaching a signature and publishing to the global topic:

// Broadcast implementation from session.go:121-135
if routing.IsBroadcast && len(routing.To) == 0 {
    signature, _ := s.identityStore.SignMessage(&tssMsg)
    tssMsg.Signature = signature
    msg, _ := types.MarshalTssMessage(&tssMsg)
    s.pubSub.Publish(s.topicComposer.ComposeBroadcastTopic(), msg)
}

Direct Peer-to-Peer Pathway

The same function handles direct routing (lines 48‑71) by iterating over recipients, encrypting payloads, and dispatching via the DirectMessaging interface from pkg/messaging/point2point.go:

// Direct implementation from session.go:48-71
msg, _ := types.MarshalTssMessage(&tssMsg)
selfID := partyIDToNodeID(s.selfPartyID)
for _, to := range routing.To {
    toNodeID := partyIDToNodeID(to)
    topic := s.topicComposer.ComposeDirectTopic(selfID, toNodeID)
    if selfID == toNodeID {
        s.direct.SendToSelf(topic, msg) // local delivery without encryption
    } else {
        cipher, _ := s.identityStore.EncryptMessage(msg, toNodeID)
        s.direct.SendToOther(topic, cipher) // remote delivery with encryption
    }
}

Local self-messages bypass encryption through SendToSelf, while remote messages use SendToOther with optional retry logic via SendToOtherWithRetry for reliability.

Topic Composition Strategy

Global Broadcast Subjects

The TopicComposer.ComposeBroadcastTopic() method generates a single subject pattern like mpc.<walletID>.broadcast, creating a unified namespace where all nodes listen for session-wide coordination messages.

Directed Subject Naming

For point-to-point communication, TopicComposer.ComposeDirectTopic(fromID, toID) constructs targeted subjects following the pattern mpc.direct:<fromID>:<toID>:<walletID>. This naming convention enables selective message routing where nodes subscribe only to topics matching their own ID as the recipient, filtering irrelevant traffic at the NATS subscription level.

Message Reception Flows

Subscribing to Broadcasts

Each MPC session establishes a broadcast subscription once via subscribeBroadcastAsync (lines 86‑92 in session.go). The handler receives raw NATS messages and immediately verifies signatures before forwarding to the TSS state machine:

func (s *session) subscribeBroadcastAsync() {
    go func() {
        topic := s.topicComposer.ComposeBroadcastTopic()
        sub, err := s.pubSub.Subscribe(topic, func(natMsg *nats.Msg) {
            s.receiveBroadcastTssMessage(natMsg.Data) // verifies signature
        })
        s.broadcastSub = sub
    }()
}

Handling Direct Messages

Nodes listen on direct subjects through subscribeDirectTopicAsync (lines 64‑70), which registers handlers that invoke receiveP2PTssMessage. This function extracts the sender ID from the topic string using extractSenderIDFromDirectTopic (lines 85‑90), decrypts payloads using identityStore.DecryptMessage (unless the message originated from self), and processes the unmarshaled TssMessage:

func (s *session) receiveP2PTssMessage(topic string, cipher []byte) {
    senderID := extractSenderIDFromDirectTopic(topic)
    var plaintext []byte
    if senderID == partyIDToNodeID(s.selfPartyID) {
        plaintext = cipher // self-message, no decryption needed
    } else {
        plaintext, _ = s.identityStore.DecryptMessage(cipher, senderID)
    }
    msg, _ := types.UnmarshalTssMessage(plaintext)
    s.receiveTssMessage(msg)
}

Error Handling and Reliability

Broadcast routing aborts message processing immediately if signature verification fails or unmarshaling errors occur, treating these as fatal protocol violations. Direct routing handles decryption failures by producing errors that cause message discard, while transport-level retries are managed through SendToOtherWithRetry to handle transient network failures between specific peers.

Summary

  • Broadcast routing uses NATS pub/sub on shared topics with signature-based authentication, suitable for global coordination where all parties require message visibility.
  • Direct peer-to-peer routing employs NATS point-to-point channels with encryption-based confidentiality, routing messages through recipient-specific subjects constructed via TopicComposer.ComposeDirectTopic.
  • The routing decision hinges on the routing.IsBroadcast flag and the length of routing.To, implemented centrally in pkg/mpc/session.go.
  • Security models differ fundamentally: broadcasts rely on digital signatures for integrity, while direct messages rely on encryption for both privacy and authenticity.
  • Error handling distinguishes between signature verification failures (broadcast) and decryption failures (direct), with the latter supporting retry mechanisms for enhanced reliability.

Frequently Asked Questions

Does mpcium encrypt broadcast messages?

No, broadcast messages are not encrypted. They are signed using the sender's identity key via identityStore.SignMessage, but transmitted in plaintext since they must be readable by all session participants. Authentication relies entirely on signature verification through identityStore.VerifyMessage upon receipt.

How does mpcium handle messages sent to the same node?

When the sender and recipient IDs match in direct routing, mpcium invokes direct.SendToSelf (as seen in session.go lines 58‑60). This local delivery path skips encryption and decryption overhead, passing the marshaled message directly to the local handler without traversing the network layer.

What NATS subject patterns does mpcium use for routing?

Broadcast messages use subjects generated by TopicComposer.ComposeBroadcastTopic(), following the pattern mpc.<walletID>.broadcast. Direct messages use TopicComposer.ComposeDirectTopic(fromID, toID), creating subjects like mpc.direct:<fromID>:<toID>:<walletID>, ensuring unique routing paths between specific node pairs.

What happens if a direct message fails to decrypt?

Decryption failures in direct messaging return errors that cause the message to be discarded without processing. Unlike broadcast failures—which typically abort the session—direct message errors are isolated to the specific communication channel. The system supports reliability through SendToOtherWithRetry, which handles transient failures and network timeouts for peer-to-peer transmissions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →