Skip to content

MPC Signing Architecture

Multi-Party Computation (MPC) threshold signing for enhanced key security.


Overview

MPC signing eliminates the single point of failure in traditional key management. Instead of one party holding the complete private key, the key is split into shares distributed between the client device and a server-side worker. Signatures are produced through a cryptographic protocol that never reconstructs the full key.

┌─────────────────────────────────────────────────────────────────┐
│                     Traditional vs MPC Signing                    │
│                                                                   │
│  Traditional:                                                     │
│  ┌────────────────────┐                                          │
│  │ Full Private Key   │ → sign(message) → Signature              │
│  │ (Single point of   │                                          │
│  │  failure)           │                                          │
│  └────────────────────┘                                          │
│                                                                   │
│  MPC (Stratos Vault):                                            │
│  ┌──────────────┐  ┌──────────────┐                              │
│  │ Client Share │  │ Server Share │                               │
│  │ (x1 / f(1)) │  │ (x2 / f(2)) │                               │
│  └──────┬───────┘  └──────┬───────┘                              │
│         │    Protocol      │                                      │
│         └──────┬───────────┘                                      │
│                ▼                                                   │
│          Signature                                                │
│    (Key never reconstructed)                                     │
└─────────────────────────────────────────────────────────────────┘

How It Works

Key Generation (Distributed Key Generation)

During wallet registration with MPC enabled, key shares are generated using Distributed Key Generation (DKG) — each party independently generates their own share. The full private key never exists in memory on any machine, not even temporarily.

Secp256k1 DKG (EVM, BTC, TRON)

Registration Flow (2 round-trips):

Client                                          Server (Durable Object)
  │                                                │
  │  1. POST /api/mpc/dkg-init { mpcKeyId }        │
  │───────────────────────────────────────────────►│
  │                                                │  Generate x2 = random scalar
  │                                                │  Q2 = x2 * G
  │                                                │  Generate Paillier keypair (p, q)
  │                                                │  cKey = Enc(paillierPk, x2)
  │  2. { Q2Hex, paillierPk, cKeyHex }            │  Store x2 + paillierSk
  │◄───────────────────────────────────────────────│
  │                                                │
  │  3. Generate x1 = random scalar                │
  │     Joint public key Q = x1 * Q2               │
  │     Derive address from Q                      │
  │     Encrypt x1 with PRF → store in D1          │
  │                                                │
  │  4. POST /api/mpc/dkg-complete { publicKeyHex } │
  │───────────────────────────────────────────────►│
  │                                                │  Store joint public key

The multiplicative relationship sk = x1 * x2 mod N is established without computing it. The joint public key Q = (x1 * x2) * G is derived via EC point multiplication: x1 * (x2 * G).

Ed25519 DKG (SVM, TON)

FROST uses Feldman Verifiable Secret Sharing — a polynomial generates shares directly:

Client generates polynomial f(x) = a0 + a1*x:
  share1 = f(1) = a0 + a1      → encrypted with PRF, stored in D1
  share2 = f(2) = a0 + 2*a1    → sent to server DO
  Commitments C0 = a0*G, C1 = a1*G
  a0 and a1 zeroed immediately

The secret a0 is never materialized as a standalone value after share computation.

Signing Protocol

When a transaction needs to be signed:

  1. User authenticates with WebAuthn passkey
  2. PRF extension decrypts the client share
  3. Client and server execute a multi-round signing protocol
  4. The combined output is a standard signature (indistinguishable from single-party)
  5. All nonces and session data are zeroed immediately

Lindell 2PC-ECDSA (secp256k1)

Used for Ethereum, Bitcoin, TRON, and other secp256k1-based chains.

Key Shares (Multiplicative)

Using DKG, each party generates their own share independently:

Server generates: x2 = random, Q2 = x2 * G
Client generates: x1 = random
Joint public key: Q = x1 * Q2 = (x1 * x2) * G

The full key x1 * x2 is NEVER computed.
Each party only knows their own share.

Paillier Encryption

The protocol uses Paillier homomorphic encryption to enable computation on encrypted values:

  • Client generates a Paillier keypair during keygen
  • Paillier public key is stored with client data
  • Paillier secret key components (lambda, mu, n, n2) are stored server-side
  • Homomorphic properties allow the server to compute on encrypted shares

Signing Protocol (3 Rounds)

Client                               Server
  │                                     │
  │  Round 1: Nonce Exchange            │
  │  k1 = random()                      │
  │  R1 = k1 * G                        │
  │  ────── R1 ──────────────────────>  │
  │                                     │  k2 = random()
  │                                     │  R2 = k2 * G
  │  <─────── R2, sessionId ──────────  │
  │                                     │
  │  Round 2: Partial Signature         │
  │  R = k1 * R2 (combined nonce)      │
  │  r = R.x mod n                     │
  │  c3 = Enc(pk, k1^(-1)*m + ...)    │
  │  ────── c3 ──────────────────────>  │
  │                                     │
  │  Round 3: Final Signature           │
  │                                     │  Decrypt c3
  │                                     │  Compute s = k2^(-1) * ...
  │  <─────── (r, s, v) ─────────────  │
  │                                     │
  │  Valid ECDSA signature              │

FROST 2-of-2 (ed25519)

Used for Solana, TON, and other ed25519-based chains. Implements RFC 9591.

Key Splitting (Feldman VSS)

The private key is shared using a polynomial:

Polynomial: f(x) = a0 + a1 * x (degree 1 for 2-of-2)
  where a0 = secret key

Client share: f(1) = a0 + a1
Server share: f(2) = a0 + 2*a1

Commitments: C0 = a0 * G, C1 = a1 * G (for verification)
Public key:  C0 (= a0 * G, standard ed25519 public key)

Signing Protocol (2 Rounds)

Client                               Server
  │                                     │
  │  Round 1: Nonce Commitments         │
  │  d1, e1 = random nonces            │
  │  D1 = d1*G, E1 = e1*G             │
  │  ────── D1, E1 ─────────────────>  │
  │                                     │  d2, e2 = random nonces
  │                                     │  D2 = d2*G, E2 = e2*G
  │  <─────── D2, E2, sessionId ─────  │
  │                                     │
  │  Round 2: Partial Signatures        │
  │  rho = H(message, D1+D2, E1+E2)   │
  │  R = (D1+D2) + rho*(E1+E2)        │
  │  c = H(R, PublicKey, message)      │
  │  z1 = d1 + rho*e1 + c*L1*f(1)    │
  │  ────── z1, message ─────────────>  │
  │                                     │  z2 = d2 + rho*e2 + c*L2*f(2)
  │                                     │  z = z1 + z2
  │                                     │  signature = (R, z)
  │  <─────── signature ─────────────  │
  │                                     │
  │  Valid ed25519 signature            │

Lagrange coefficients L1 and L2 ensure correct threshold reconstruction.


Server Architecture

Two-System Separation

The portal and MPC signer run on independent systems (separate Cloudflare accounts, separate infrastructure, separate administrators). Communication is over authenticated HTTPS — not a service binding. This ensures no single system compromise exposes both key halves.

  System A (Portal)                    System B (MPC Signer)
  ┌──────────────────────┐             ┌──────────────────────┐
  │  Cloudflare Pages    │   HTTPS +   │  Cloudflare Worker   │
  │  + D1 Database       │─────────────│  + Durable Objects   │
  │                      │  X-MPC-Auth │                      │
  │  Holds: encrypted    │   header    │  Holds: server       │
  │  client shares       │             │  shares (plaintext)  │
  └──────────────────────┘             └──────────────────────┘

Configuration is set in the admin panel under Configuration → Security:

SettingDescription
MPC_SIGNER_URLExternal URL of the MPC signer worker
MPC_AUTH_SECRETShared secret (must match on both systems)

MPC Signer Worker

The server side is a Cloudflare Worker with Durable Objects. Authentication via X-MPC-Auth header is required — the worker rejects all requests if MPC_AUTH_SECRET is not configured:

┌─────────────────────────────────────────────────────────────┐
│                    MPC Signer Worker                          │
│                                                               │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │                  Router                                   │ │
│  │  POST /dkg-init       → DKG round 1 (server generates)  │ │
│  │  POST /dkg-complete   → DKG round 2 (store pubkey)      │ │
│  │  POST /keygen         → Store server share (FROST)       │ │
│  │  POST /sign-init      → Signing round 1 (nonce exchange)│ │
│  │  POST /sign-round2    → Signing round 2+ (signature)    │ │
│  │  POST /export         → Export key data (for backup)     │ │
│  │  POST /import         → Import key data (from backup)    │ │
│  │  POST /backup-export  → Bulk export (multiple keys)      │ │
│  │  POST /backup-restore → Bulk import (multiple keys)      │ │
│  │  POST /status         → Diagnostic (has key?)            │ │
│  │  POST /delete         → Remove all key data              │ │
│  └──────────────────────────┬──────────────────────────────┘ │
│                              │                                │
│  ┌──────────────────────────▼──────────────────────────────┐ │
│  │              MpcKeyStore (Durable Object)                │ │
│  │                                                           │ │
│  │  storage:                                                 │ │
│  │    protocol: 'secp256k1' | 'ed25519'                     │ │
│  │    keyData:  JSON(server share + crypto params)           │ │
│  │                                                           │ │
│  │  in-memory:                                               │ │
│  │    sessions: Map<sessionId, SigningSession>               │ │
│  │    (auto-expire after 30s, one-time use)                 │ │
│  └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Session Security

  • Each signing session gets a unique sessionId (UUID)
  • Sessions expire after 30 seconds
  • Sessions are one-time use - deleted immediately after signing
  • Nonces in expired sessions are explicitly zeroed
  • Worker-to-worker authentication via MPC_AUTH_SECRET

API Reference

DKG Init (secp256k1)

Starts distributed key generation. Server generates its share, returns public data.

POST /api/mpc/dkg-init
Authorization: Bearer <session_token>

Request:  { "mpcKeyId": "user_evm_uuid" }

Response: {
  "success": true,
  "data": {
    "Q2Hex": "02ab...",       // Server's public point (compressed)
    "paillierPk": "c3f...",   // Paillier public key N
    "cKeyHex": "8a1..."       // Enc(pk, x2) for signing protocol
  }
}

DKG Complete (secp256k1)

Finalizes DKG. Client sends the derived joint public key.

POST /api/mpc/dkg-complete
Authorization: Bearer <session_token>

Request:  { "mpcKeyId": "user_evm_uuid", "publicKeyHex": "02cd..." }
Response: { "success": true }

Key Generation (FROST / ed25519)

Stores server share for ed25519 chains (FROST already uses proper DKG client-side).

POST /api/mpc/keygen
Authorization: Bearer <session_token>

{
  "protocol": "ed25519",
  "mpcKeyId": "unique-key-identifier",
  "share2": "...", "commitmentC0": "...", "commitmentC1": "...",
  "publicKeyHex": "..."
}

Sign Init (Round 1)

POST /api/mpc/sign-init
Authorization: Bearer <session_token>

// secp256k1:
{ "protocol": "secp256k1", "mpcKeyId": "...", "clientR1Hex": "compressed-point-hex" }

// ed25519:
{ "protocol": "ed25519", "mpcKeyId": "...", "clientD1Hex": "...", "clientE1Hex": "..." }

Sign Round 2

POST /api/mpc/sign-round2
Authorization: Bearer <session_token>

// secp256k1:
{ "protocol": "secp256k1", "mpcKeyId": "...", "sessionId": "...", "ciphertext": "paillier-hex" }

// ed25519:
{ "protocol": "ed25519", "mpcKeyId": "...", "sessionId": "...", "clientZ1Hex": "...", "messageHex": "..." }

Migration (Full Key → MPC)

POST /api/mpc/migrate
Authorization: Bearer <session_token>

{ "chainType": "evm", "encryptedClientShare": "prf-encrypted-share", "mpcKeyId": "..." }

Portal Backup (Superadmin)

Exports D1 database tables only (encrypted client shares, user data, config). Available via admin panel Configuration section.

GET /api/superadmin/backup
X-Superadmin-Token: <token>

Response: { version, type: "portal", timestamp, database: { table: rows[] } }

MPC Signer Backup (Superadmin + IP Whitelist)

Exports Durable Object server shares only. Requires caller IP to be in MPC_BACKUP_IP_WHITELIST (empty whitelist = allow all). Available via admin panel Configuration section.

GET /api/superadmin/backup-mpc
X-Superadmin-Token: <token>

Response: { version, type: "mpc-signer", timestamp, mpcKeys: [...] }

The two backup files should be stored separately by different custodians. Neither file alone can reconstruct a private key.

Portal Restore (Superadmin)

POST /api/superadmin/restore?mode=merge|clean
X-Superadmin-Token: <token>
Body: portal backup JSON (rejects MPC backup files)

MPC Signer Restore (Superadmin + IP Whitelist)

POST /api/superadmin/restore-mpc
X-Superadmin-Token: <token>
Body: MPC backup JSON (rejects portal backup files)

MPC Backup IP Whitelist

GET  /api/superadmin/mpc-whitelist  → { whitelist, yourIp, yourIpWhitelisted }
PUT  /api/superadmin/mpc-whitelist  → { whitelist: ["1.2.3.4", ...] }

Only IPs currently in the whitelist can modify it. Empty whitelist allows all IPs.

Status (Superadmin)

POST /api/mpc/status
X-Superadmin-Token: <token>
{ "mpcKeyId": "..." }

Database Schema

MPC adds two columns to the wallet_addresses table:

sql
key_type TEXT DEFAULT 'full'  -- 'full' = PRF-encrypted full key
                              -- 'mpc_share' = PRF-encrypted client share

mpc_key_id TEXT               -- Durable Object name ID for server share lookup

Configuration

Enabling MPC

  1. Deploy the MPC Signer worker (stratos-mpc-signer) on a separate Cloudflare account or separate infrastructure
  2. Set MPC_AUTH_SECRET on the signer worker:
bash
echo "your-shared-secret" | wrangler secret put MPC_AUTH_SECRET --name stratos-mpc-signer
  1. In the portal admin panel, go to Configuration → Security and set:
SettingValue
MPC SigningEnabled
MPC Signer URLhttps://your-mpc-signer.workers.dev
MPC Auth SecretSame secret set on the signer

No service binding or wrangler.toml changes needed — communication is over external HTTPS.

Admin Panel

The admin panel (Configuration section) provides:

  • MPC toggle — Enable/disable MPC for new registrations
  • MPC Signer URL / Auth Secret — Configure portal-to-signer connection
  • Portal Backup — Download/restore D1 database (encrypted client shares)
  • MPC Signer Backup — Download/restore server shares (IP-whitelisted)
  • IP Whitelist — Manage which IPs can access MPC backup endpoints

Transparent Integration

MPC signing is transparent to dock apps and SDK consumers. The SigningContext determines the mode:

typescript
// The sign adapter dispatches automatically
// Dock apps call the same SDK methods regardless of mode

// Local mode:
signSecp256k1(hash, key, { mode: 'local', privateKeyHex: '...' })

// MPC mode:
signSecp256k1(hash, key, { mode: 'mpc', mpcClient, clientKeyData: '...' })

// Both produce identical ECDSA/EdDSA signatures

All chain signers (EVM, Solana, TON, TRON, Bitcoin) use the sign adapter, making MPC a zero-change upgrade for application developers.



Backup & Restore

MPC key data is distributed across two stores — both must be backed up together:

StoreContainsBacked Up By
D1 DatabasePRF-encrypted client shares (wallet_addresses, passkey_encrypted_keys)GET /api/superadmin/backup
Durable ObjectsServer shares (x2/share2 + Paillier secret keys)Same endpoint (discovers mpcKeyIds from D1, exports each DO)

Three Dependencies for Signing

All three must align for a valid signature:

  1. User's passkey — hardware-bound, non-exportable, domain-locked via RP ID
  2. D1 database — holds PRF-encrypted client share
  3. Durable Object — holds server share

If any one is lost, signing fails. The backup/restore system protects against loss of #2 and #3.

Domain Migration Warning

The passkey PRF output is bound to the RP ID (domain). Changing the domain means:

  • Existing passkeys won't authenticate on the new domain
  • PRF output changes, making encrypted client shares unreadable
  • A re-keying migration flow is required with both domains live simultaneously

Prevention: Set RP ID to the registrable domain (e.g., cantondefi.com) rather than a subdomain. This allows subdomain changes without breaking passkeys.

Backup Schedule Recommendation

FrequencyTrigger
DailyAutomated cron backup to encrypted storage
After user registrationNew MPC keys created
Before infrastructure changesDomain, account, or worker migrations

The backup file contains server shares in plaintext — store it encrypted at rest with restricted access.


Next Steps

Enterprise-grade multi-chain wallet infrastructure.