
Encrypting the private key still leaves one decryptable key behind one server — the honeypot that MPC later removes. Your backend can decrypt every user's private key. So can your ENCRYPTION_KEY . So can whoever gets far enough into your servers: a leaked cloud token, a poisoned CI job, one unlucky RCE. You encrypted the keys. You feel safe. You've quietly built the biggest honeypot in the company. I know, because that's how we started: Wallet.createRandom() , AES-256-GCM, an encrypted blob in Postgres, one ENCRYPTION_KEY in the env. It ships fast and it passes the demo. It also means one stolen secret unlocks every user at once. That's the exact failure that drained Ronin (~$625M) and Harmony (~$100M). Nobody cracked the cryptography in either case. They found the one place that could. This article is the fix I wish I'd read first: MPC-based threshold signing, where the private key is never assembled anywhere. We'll cover what it actually changes, what it does not solve, which protocols and libraries are safe to build on in 2026, and how to migrate an existing "encrypted key in a database" system without a big-bang cutover. If you already run custodial Web3 infra and know how MPC-TSS works, skip to Section 4 (the protocol reality check) and Section 7 (one master wallet for a million users). That's where the real decisions live. Table of Contents The real problem — it's not encryption, it's centralization What MPC-TSS actually changes MPC in 60 seconds, for engineers The protocol reality check (2026) The open-source options: build your own vs. a turnkey server Licensing traps One master wallet for a million users (CKD) Who actually runs MPC in production Migrating from encrypted keys to MPC The mistakes teams make (and what MPC won't save you from) One strong primitive, not the whole system Section 1: The real problem — not encryption, but centralization Here's the custodial flow almost every team ships first. It works, and that's the trap: Generate a wallet, a secp256k1 private key Encrypt it with AES-256-GCM Store the encrypted blob in PostgreSQL Decrypt it in the backend whenever you need to sign This protects you against exactly one thing: a database-only leak. Steal the Postgres dump and nothing else, and all you have is ciphertext. But your backend has to decrypt to function. So an attacker who gets enough control of it, through RCE, stolen cloud credentials, a CI/CD compromise, or a bad insider, doesn't need to break any cryptography. They use your own signing path. The AES does nothing, because your server was always going to decrypt. This keeps draining real money, and it always looks the same: Ronin Bridge (2022, ~$625M): attackers got 5 of 9 validator keys. Control was too concentrated; a Lazarus phishing campaign was enough to reach them. Harmony Horizon Bridge (2022, ~$100M): a 2-of-5 multisig where the attacker collected just 2 keys and drained it. Nobody cracked ECDSA. They collected enough key material from too few places. So the goal isn't "encrypt the private key better." It's this: Remove the possibility that any single machine, process, or operator can unilaterally use the key. That's what multi-party computation gives you. Section 2: What MPC-TSS actually changes In threshold MPC signing (MPC-TSS), the private key is never assembled in one place. Not in memory, not on disk, not for a microsecond. The key is split into shares , each share lives on an independent node, and a threshold of nodes cooperates to produce a signature. The blockchain sees a completely normal ECDSA signature and can't tell the difference. The canonical setup is three nodes with a 2-of-3 threshold: Any two of three nodes can jointly sign. One node, or the backend, is never enough. Walk the attack scenarios and the difference is structural, not cosmetic: The database dump — Traditional (encrypted key): Ciphertext (safe, for now) · MPC 2-of-3: Nothing useful Database + encryption key — Traditional (encrypted key): All keys stolen · MPC 2-of-3: Nothing useful Full backend / RCE — Traditional (encrypted key): All keys stolen · MPC 2-of-3: No key exists to steal One MPC node — Traditional (encrypted key): — · MPC 2-of-3: Not enough to sign One disk snapshot of one node — Traditional (encrypted key): — · MPC 2-of-3: Not enough to sign One honest caveat on that last row: an attacker who owns the backend can't exfiltrate a key, because there isn't one. But they can still ask the cluster to sign while they hold the box. MPC removes key theft, not a hijacked signing path. We come back to that in Section 10. The backend holds zero private keys. When it needs a signature, it hashes the payload locally and sends a 32-byte digest to the cluster, which returns (r, s, v) . Own the application server completely, and you get nothing to steal, because the thing you came for was never there. Section 3: MPC in 60 seconds (for engineers) Two different things get called "MPC," and mixing them up causes half the confusion in this space: General MPC / SMPC — parties jointly compute a function over private inputs without revealing them. Banks scoring fraud together, hospitals running joint analytics, private ML. Threshold signature MPC (TSS) — the specialized case for key management and signing. Wallet infra lives in the second one: threshold ECDSA (Ethereum, Bitcoin, most EVM/L2 chains) or threshold EdDSA (Solana and friends). There are only two ceremonies to understand: DKG (distributed key generation). The nodes jointly generate key shares. No node ever sees the whole private key; each ends up holding its own share plus the shared public key. Keygen needs every participating node online. Signing. Your backend hashes the message and sends the 32-byte digest. A threshold of nodes runs the multi-round protocol and returns a standard (r, s, v) , valid on-chain. DKG runs once. After that, signing only needs a threshold of nodes, not all of them. One detail trips up everyone on day one: In tss-lib -style implementations, threshold is the polynomial degree t , and the number of signers required is t + 1 . So a 2-of-3 wallet is configured with threshold: 1 (degree 1 → 2 signers), not threshold: 2 . Get that wrong and you've mis-provisioned your security by one. And remember the asymmetry: keygen needs every node, signing needs only the threshold. A node can be down and you can still sign, which is exactly the availability you want. Section 4: The protocol reality check (2026) This is where naive advice goes wrong. People pick a protocol by name and stop. A protocol name is not a security guarantee. You have to evaluate implementation, version, and threat model together. The same protocol family can be safe or unusable depending on the exact library and version. The name alone tells you nothing. GG18 / GG20 The most widely implemented family, and the basis of most turnkey servers. It also had serious holes: **BitForge (CVE-2023-33241) **, disclosed August 2023, CVSS 9.1–9.6. A malicious signer crafts a bad Paillier modulus and, in some implementations, extracts another party's key share in as few as 16 signing attempts (the real range runs from 16 up to ~a billion, depending on the implementation). The catch: it needs a malicious participating party. If you control all your own nodes, an outside attacker can't mount it. **TSSHOCK ** (Verichains, Black Hat USA 2023), a related class against GG18/GG20/CGGMP21 that can "leave no trace and appear innocent to the other parties." The fix, Paillier-Blum plus no-small-factor proofs, is in tss-lib v2.x and maintained forks. So GG18/GG20 on a patched implementation, with all nodes under your control, is a defensible self-hosted choice. On an unpatched one, it isn't. CGGMP21 Newer, academically stronger, and carrying a live problem in Rust. **CVE-2025-66017 / RUSTSEC-2025-0127 **: signature forgery through altered presignatures, CVSS 8.2. The advisory is blunt: "Patched: no patched versions" for the cggmp21 crate. There's a second twist that lands right on our path here: presignatures plus HD/BIP-32 derivation drop effective security to 85 bits, and this whole article is about to steer you toward HD derivation (Section 7). Every serious writeup says the same thing: don't start a new system on raw cggmp21 . CGGMP24 The safer forward path. The same class of issue is patched in [v0.7.0-alpha.2 ](https://github.com/LFDT-Lockness/cggmp21), and the API hardening is real. If you're choosing a modern protocol today, this is the direction. DKLS-family (DKLS23) Strong research, good performance in specific stacks. Watch the license (Section 6). Implementation quality and licensing vary wildly by vendor. GG18 / GG20 — Status 2026: Mature, patched forks exist · New self-hosted system?: Yes, if patched and all nodes yours CGGMP21 — Status 2026: Unpatched forgery CVE · New self-hosted system?: No, don't start here CGGMP24 — Status 2026: Patched ( v0.7.0-alpha.2 ) · New self-hosted system?: Yes, modern forward path DKLS23 — Status 2026: Strong, license-dependent · New self-hosted system?: Only after reading the LICENSE Never trust a "we use protocol X" claim. Check the exact library, the exact version, whether the CVE patch is actually in the code path you call, and whether your threat model even includes a malicious party. Section 5: Build your own vs. a turnkey server If you want self-hosted MPC today, you have two real options. Option A — build on a library You take a cryptographic library and build the server, networking, orchestration, storage, backups, and observability yourself. [**bnb-chain/tss-lib ](https://github.com/bnb-chain/tss-lib)** — Language: Go · License: MIT · Audit: Kudelski (Oct 2019) · Notes: The de-facto GG18/GG20 lib [**coinbase/cb-mpc ](https://github.com/coinbase/cb-mpc)** — Language: C++ · License: MIT · Audit: Cure53 (Q4 2024) · Notes: High-quality, library only — no server/storage/API LFDT-Lockness [cggmp24 ](https://github.com/LFDT-Lockness/cggmp21) — Language: Rust · License: MIT/Apache-2.0 · Audit: Kudelski · Notes: Modern protocol, patched You get total control and protocol-level flexibility. You also now own networking, retries, key storage, backups, disaster recovery, monitoring, and the entire incident-response surface. That's weeks to months of work before you sign your first production transaction. Option B — an open-source MPC server The most turnkey route right now is **Mpcium **: a Go server backed by tss-lib , shipping a full cluster (nodes, message bus, service discovery, encrypted storage) plus client SDKs, under Apache-2.0. You get a real cluster out of the box and a dramatically lower starting cost. In exchange you accept an opinionated architecture, feature gaps you'll fork around (trusted-dealer key import, a REST façade over its NATS pub/sub), and a DevOps burden that's still yours. The heuristic I keep coming back to: weeks of building networking, storage, and DR from scratch, versus adding one feature to a server that already works. Unless you have a hard protocol requirement, extend the existing server. A quick note on the rest of the field, so you don't lose a week evaluating them: ZenGo — the [multi-party-ecdsa ](https://github.com/ZenGo-X/multi-party-ecdsa) library is explicitly abandoned; [gotham-city ](https://github.com/ZenGo-X/gotham-city) is 2-of-2 only, demo-grade, and effectively dormant (last commit 2024). **Safeheron ** — open crypto libraries, proprietary server. **Sodot ** — self-hostable, but proprietary: a closed-source SDK you can't inspect or fork. **Lit Protocol ** — a decentralized node network you consume, not a private cluster you run yourself. Section 6: Licensing traps This one surprises people in legal review, not code review. A repo can be source-available or non-commercial while looking open at a glance. The protocol paper can be fully open while the implementation's license is restrictive. The cleanest example: one popular dkls23 implementation ( Silence Labs ) ships under a non-commercial license (SLL), and commercial use needs a separate paid license. A different , independent DKLs23 implementation ( 0xCarbon ) is dual MIT/Apache-2.0. Same protocol, completely different rights. Open the actual LICENSE file, in the exact repo and version you plan to ship. Not the README, not the paper. The license, pinned to your version. Section 7: One master wallet for a million users (CKD) Give every user their own wallet the obvious way and each one gets its own DKG across your nodes. That's Mpcium's default, and it works fine. The catch isn't performance, it's what it does to your backups and your operations. Every per-user DKG needs all three nodes online, and every wallet it creates is another independent set of shards on every node. So the MPC state you have to protect grows with your user base: every signup is one more ceremony to run and one more set of shards you can never lose. At a handful of users that's nothing. At a few million, provisioning and backup are the whole problem. CKD collapses all of it into one ceremony. BIP-32 hierarchical deterministic wallets, the trick Bitcoin gave us in 2012, derive an effectively infinite tree of child keys from one master. Run the DKG once, and every user after that is a pure derivation off the master — no ceremony, no nodes online, and a single backup covers all of them. That's CKD (child key derivation) : Run one DKG at init → a single master MPC wallet (still split into 3 shards, still never assembled) For each user, derive child = derive(masterPubKey, chainCode, userIndex) . Pure math, no ceremony, no nodes required online To sign, each node applies the derivation offset to its own share and signs. The child key, like the master, is never assembled Per-user DKG on the left runs a full ceremony per user. CKD on the right derives every user from one master, with no per-user ceremony. The operational gap isn't incremental, it's categorical: Creating a user — Per-user DKG: a key ceremony, all nodes · CKD (one master): a derivation, no ceremony 10,000 users — Per-user DKG: 10k ceremonies, 30k shards · CKD (one master): 1 ceremony, 3 shards Backup cadence — Per-user DKG: after every new user · CKD (one master): once, at init MPC node down — Per-user DKG: new users wait · CKD (one master): new users still created Disaster recovery — Per-user DKG: restore everything · CKD (one master): restore master; users are DB rows "Is it as secure?" is the fair question, and the answer is yes, under the same threat model: 1 node compromised — Per-user DKG: Safe · CKD: Safe 2 of 3 nodes compromised — Per-user DKG: Compromised · CKD: Compromised Backup stolen — Per-user DKG: Compromised · CKD: Compromised 2 nodes in a secure enclave — Per-user DKG: Protected · CKD: Protected That equivalence is about node compromise . Be clear-eyed about what CKD gives up, because this is the part the marketing pages skip: You can't rotate or revoke a single user's key in isolation. Everyone derives from the same master; there's no per-user share to roll. You can't prove a user's key is cryptographically independent of the others, which some regulatory or audit regimes want. Anyone holding the master public key and chain code can enumerate every user address. For an opt-in custodial service that's a non-issue; for a privacy-sensitive product it's a real deanonymization vector. If none of that bites your product, CKD is a clean way to scale. If one does — and for many products one will — per-user DKG is the safer default, and that's when it earns its cost. The BIP-32 caveat, stated honestly: with the parent public key, the chain code, and a full child private key, you can recover the parent key. In MPC that's defused, because the child private key never exists as a whole; each node holds only a share delta. The chain_code isn't secret-shared either (every node has a full copy), but that doesn't change the picture: recovering the parent still needs a full child private key, and that needs a threshold of nodes, which is total compromise already. I didn't take any of this on faith. An end-to-end recovery test ran the full flow, create the master, derive several users, sign with each, back up, destroy the cluster, restore, verify, and passed 12 of 12 checks: every derived address matched, every signature still verified, and a brand-new user created after the restore worked too. A separate cross-machine drill, destroy the cluster and rebuild on fresh hardware, is what flushed out eleven runbook bugs, one of which would have quietly lost every key in a real emergency. The next article takes that drill apart in full. Section 8: Who actually runs MPC in production MPC is no longer a crypto-startup curiosity. The clearest sign of how seriously the industry takes it: Fireblocks , one of the largest custody providers, is the team that discovered and disclosed the BitForge vulnerability class from Section 4. When your custody vendor is the one running the offensive research that finds MPC's own weak spots, the primitive has left the lab. Who else is standing on it: **IBM Digital Asset Haven ** (2025), developed with Dfns , combines Dfns's threshold signing with IBM's HSM and offline-signing models for institutional custody. MPC under an IBM-branded product is about as mainstream as it gets. Dfns runs its KU23 protocol on its key-management network — by its own figures 10M+ keys and ~$1B signed per month — and plans to open-source it and move governance to the Linux Foundation's LF Decentralized Trust (the Lockness project). Coinbase open-sourced its own MPC library, [cb-mpc ](https://github.com/coinbase/cb-mpc) (MIT, Cure53-audited). You don't open-source your custody crypto unless it's core to how you operate. Fordefi , BitGo , and Copper ship MPC/TSS custody to institutional desks and DeFi. Adopting MPC isn't an exotic bet. It's the same primitive regulated, multi-billion-dollar custodians standardized on. Section 9: Migrating from encrypted keys to MPC You rarely start clean. Here's a rollout that doesn't need a big-bang cutover. The safe path is a signer abstraction: your service code never changes, only what sits behind the interface does. Inventory every signing call site. Every place a key is created, decrypted, or used, across services, jobs, and scripts. Introduce a signer abstraction. Replace direct wallet creation with a single signing interface so the rest of your code stops caring where the key lives. // Before: the key exists in your process const wallet = new ethers.Wallet(decrypt(row.encryptedKey)); const sig = await wallet.signTransaction(tx); // After: the key never exists here — you send a digest out to sign const signer = mpc.getSigner(user.derivationPath); const sig = await signer.signTransaction(tx); // digest -> MPC -> (r, s, v) Add MPC signer adapters. One thin adapter for ethers , one for viem , both just forwarding the digest to the cluster. Dual-run behind a flag. MPC_ENABLED , rolled out to a controlled cohort first. Cut over new users first. New wallets go through MPC; the legacy path becomes read-only. Migrate or rotate legacy keys. Import if your server supports trusted-dealer import; otherwise move funds to MPC-managed wallets and retire the old ones. Delete the decryption path. Rip out every getPrivateKey() -style helper. If the code can't decrypt a key, neither can an attacker who owns the process. Test disaster recovery before launch. Restore drills are mandatory, not optional. (The next article is one long argument for why.) Section 10: The mistakes teams make After shipping this to production, the most useful thing I learned is that the cryptography almost never breaks. The orchestration does. The mistakes repeat: Misreading threshold semantics, the t vs t + 1 count from Section 3. Underestimating the message bus and metadata store. The signing protocol is chatty and stateful, and your NATS/Consul/BadgerDB layer is now load-bearing. Assuming one datastore backup is enough. It isn't; you need the key shares and the routing metadata. Running keygen while application consumers race for the result events, silently stealing DKG output from your init flow. Treating MPC as "security complete" and skipping the policy layer, which brings us to the most important caveat here. MPC is not magic MPC dramatically reduces key- theft risk. On its own it does nothing about: signing the wrong payload, if your backend is tricked into it compromised approval or policy logic frontend / UI supply-chain attacks that fool an operator into approving a bad action broad application-layer abuse after a backend compromise The cautionary tale is **Bybit (February 2025, ~$1.5B) **, the largest crypto theft to date. Bybit had multisig (Safe). The attackers didn't break the keys; they compromised the UI so operators approved a malicious transaction. MPC wouldn't have stopped it, because the failure was in intent, not custody. So treat MPC as a very strong primitive inside a layered system. Policy engine, transaction simulation, allowlists, anomaly detection, segregated duties, hardened ops: all still matter. Section 11: One strong primitive, not the whole system If your backend can decrypt every user's private key today, you don't have a cryptography problem. You have a system-design problem: one key, one server, one blast radius. MPC-TSS is the most practical way I know to redesign that system so key theft becomes structurally hard and the blast radius of a compromise shrinks. Not impossible. Materially harder. In security architecture, that difference is the whole game. One warning before you run off and build it, though. The day I moved our first wallet onto MPC and deleted the old getPrivateKey() helper felt great, right up until I realized MPC had solved the theft problem and none of the others. It will faithfully sign the wrong transaction if something upstream tells it to. So ship it as one strong primitive in a layered system, not as the thing that lets you stop worrying. Next up, the hands-on half: deploying a self-hosted 2-of-3 MPC signing cluster with Docker. The DKG ceremony, the backup model, and the disaster-recovery drill that caught a silent, cluster-killing bug — the one I mentioned above, in full. If this was useful, I write about backend, crypto infrastructure, and shipping distributed systems to production.
View original source — Hacker Noon ↗


