A self-custodial Bitcoin wallet powered by FROST threshold signatures and a Raspberry Pi Pico 2 hardware signer. No single device ever holds the full private key.
Three independent identities — your phone, a hardware signer, and a coordination server — jointly control your funds through a 2-of-3 threshold scheme. Transactions require cooperation between any two parties, eliminating single points of failure while keeping the user in control.
+-----------------------+
| Coordination Server |
| (Rust, gRPC) |
| Identity 3/3 |
+-----------+-----------+
|
gRPC (50051)
|
+---------------------+---------------------+
| |
+-------------+--------------+ +--------------+-------------+
| Android Phone | USB OTG | Pico 2 Hardware Signer |
| Flutter App +-------------+ Embassy firmware |
| Identity 1/3 | HID 64B | Identity 2/3 |
| Signing + wallet logic | reports | Recovery key in flash |
+----------------------------+ +----------------------------+
| Identity | Held by | Purpose |
|---|---|---|
| Signing | Phone (local) | Day-to-day transaction signing |
| Recovery | Pico 2 (USB HID) | Policy changes, recovery operations |
| Server | Coordination server | Co-signs transactions, never learns the full key |
Any 2-of-3 can produce a valid Taproot (BIP-340) signature. The server alone cannot move funds.
MPCWallet/
+-- ap/ Flutter mobile app (Merlin Wallet)
+-- client/ Dart client library (DKG, signing, UTXO management, FFI wrapper)
+-- threshold/ FROST & DKG cryptography (Rust, no_std, secp256k1)
+-- threshold-ffi/ C-ABI shared library wrapping threshold for Dart FFI
+-- server/ Rust gRPC coordination server (Wasmtime + cosigner WASM)
+-- cosigner/ WASM cosigner component (server-side threshold crypto)
+-- pico-signer/ Raspberry Pi Pico 2 firmware (Embassy + USB HID)
+-- protocol/ gRPC stubs and proto definitions
+-- e2e/ End-to-end integration tests (includes signer-server)
+-- scripts/ Utilities (bitcoin.sh, test_pico.py, udev rules)
+-- docker-compose.yml Bitcoin regtest environment (bitcoind + electrs)
+-- Makefile Build, flash, and run targets
Android wallet UI built with Provider state management and GoRouter navigation. Onboarding flow guides the user through server connection, hardware signer pairing, and DKG key generation. Supports sending/receiving Bitcoin, spending policies, and QR codes.
High-level Dart API that orchestrates the full MPC protocol. Manages two local identities (signing + recovery), communicates with the coordination server over gRPC, drives the hardware signer over USB HID, and handles Taproot address derivation, UTXO tracking, coin selection, and PSBT construction. Includes the Dart FFI wrapper for the threshold crypto library.
#![no_std] Rust implementation of FROST (Flexible Round-Optimized Schnorr Threshold Signatures) over secp256k1 using the k256 crate. Includes the full 3-round DKG protocol, Pedersen VSS, nonce commitment generation, signature share computation, Lagrange interpolation, Taproot key tweaking, and key refresh. Compiles for four targets: native (tests & server), wasm32-wasip1 (cosigner), thumbv8m.main-none-eabihf (Pico 2), and Dart FFI (libthreshold_ffi.so).
C-ABI shared library that wraps the threshold crate for consumption by Dart via dart:ffi. Exposes handle-based functions for DKG, nonce generation, signing, and key refresh. The Dart FFI bindings live inside client/lib/threshold/.
Rust gRPC server that participates as the third identity in DKG and signing. Each user gets an isolated WASI sandbox — the server uses Wasmtime to instantiate a per-user cosigner WASM component that holds the server's key share and performs threshold crypto operations. Routes packages between participants, aggregates signature shares, enforces spending policies, and interfaces with Bitcoin Core (RPC) and Electrs (UTXO indexing).
WASI P2 Component Model guest that encapsulates all threshold cryptography on the server side. Compiled to wasm32-wasip1 and loaded by the server into per-user Wasmtime instances. Exposes DKG, nonce generation, signing, and key refresh operations through a WIT interface. Shares no memory between users.
Embassy-based async firmware for the RP2350 (Raspberry Pi Pico 2). Communicates over vendor-defined USB HID (64-byte reports) using a chunking protocol for JSON messages up to 8KB. Persists key material to the last 4KB flash sector after DKG. Handles all six commands: dkg_init, dkg_round2, dkg_round3, generate_nonce, sign, get_info.
Standalone Rust TCP server that implements the same JSON command protocol as the Pico firmware. Used in E2E and integration tests to simulate the hardware signer without physical hardware.
All three identities participate. Each generates a random polynomial, exchanges commitments (Round 1), distributes secret shares (Round 2), and verifies shares against commitments to derive their KeyPackage and the shared PublicKeyPackage (Round 3). No party ever sees the full private key.
Any two identities can co-sign. Each generates an ephemeral nonce pair and exchanges commitments (Round 1). Each computes a signature share using their secret share, Lagrange coefficients, and the challenge hash (Round 2). The server aggregates shares into a final BIP-340 Schnorr signature (R, z).
Key refresh creates additional key shares with time-windowed spending limits. Transactions below the threshold use the policy key (phone + server, no hardware signer needed). Policy updates and deletion require a recovery signature from the Pico.
Messages between the phone and Pico are split into 64-byte HID reports:
First report: [channel:2][cmd:1][seq:2][total_len:2][payload:57B]
Continuation: [channel:2][cmd:1][seq:2][payload:59B]
Channel 0x0101, command 0x05 (MSG). Sequence numbers are big-endian u16. Last packet zero-padded. Strictly request-response.
- Dart >= 3.3
- Flutter >= 3.4 (Android SDK configured)
- Rust (stable toolchain)
- Docker & Docker Compose
- protoc with Dart plugin (only if regenerating gRPC stubs)
- Android device with USB OTG support (for hardware signer testing)
# Install Rust targets
rustup target add wasm32-wasip1 # Cosigner WASM component
rustup target add thumbv8m.main-none-eabihf # Pico 2 firmware
# Install cargo-component for building WASI components
cargo install cargo-component
# Install probe-rs for Pico flashing (optional, UF2 also supported)
cargo install probe-rs-toolsmake regtest-up # Docker: bitcoind + electrs
make bitcoin-init # Mine 150 blocksmake server-run # Builds cosigner WASM + server, runs on :50051In a second terminal:
cd ap && flutter run # Launch on emulatorFlash the Pico (hold BOOTSEL, plug in USB, release):
make pico-flash # Build firmware, convert to UF2, copy to RP2350 driveConnect your Android phone via ADB (wireless debugging recommended to free the USB port for the Pico):
adb pair <ip>:<pairing-port> # Pair once
adb connect <ip>:<connect-port> # Connect
make adb-reverse # Forward ports 50051 + 50001 to PCIn a second terminal:
cd ap && flutter run # Select "Hardware Signer (USB)" in onboardingConnect the Pico to the phone via USB OTG adapter. The app will auto-discover it.
# Quick: just get_info
make pico-test
# Full: 2-of-2 DKG + sign with signer-server as second participant
make pico-test ARGS="--full-dkg"Requires the Python hidapi package (pip install hidapi) and the udev rule:
sudo cp scripts/99-pico-signer.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger# Threshold library unit tests (Rust)
cd threshold && cargo test --features std
# Threshold FFI tests
cd threshold-ffi && cargo test
# End-to-end (builds all deps, starts Docker automatically)
make e2e-test
# Pico firmware over USB HID
make pico-test ARGS="--full-dkg"| Target | Description |
|---|---|
regtest-up |
Start bitcoind + electrs via Docker Compose |
regtest-down |
Stop Docker containers and kill server processes |
server-build |
Build the Rust gRPC server |
server-run |
Build cosigner WASM + server and run on :50051 |
server-stop |
Stop the Rust gRPC server |
cosigner-build |
Build WASM cosigner component (wasm32-wasip1) |
bitcoin-init |
Mine 150 regtest blocks |
signer-build |
Build the TCP signer-server in e2e/signer-server/ |
signer-run |
Build and run signer-server on port 9090 (for e2e tests) |
e2e-test |
Run full E2E test (builds all deps, requires Docker) |
threshold-ffi-build |
Build threshold FFI shared library (libthreshold_ffi.so) |
threshold-test |
Run threshold Rust unit tests |
threshold-ffi-test |
Run threshold-ffi tests |
pico-build |
Build Pico 2 firmware |
pico-flash |
Build, convert to UF2, and copy to RP2350 drive |
pico-test |
Test Pico over USB HID (ARGS="--full-dkg" for full test) |
adb-reverse |
Forward ports 50051 + 50001 from phone to PC |
regtest |
Full dev stack: Docker + init + signer + server |
regtest-hardware |
Hardware dev stack: Docker + init + ADB + server |
proto |
Regenerate Dart gRPC stubs from .proto files |
- The full private key never exists on any single device.
- The hardware signer's secret share never leaves the Pico's flash memory.
- The server cannot unilaterally sign — it always needs cooperation from the phone or Pico.
- The server is designed to run inside a Trusted Execution Environment (TEE), ensuring the server operator cannot access key shares in memory.
- Each user's server-side key share runs in an isolated WASM sandbox (Wasmtime) with no shared memory between users.
- Signing requests are authenticated with Schnorr signatures over timestamped messages to prevent replay attacks.
- Policy changes (update/delete spending limits) require a recovery signature from the hardware signer.
- The Pico uses the RP2350's hardware TRNG for all randomness.
- FROST: Flexible Round-Optimized Schnorr Threshold Signatures
- BIP-340: Schnorr Signatures for secp256k1
- BIP-341: Taproot
- Pedersen's Verifiable Secret Sharing
- WASI Component Model
- Embassy: Async embedded framework for Rust
This project is part of the Bitspend Payment ecosystem.