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.

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
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
// 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
// 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
// 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 |
|---|---|---|
| Create signing session with transaction details | Verify session ID uniqueness |
| Select threshold number of parties to participate | Verify parties are online and authenticated |
| Exchange commitments to ephemeral keys | Validate commitment format and freshness |
| Each party computes and shares partial signature | Verify partial signature validity |
| Combine partial signatures into final signature | Verify final signature against public key |
| 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
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
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
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
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:
- Each node generates an ephemeral X25519 key pair and subscribes to the shared
ecdh:exchangetopic. - Nodes broadcast their public key plus timestamp, signed with their Ed25519 identity (
SignEcdhMessage). - Listeners verify the signature, perform
ECDH()using their own private key, and derive a symmetric key with HKDF-SHA256. - The derived 32-byte key is cached per peer via
identityStore.SetSymmetricKeyand reused by the AES-GCM helpers above.
HKDF key derivation
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
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