MPC Wallet Security

Fystack uses Multi-Party Computation (MPC) to eliminate single points of failure in private key management. No single party ever possesses a complete private key. Instead, key shares are distributed across multiple nodes, and signatures are computed collaboratively using threshold cryptography.

This document outlines how Fystack's MPC implementation protects wallet keys through distributed trust, secure communication, and verifiable computation.

Fystack MPC wallet security architecture

Node Responsibilities

Each node:

  • Stores only its encrypted share locally.
  • Participates in threshold signing using Ed25519 verification and ECDH-secured P2P communication.
  • Cannot reconstruct or export the full key, even if compromised.

Property

Description

Confidentiality

No single node or operator can access a full private key.

Integrity

All inter-node messages are authenticated via Ed25519 or Cloud KMS signatures.

Resilience

Supports n nodes with threshold t, tolerating up to (t-1) node failures.

Encryption at Rest

Key shares are stored locally with AES encryption.

Encryption in Transit

Enforced via NATS TLS plus an ECDH layer for P2P channels.


1. Overview

Multi-Party Computation enables multiple parties to jointly compute cryptographic operations without any single party having access to complete private keys. Fystack implements threshold signature schemes where a minimum number of parties (threshold) must cooperate to produce a valid signature.

Component

Description

MPC Nodes

Minimum of 3 nodes form a cluster.

Threshold (t)

Wallet remains secure even if up to (t-1) nodes are compromised.

Key Shares

Private key is mathematically split into multiple shares.

Share Encryption

Each share is encrypted using AES (BadgerDB default encryption).

Messaging Security

Dual-layer protection: NATS TLS plus ECDH key exchange for P2P encryption.

Message Authentication

Apex communicates with the MPC cluster using signed messages (Ed25519 or Cloud KMS).

Key Benefits

  • No single point of compromise: Compromise of one node does not expose private keys
  • Distributed trust: Multiple independent parties must cooperate
  • Transparent to blockchain: Output signatures are standard ECDSA/EdDSA signatures
  • Dynamic threshold: Can adjust signing requirements based on transaction value
  • Audit trail: Every signing session is logged and traceable

2. Distributed Key Generation (DKG)

Fystack uses Distributed Key Generation to create wallet keys without any party ever possessing the complete private key.

DKG Process

Phase 1: Commitment

  • Each party generates a random polynomial of degree t-1 (where t is the threshold)
  • Parties broadcast commitments to their polynomials
  • Commitments are verified using Feldman's VSS (Verifiable Secret Sharing)

Phase 2: Share Distribution

  • Each party evaluates their polynomial at specific points
  • Shares are sent securely to corresponding parties over encrypted channels
  • Each party receives one share from every other party

Phase 3: Verification

  • Parties verify received shares against broadcast commitments
  • Invalid shares are identified and complaints are filed
  • Protocol aborts if too many invalid shares are detected

Phase 4: Key Construction

  • Each party combines valid shares to form their key share
  • Public key is computed from the commitments
  • No party knows the complete private key

DKG Configuration

yaml
mpc_threshold: 2   # Number of MPC nodes required to sign

mpc_threshold represents the threshold t in an n-of-t scheme. With n active MPC nodes, the system enforces 2 ≤ t ≤ n, tolerating up to (t - 1) faulty or offline nodes while still producing signatures. For example, n = 3 with mpc_threshold: 2 yields a 2-of-3 cluster: any two authenticated nodes can sign, but compromising a single node is insufficient to recover the private key.

For GG18/GG20-style robust MPC protocols we additionally recommend t > n / 2, which guarantees Byzantine fault tolerance: even if f = n - t nodes are malicious, offline, or colluding, honest nodes remain a majority and can prevent bogus signatures or private-key reconstruction.

Security Properties

  • Robustness: Tolerates up to n-t malicious parties
  • Verifiability: All parties can verify correctness of shares
  • Privacy: No information about private key is leaked
  • Non-interactive: After setup, no further DKG required

3. Threshold Signing

Threshold signing allows a subset of parties (meeting the threshold) to collaboratively produce a valid signature without reconstructing the private key.

Signing Protocol (ECDSA)

Round 1: Commitment Phase

Commitment Generation

pseudo
// Each signer generates commitment
const k_i = randomScalar()
const gamma_i = randomScalar()
const commitment_i = {
  K_i: k_i * G,           // Ephemeral public key
  Gamma_i: gamma_i * G,   // Blinding factor
}
broadcastCommitment(commitment_i)

Round 2: Partial Signature Generation

Partial Signature

pseudo
// After receiving all commitments
const R = combineCommitments(allCommitments)
const r = R.x  // x-coordinate of combined point

// Each party computes partial signature
const s_i = k_i + r * keyShare_i * hash(message)
broadcastPartialSignature(s_i)

Round 3: Signature Aggregation

Signature Combination

pseudo
// Combine partial signatures
const s = lagrangeInterpolation(partialSignatures)
const signature = { r, s }

// Verify signature
const valid = ecdsaVerify(signature, publicKey, message)

Signing Workflow

Step

Action

Security Check

  1. Session Init

Create signing session with transaction details

Verify session ID uniqueness

  1. Party Selection

Select threshold number of parties to participate

Verify parties are online and authenticated

  1. Commitment Round

Exchange commitments to ephemeral keys

Validate commitment format and freshness

  1. Partial Signatures

Each party computes and shares partial signature

Verify partial signature validity

  1. Aggregation

Combine partial signatures into final signature

Verify final signature against public key

  1. Broadcast

Submit signed transaction to blockchain

Confirm transaction acceptance


4. Network Security

NATS Transport (TLS)

MPC nodes exchange traffic over the shared NATS bus declared in config.yaml. Production deployments require TLS endpoints plus pinned client certificates:

NATS TLS configuration

yaml
nats:
  url: tls://nats.example.com:4222
  username: ""
  password: ""
  tls:
    client_cert: "/etc/mpcium/certs/client-cert.pem"
    client_key: "/etc/mpcium/certs/client-key.pem"
    ca_cert: "/etc/mpcium/certs/rootCA.pem"

The deployments/systemd/setup-config.sh script refuses to run in production without the tls:// URL and the nats.tls.* paths above, ensuring every node speaks to NATS through an authenticated TLS tunnel.

Signed Message Bus

Before a TSS payload is published, the node signs it with its Ed25519 identity (see pkg/identity/fileStore):

Sign TSS messages

go
func (s *fileStore) SignMessage(msg *types.TssMessage) ([]byte, error) {
	msgBytes, err := msg.MarshalForSigning()
	if err != nil {
		return nil, fmt.Errorf("failed to marshal message for signing: %w", err)
	}
	signature := ed25519.Sign(s.privateKey, msgBytes)
	return signature, nil
}

Every node verifies the signature before accepting a message, preventing spoofed peers or replayed data:

Verify TSS messages

go
func (s *fileStore) VerifyMessage(msg *types.TssMessage) error {
	if msg.Signature == nil {
		return fmt.Errorf("message has no signature")
	}
	publicKey, err := s.GetPublicKey(partyIDToNodeID(msg.From))
	if err != nil {
		return fmt.Errorf("failed to get sender's public key: %w", err)
	}
	msgBytes, err := msg.MarshalForSigning()
	if err != nil {
		return fmt.Errorf("failed to marshal message for verification: %w", err)
	}
	if !ed25519.Verify(publicKey, msgBytes, msg.Signature) {
		return fmt.Errorf("invalid signature")
	}
	return nil
}

Point-to-Point Encryption

Nodes run an ECDH exchange over dedicated NATS topics (pkg/mpc/key_exchange_session.go) to derive symmetric keys per peer. Those keys drive AES-GCM encryption for every payload:

Encrypt TSS payloads

go
func (s *fileStore) EncryptMessage(plaintext []byte, peerID string) ([]byte, error) {
	key, err := s.GetSymmetricKey(peerID)
	if err != nil {
		return nil, err
	}
	if key == nil {
		return nil, fmt.Errorf("no symmetric key for peer %s", peerID)
	}
	return encryption.EncryptAESGCMWithNonceEmbed(plaintext, key)
}

ECDH Handshake Flow

The ECDH session (pkg/mpc/key_exchange_session.go) is layered on top of the NATS bus:

  1. Each node generates an ephemeral X25519 key pair and subscribes to the shared ecdh:exchange topic.
  2. Nodes broadcast their public key plus timestamp, signed with their Ed25519 identity (SignEcdhMessage).
  3. Listeners verify the signature, perform ECDH() using their own private key, and derive a symmetric key with HKDF-SHA256.
  4. The derived 32-byte key is cached per peer via identityStore.SetSymmetricKey and reused by the AES-GCM helpers above.

HKDF key derivation

go
func (e *ecdhSession) deriveSymmetricKey(secret []byte, peerID string) []byte {
	info := deriveConsistentInfo(e.nodeID, peerID) // stable ordering of IDs
	hkdfReader := hkdf.New(sha256.New, secret, nil, info)
	key := make([]byte, 32) // AES-256 key
	if _, err := hkdfReader.Read(key); err != nil {
		e.errCh <- err
		return nil
	}
	return key
}

If a peer disconnects, RemovePeer drops the cached symmetric key so a fresh handshake is required on reconnect, keeping channel keys ephemeral.

Network Isolation

Control

Implementation

Private Network

MPC nodes communicate over private VPC/VPN

Firewall Rules

Only MPC ports open, restricted to known node IPs

Rate Limiting

Maximum message rate per node to prevent DoS

Connection Monitoring

All connections logged and monitored for anomalies


5. Storage Security

Key shares persist inside BadgerDB. The MPC service refuses to open the database unless an encryption key (badger_password) is supplied, and that key is fed into Badger's AES-256 encryption via WithEncryptionKey(...) (pkg/kvstore/badger.go). Backups reuse the same encrypted format, so both the live DB files and snapshot archives remain unreadable without the password.

Badger encryption

go
opts := badger.DefaultOptions(config.DBPath).
  WithEncryptionKey(config.EncryptionKey).
  WithCompression(options.ZSTD)

db, err := badger.Open(opts)

6. Summary

Fystack's MPC implementation eliminates single points of failure through distributed trust, threshold cryptography, and secure multi-party protocols. By splitting private keys into shares and requiring collaborative signing, we ensure that no single compromise can lead to asset loss.

Key security features:

  • Distributed Key Generation with no single point of trust
  • Threshold signatures requiring multiple parties to cooperate
  • Encrypted communication with TLS + ECDH-derived AES-GCM payloads
  • BadgerDB encryption at rest using AES-256 with operator-supplied passwords
  • Verifiable computation to detect malicious parties