How NATS Pub/Sub Integrates with the TSS Protocol in mpcium

mpcium runs threshold-signature sessions (TSS) over a cluster of nodes that communicate exclusively through NATS, using a three-layer architecture that encodes cryptographic shares into signed messages for broadcast pub/sub and encrypted payloads for point-to-point request-reply patterns.

mpcium is an open-source threshold signature scheme implementation that relies on NATS as its sole network transport. By abstracting all peer-to-peer communication behind NATS pub/sub subjects and request-reply patterns, the repository decouples complex cryptographic state machines from networking concerns. This integration allows TSS protocol rounds to flow across distributed nodes while maintaining strict authentication and confidentiality through signatures and encryption applied before messages ever reach the wire.

The Three-Layer Architecture

The integration is engineered around distinct responsibilities that separate concerns:

  • Message format layer (pkg/types/tss.go): Defines TssMessage structures with wallet IDs, routing metadata, and optional signatures. Functions NewTssMessage, MarshalTssMessage, and UnmarshalTssMessage handle wire encoding.

  • Transport abstraction layer (pkg/messaging/pubsub.go and pkg/messaging/point2point.go): Hides NATS details behind generic interfaces. PubSub provides broadcast capabilities via Publish and Subscribe, while DirectMessaging handles point-to-point SendToOther operations using NATS request-reply.

  • Session orchestration layer (pkg/mpc/session.go): Bridges the TSS library callbacks to NATS publishes and converts incoming NATS messages back into tss.Party updates through functions like handleTssMessage, receiveBroadcastTssMessage, and receiveP2PTssMessage.

Encoding and Securing TSS Shares

Before any data leaves the node, the session constructs a TssMessage in pkg/types/tss.go. This struct encapsulates the raw TSS bytes along with routing information indicating whether the message is a broadcast and which parties are involved.

For broadcast messages, mpcium attaches a cryptographic signature to prove origin authenticity. The identityStore provides SignMessage and VerifyMessage operations used immediately before marshalling:

// pkg/mpc/session.go – lines 95-135 (simplified)
tssMsg := types.NewTssMessage(s.walletID, data, routing.IsBroadcast, routing.From, routing.To)

if routing.IsBroadcast && len(routing.To) == 0 {
    signature, _ := s.identityStore.SignMessage(&tssMsg)
    tssMsg.Signature = signature
    payload, _ := types.MarshalTssMessage(&tssMsg)
    s.pubSub.Publish(s.topicComposer.ComposeBroadcastTopic(), payload)
}

For point-to-point messages, the payload is encrypted for the specific recipient using identityStore.EncryptMessage before transmission, ensuring only the intended peer can decrypt the share.

Broadcasting Shares via NATS Pub/Sub

Publishing Broadcast Messages

Broadcast dissemination relies on the PubSub interface implemented in pkg/messaging/pubsub.go. When the session publishes, it delegates to a thin NATS wrapper:

// pkg/messaging/pubsub.go – lines 34-38
func (n *natsPubSub) Publish(topic string, message []byte) error {
    logger.Debug("[NATS] Publishing message", "topic", topic)
    return n.natsConn.Publish(topic, message)
}

The topic is dynamically composed (e.g., mpc.tss.broadcast:<walletID>) via the TopicComposer.

Subscribing to Broadcast Topics

On the receiving side, session.go subscribes asynchronously:

// pkg/mpc/session.go – inside subscribeBroadcastAsync
sub, err := s.pubSub.Subscribe(topic, func(natMsg *nats.Msg) {
    s.receiveBroadcastTssMessage(natMsg.Data)
})

The handler receiveBroadcastTssMessage unmarshals the payload, verifies the attached signature using identityStore.VerifyMessage, and forwards the verified share to receiveTssMessage, which ultimately feeds the TSS library:

// pkg/mpc/session.go – lines 200-216 (excerpt)
msg, _ := types.UnmarshalTssMessage(rawMsg)
s.identityStore.VerifyMessage(msg)  // signature check
s.receiveTssMessage(msg)            // bridges to TSS party

Point-to-Point Communication via NATS Request-Reply

Sending Direct Messages

Non-broadcast TSS shares require direct node-to-node communication. The session encrypts the message for the destination node ID, then invokes the direct messaging abstraction:

// pkg/mpc/session.go – lines 148-167 (simplified)
cipher, _ := s.identityStore.EncryptMessage(msg, toNodeID)
s.direct.SendToOther(topic, cipher)

The implementation in pkg/messaging/point2point.go leverages NATS's built-in request-reply pattern with a hard timeout:

// pkg/messaging/point2point.go – lines 42-58 (excerpt)
func (d *natsDirectMessaging) SendToOther(topic string, message []byte) error {
    _, err := d.natsConn.Request(topic, message, 3*time.Second)
    return err
}

Receiving and Decrypting Direct Messages

Incoming direct messages are handled by receiveP2PTssMessage, which first decrypts the ciphertext (unless the message originated from the local node), unmarshals the payload, and processes it through the same receiveTssMessage pipeline used for broadcasts.

Bridging Messages into the TSS Library

The receiveTssMessage function in pkg/mpc/session.go (lines 226-260) serves as the final bridge between NATS transport and the underlying TSS state machine. It determines the appropriate cryptographic round via getRoundFunc and updates the local party state:

// pkg/mpc/session.go – lines 226-260 (excerpt)
round, _ := s.getRoundFunc(msg.MsgBytes, s.selfPartyID, msg.IsBroadcast)
s.party.UpdateFromBytes(msg.MsgBytes, msg.From, msg.IsBroadcast)

This call to party.UpdateFromBytes transitions the TSS protocol state, completing the round-trip from TSS library → NATS wire → remote peer → TSS library update.

Summary

  • Three-layer design: mpcium separates message formatting (pkg/types/tss.go), NATS transport (pkg/messaging/), and session orchestration (pkg/mpc/session.go) to keep cryptographic logic isolated from networking code.

  • Dual transport modes: Broadcast shares use NATS pub/sub with attached signatures for authenticity, while point-to-point shares use NATS request-reply with payload encryption for confidentiality.

  • Immutable entry point: All network traffic eventually flows through session.receiveTssMessage, which validates signatures or decrypts payloads before passing bytes to tss.Party.UpdateFromBytes.

  • Testable abstractions: The PubSub and DirectMessaging interfaces allow the TSS protocol to be tested without running a live NATS server, as the concrete implementations merely wrap natsConn.Publish and natsConn.Request.

Frequently Asked Questions

Why does mpcium use NATS for TSS communication instead of TCP or gRPC?

NATS provides out-of-the-box pub/sub routing, request-reply patterns, and subject-based addressing that map naturally to TSS broadcast and p2p rounds. According to the mpcium source code, using NATS eliminates the need for custom peer discovery and connection management, allowing the repository to focus strictly on cryptographic correctness while relying on NATS for scalable message delivery.

How are broadcast TSS messages authenticated?

Broadcast messages are signed before transmission using identityStore.SignMessage in pkg/mpc/session.go. Upon receipt, receiveBroadcastTssMessage calls identityStore.VerifyMessage to validate the signature against the sender's public key before the share ever touches the TSS library state machine.

What happens if a point-to-point NATS request times out?

The SendToOther implementation in pkg/messaging/point2point.go uses natsConn.Request with a three-second timeout. If the remote peer does not respond within this window, the function returns an error that propagates up to the session layer, which can then trigger protocol-level retries or abort the signing round depending on the TSS scheme's fault tolerance requirements.

Are TSS message payloads encrypted on the wire?

Point-to-point payloads are encrypted using identityStore.EncryptMessage before being passed to SendToOther, ensuring confidentiality between specific peers. Broadcast messages are not encrypted but carry cryptographic signatures to guarantee authenticity and integrity, which is sufficient for schemes where broadcast shares are designed to be public within the signing committee.

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 →