How Errors Are Propagated and Handled in Distributed MPC Sessions

Errors in mpcium's distributed MPC sessions propagate through dedicated Go channels, allowing non-blocking failure detection while centralized event consumers transform low-level faults into structured error events for downstream handling.

In the mpcium open-source framework, every Multi-Party Computation (MPC) session—whether for key generation, signing, or ECDH exchange—runs inside its own Go struct. Rather than returning errors directly via function calls, the system adopts a channel-centric error propagation model. This ensures that failures in cryptographic operations, network message handling, or TSS (Threshold Signature Scheme) round processing surface asynchronously without blocking the main execution flow.

The Channel-Centric Error Propagation Model

Each session struct declares a dedicated error channel at construction time. In pkg/mpc/session.go, the base session struct defines:

type session struct {
    ...
    ErrCh    chan error      // 【session.go#L61-L62】
}

Sessions expose this channel through the ErrChan() method, allowing callers to select on it:

func (s *session) ErrChan() <-chan error { // 【session.go#L333】
    return s.ErrCh
}

When any internal component encounters a failure, it pushes the error onto s.ErrCh rather than returning it. This pattern appears consistently across TSS message handlers, P2P decryption routines, and broadcast verification logic.

Internal Error Sources and Propagation Points

TSS Message Handling Failures

During signing or key generation, the handleTssMessage method in pkg/mpc/session.go captures cryptographic failures and forwards them to the error channel:

s.ErrCh <- fmt.Errorf("failed to sign message: %w", err) // 【session.go#L124-L130】

Similarly, round-specific processing errors in receiveTssMessage are wrapped and sent:

s.ErrCh <- errors.Wrap(err, "Broken TSS Share") // 【session.go#L228-L229】

P2P Decryption and Broadcast Verification

When decrypting peer-to-peer messages or verifying broadcast signatures, failures indicate potential tampering. The session pushes these errors immediately:

// P2P decryption failure
s.ErrCh <- fmt.Errorf("failed to decrypt message: %w, tampered message", err) // 【session.go#L188-L190】

// Broadcast signature verification failure
s.ErrCh <- fmt.Errorf("Failed to verify message: %w, tampered message", err) // 【session.go#L212-L213】

ECDH Key Exchange Errors

The ecdhSession in pkg/mpc/key_exchange_session.go maintains its own errCh channel. Signature verification failures, key derivation errors, or HKDF failures are sent via:

e.errCh <- err // 【key_exchange_session.go#L107-L110】

The channel is declared at struct initialization:

errCh    chan error // 【key_exchange_session.go#L74-L77】

Consuming Errors at the Event Consumer Layer

The high-level eventConsumer in pkg/eventconsumer/event_consumer.go centralizes error handling. After creating a session, it launches a goroutine that selects on both the context cancellation and the session's error channel:

go func() {
    defer wg.Done()
    select {
    case <-ctxEcdsa.Done():
        successEvent.ECDSAPubKey = ecdsaSession.GetPubKeyResult()
    case err := <-ecdsaSession.ErrChan(): // 【event_consumer.go#L96-L108】
        logger.Error("ECDSA keygen session error", err)
        ec.handleKeygenSessionError(walletID, err, "ECDSA keygen session error", natMsg)
        errorChan <- err
        doneEcdsa()
    }
}()

When an error arrives, the consumer performs three actions:

  1. Logs the error via logger.Error.
  2. Transforms the error into a structured KeygenResultEvent using handleKeygenSessionError.
  3. Forwards the error to a central errorChan for system-wide coordination.

Structured Error Event Construction

The helper method handleKeygenSessionError converts raw errors into domain-specific events:

func (ec *eventConsumer) handleKeygenSessionError(walletID string, err error,
    contextMsg string, natMsg *nats.Msg) {
    fullErrMsg := fmt.Sprintf("%s: %v", contextMsg, err)          // 【event_consumer.go#L77-L78】
    errorCode := event.GetErrorCodeFromError(err)                // 【event_consumer.go#L80】
    keygenResult := event.KeygenResultEvent{
        ResultType:  event.ResultTypeError,
        ErrorCode:   string(errorCode),
        WalletID:    walletID,
        ErrorReason: fullErrMsg,
    }                                                          // 【event_consumer.go#L81-L86】
    // marshal & enqueue omitted for brevity
}

This ensures that low-level failures (e.g., "failed to decrypt message") become high-level events with error codes, wallet IDs, and full context strings.

Registry-Level Error Handling for ECDH Sessions

The registry in pkg/mpc/registry.go manages peer connections and watches the ECDH session's error channel separately:

for err := range r.ecdhSession.ErrChan() { // 【registry.go#L410-L413】
    logger.Error("ECDH error", err)
}

Unlike key generation or signing sessions, ECDH errors are treated as recoverable connection failures. The registry logs the error and relies on triggerECDHExchange to re-establish the secure channel, ensuring the distributed MPC network remains available despite transient cryptographic failures.

Practical Implementation Example

The following pattern demonstrates how to integrate error handling when initiating a distributed MPC session:

// 1️⃣ Create a signing session (any concrete implementation)
sess, err := node.CreateSigningSession(mpc.SessionTypeEDDSA, walletID, threshold, version, resultQueue)
if err != nil {
    // Creation errors are immediate – handle them directly
    log.Fatalf("session creation failed: %v", err)
}

// 2️⃣ Start listening for inbound messages (broadcast + P2P)
sess.ListenToIncomingMessageAsync()

// 3️⃣ Launch a goroutine that watches the session's error channel
go func() {
    for err := range sess.ErrChan() {
        // Centralised error handling
        logger.Error("signing session error", err)

        // Convert to a high‑level event (similar to handleKeygenSessionError)
        // … enqueue a failure event, clean up resources, etc.
    }
}()

// 4️⃣ Kick‑off the protocol
if err := sess.Init(tx); err != nil {
    logger.Error("failed to initialize signing session", err)
    // optional: abort early
}

This implementation mirrors the production flow in event_consumer.go and guarantees that any failure—whether from TSS processing, network decryption, or signature verification—is captured without blocking the main execution flow.

Summary

  • Channel-based propagation is the core mechanism: every MPC session owns an ErrCh that receives errors from internal components.
  • Exposed via ErrChan(): Sessions return a read-only channel interface, allowing callers to select or range over errors without accessing internal state.
  • Centralized consumption: The eventConsumer listens on ErrChan(), logs failures, and transforms them into structured KeygenResultEvent objects with error codes and wallet IDs.
  • Recovery without blocking: The registry handles ECDH errors as recoverable events, logging them and triggering re-exchange rather than terminating the session.
  • Consistent wrapping: All errors use pkg/common/errors for contextual wrapping, ensuring stack traces and error codes propagate correctly through the distributed system.

Frequently Asked Questions

How does mpcium ensure non-blocking error propagation in distributed MPC sessions?

mpcium implements a channel-centric error model where each session maintains a dedicated ErrCh field. Internal components—such as TSS message handlers in pkg/mpc/session.go or ECDH logic in pkg/mpc/key_exchange_session.go—push errors onto this channel using s.ErrCh <- err. The session exposes a read-only view via ErrChan(), allowing the eventConsumer to use Go's select statement to listen for errors concurrently without blocking the main protocol execution.

What happens when an ECDH key exchange fails in mpcium?

When the ecdhSession encounters a signature verification or key derivation error, it sends the error to its internal errCh channel. The registry in pkg/mpc/registry.go listens on this channel via for err := range r.ecdhSession.ErrChan(). Rather than terminating the system, the registry logs the error and treats it as a recoverable failure. The session can then be re-triggered via triggerECDHExchange, allowing the distributed MPC network to re-establish secure channels without service interruption.

How are low-level TSS errors converted to high-level events?

Low-level failures—such as decryption errors in receiveP2PTssMessage or signature verification failures in receiveBroadcastTssMessage—are pushed onto the session's ErrCh. The eventConsumer captures these via ecdsaSession.ErrChan() or eddsaSession.ErrChan(). It then invokes handleKeygenSessionError, which constructs a KeygenResultEvent struct containing the ResultTypeError constant, a mapped error code from event.GetErrorCodeFromError(err), the wallet ID, and the full error message. This structured event is then enqueued for downstream API or UI consumption.

Where are error channels defined in the mpcium source code?

Error channels are defined in the core session structs. The base session struct in pkg/mpc/session.go declares ErrCh chan error at line 61. The ECDH-specific session in pkg/mpc/key_exchange_session.go declares its own errCh chan error within the ecdhSession struct at lines 74-77. Both channels are initialized during session construction and exposed via public methods (ErrChan()) to enable external monitoring.

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 →