Skip to content

About

Educational local MPC wallet lab with real threshold ECDSA, cb-mpc, React, viem, and Anvil

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

MPC Wallet Lab

Real threshold ECDSA, visible end to end

Create, fund, sign with, break, recover, and refresh an Ethereum MPC wallet — locally.

cb-mpc Ethereum Anvil CI License

No full private key is created, stored, reconstructed, or exported.

MPC Wallet Lab wallet registry

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.

What you can explore

  • Create configurable k-of-m wallets 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 degraded or unavailable status.
  • Prove that a remaining quorum can still sign.
  • Restore only the missing participant from its .mpc backup.
  • Refresh every share without changing the Ethereum address.
  • Run multiple independent wallets, including real 2-of-3 and 3-of-5 topologies.

See the protocol, not just the result

3-of-5 MPC wallet overview and participant health Threshold-signed ETH transfer and MPC event timeline
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.

Architecture

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
Loading
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

Quick start

Requirements

  • 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.

1. Configure

cp .env.example .env

Set 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.

2. Start Anvil and signers

pnpm install
docker compose up -d --build anvil signer-a signer-b signer-c

Add D and E when experimenting with 3-of-5:

docker compose up -d signer-d signer-e

3. Start the application

pnpm dev

Open 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.

Suggested demo

  1. Create Demo Treasury with A/B/C and threshold 2.
  2. Watch DKG and download A, B, and C recovery files.
  3. Give the wallet 100 fake ETH.
  4. Sign hello MPC with A+B, then repeat with A+C and B+C.
  5. Transfer 1 ETH to a generated address with A+C.
  6. Delete B's local share and observe the wallet become degraded.
  7. Confirm A+C still signs and A+B fails before MPC starts.
  8. Restore B from the B backup and sign with A+B again.
  9. Refresh shares and verify that the address is unchanged.
  10. Create a 3-of-5 wallet and prove that three parties sign while two cannot.

Cryptographic integration

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.

Ethereum signature handling

  1. viem creates the canonical EIP-191 or EIP-1559 signing digest.
  2. The selected quorum produces ECDSA (r, s) through cb-mpc.
  3. High-S output is normalized when necessary, with parity adjusted correctly.
  4. Both Ethereum recovery parities are tested if the native output has no hint.
  5. The recovered address must match the MPC wallet address.
  6. A transaction is broadcast only after that assertion succeeds.

Backup and refresh semantics

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.

Security model

The project intentionally enforces these invariants:

  1. A complete Ethereum private key is never reconstructed or exported.
  2. Every signer owns only its own participant-local state.
  3. The coordinator persists public metadata only.
  4. The frontend never receives plaintext participant state.
  5. Opaque MPC payloads are routed in memory and excluded from SSE/persistence.
  6. Every final signature is verified by recovering the expected Ethereum address.
  7. 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.

Testing

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:integration

The 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:integration

The 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-bridge

Repository map

apps/
├── 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

Reset the lab

Remove public coordinator metadata:

pnpm lab:reset

Remove signer shares and their generated local storage keys:

docker compose down -v

Caution

Removing Compose volumes permanently deletes the local participant states. Previously downloaded .mpc recovery files are not affected.

Project status

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.


Built as an educational threshold-cryptography lab by Ivan Kurenkov.

About

Educational local MPC wallet lab with real threshold ECDSA, cb-mpc, React, viem, and Anvil

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages