Create, fund, sign with, break, recover, and refresh an Ethereum MPC wallet — locally.
No full private key is created, stored, reconstructed, or exported.
MPC Wallet Lab is a local developer playground for threshold ECDSA. It uses
Coinbase's real cb-mpc protocols to make
the complete MPC lifecycle observable without turning it into a normal wallet UI.
The Ethereum network sees an ordinary EOA and an ordinary valid secp256k1 signature. There is no multisig contract, no Safe, and no MPC-aware blockchain.
Party A share ─┐
Party B share ─┼─ threshold ECDSA ──> one signature ──> Ethereum / Anvil
Party C share ─┘
Warning
This is educational local-development software. Never use it with real funds.
- Create configurable
k-of-mwallets with 2–5 isolated parties. - Watch real distributed key generation and opaque protocol traffic.
- Sign EIP-191 messages with different valid quorums.
- Build, threshold-sign, recover, and broadcast EIP-1559 transactions.
- Fund wallets with fake ETH through Anvil's development RPC.
- Download one password-encrypted recovery artifact per participant.
- Delete a participant share and observe
degradedorunavailablestatus. - Prove that a remaining quorum can still sign.
- Restore only the missing participant from its
.mpcbackup. - Refresh every share without changing the Ethereum address.
- Run multiple independent wallets, including real 2-of-3 and 3-of-5 topologies.
|
|
| Public wallet metadata and isolated participant health | Real threshold transfer with safe protocol telemetry |
The UI exposes only safe metadata: protocol rounds, sender/receiver party IDs, opaque packet sizes, hashes, signatures, recovered addresses, transaction hashes, and receipts. Cryptographic payload bytes and participant key blobs are never rendered in the browser.
flowchart TB
Browser["React + TypeScript<br/>wallet lab UI"]
Coordinator["Coordinator<br/>Hono + viem + SSE"]
A["Signer A<br/>encrypted share A"]
B["Signer B<br/>encrypted share B"]
C["Signer C<br/>encrypted share C"]
Native["Native bridge<br/>cb-mpc v0.2.1"]
Anvil["Anvil<br/>chain 31337"]
Browser <-->|"HTTP + safe events"| Coordinator
Browser -. "encrypted backup / restore" .-> A
Browser -. "encrypted backup / restore" .-> B
Browser -. "encrypted backup / restore" .-> C
Coordinator <-->|"opaque MPC packets"| A
Coordinator <-->|"opaque MPC packets"| B
Coordinator <-->|"opaque MPC packets"| C
A & B & C <--> Native
Coordinator -->|"ordinary raw transaction"| Anvil
| Boundary | Responsibilities | Never receives or stores |
|---|---|---|
| Browser | UI, quorum selection, encrypted backup download/upload | Plaintext MPC states, storage keys |
| Coordinator | Public wallet metadata, orchestration, packet routing, Ethereum encoding | Participant key blobs, backup passwords |
| Signer host | One party's encrypted state, native child lifecycle, backup/restore | Any other party's state |
| Native bridge | DKG, threshold signing, refresh via public cb-mpc API |
Application or custody business logic |
| Anvil | Standard Ethereum JSON-RPC and transaction validation | Any knowledge of MPC |
- Docker Desktop with Compose
- Node.js 22+
- pnpm 11+
Foundry, Clang, OpenSSL build dependencies, and cb-mpc are built inside Docker.
Nothing native needs to be installed on the host.
cp .env.example .envSet a local MPC_INTERNAL_TOKEN. The signer containers generate and persist a
different 32-byte storage key in each party volume unless one is explicitly
provided.
pnpm install
docker compose up -d --build anvil signer-a signer-b signer-cAdd D and E when experimenting with 3-of-5:
docker compose up -d signer-d signer-epnpm devOpen http://localhost:5173.
| Service | Default endpoint |
|---|---|
| Web UI | http://localhost:5173 |
| Coordinator | http://localhost:3000 |
| Anvil RPC | http://127.0.0.1:8545 |
| Signers A–E | http://localhost:7001 … 7005 |
If a port is busy, update the corresponding .env value and its related URL.
For example, moving the coordinator to 3001 also requires
COORDINATOR_CALLBACK_URL=http://host.docker.internal:3001.
- Create Demo Treasury with A/B/C and threshold 2.
- Watch DKG and download A, B, and C recovery files.
- Give the wallet 100 fake ETH.
- Sign
hello MPCwith A+B, then repeat with A+C and B+C. - Transfer 1 ETH to a generated address with A+C.
- Delete B's local share and observe the wallet become degraded.
- Confirm A+C still signs and A+B fails before MPC starts.
- Restore B from the B backup and sign with A+B again.
- Refresh shares and verify that the address is unchanged.
- Create a 3-of-5 wallet and prove that three parties sign while two cannot.
The native adapter is deliberately small and uses only the public/high-level
cb-mpc v0.2.1 surface:
| Operation | Public upstream API |
|---|---|
| Threshold DKG | coinbase::api::ecdsa_mp::dkg_ac |
| Threshold signing | coinbase::api::ecdsa_mp::sign_ac |
| Share refresh | coinbase::api::ecdsa_mp::refresh_ac |
| Aggregate public key | coinbase::api::ecdsa_mp::get_public_key_compressed |
| Transport abstraction | coinbase::api::data_transport_i |
See the bridge documentation for the NDJSON process contract, exact API mapping, build details, and real smoke test.
- viem creates the canonical EIP-191 or EIP-1559 signing digest.
- The selected quorum produces ECDSA
(r, s)throughcb-mpc. - High-S output is normalized when necessary, with parity adjusted correctly.
- Both Ethereum recovery parities are tested if the native output has no hint.
- The recovered address must match the MPC wallet address.
- A transaction is broadcast only after that assertion succeeds.
A file such as wallet-<id>-party-B.mpc contains only Party B's password-encrypted
opaque MPC state. It is not the wallet private key and cannot restore A or C.
- Password encryption: scrypt + AES-256-GCM.
- Signer storage: independent AES-256-GCM key per party.
- Restore validates format, wallet ID, party ID, public key, address, and revision.
- Wrong passwords and wrong artifacts fail without changing current state.
- Refresh uses staging plus rollback-capable two-phase promotion across signers.
- Old backups remain restorable for the lab but are clearly marked stale.
The project intentionally enforces these invariants:
- A complete Ethereum private key is never reconstructed or exported.
- Every signer owns only its own participant-local state.
- The coordinator persists public metadata only.
- The frontend never receives plaintext participant state.
- Opaque MPC payloads are routed in memory and excluded from SSE/persistence.
- Every final signature is verified by recovering the expected Ethereum address.
- Failed refresh sessions preserve the last known-good shares.
This is not production custody infrastructure. Local HTTP, a shared internal
token, Docker volumes, browser-accessible signer ports, and the lab backup format
are intentionally development-oriented. Docker isolation is not hardware
isolation, and cb-mpc does not provide the surrounding transport, authentication,
storage policy, or operational controls for an application.
pnpm typecheck # strict TypeScript checks
pnpm build # production builds
pnpm test # unit tests across the workspace
pnpm test:e2e # Playwright UI smoke tests
pnpm test:integrationThe real MPC integration suite is opt-in because it creates durable lab state and requires Anvil, signer containers, and the coordinator:
RUN_REAL_MPC_INTEGRATION=true pnpm test:integrationThe native Docker smoke target runs real 2-of-3 DKG, signs with A+C, refreshes all shares, and asserts that the aggregate public key stays unchanged:
docker build --target smoke-test native/cbmpc-bridgeapps/
├── web/ React/Vite lab interface
├── coordinator/ Hono orchestration, SSE, viem/Anvil boundary
└── signer-host/ participant storage, backup, native child lifecycle
packages/shared/ API, domain, validation, and event contracts
native/cbmpc-bridge/ C++17 public cb-mpc adapter
tests/ real integration and browser tests
docs/screenshots/ project gallery
Remove public coordinator metadata:
pnpm lab:resetRemove signer shares and their generated local storage keys:
docker compose down -vCaution
Removing Compose volumes permanently deletes the local participant states.
Previously downloaded .mpc recovery files are not affected.
Implemented and verified locally:
- real 2-of-3 and 3-of-5 DKG;
- signing with different valid quorums;
- below-threshold rejection before protocol start;
- EIP-1559 transfer mined by Anvil;
- share loss, degraded operation, backup restore, and post-restore signing;
- atomic refresh with stable Ethereum address;
- multiple independent wallets.
The full implementation contract lives in SPEC.md.


