Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,20 @@
**/.venv/
**/__pycache__/

# Pico firmware
*/pico-signer.elf
*/pico-signer.uf2
# HW Signer firmware build artifacts
*.elf
*.uf2
*.bak
hwsigner-secure/target/
embassy-rp-fork/target/
threshold/target
threshold-ffi/target
cosigner/target
server/target
e2e/signer-server/target
ark/target
ark-ffi/target
hwsigner-secure/target

# IDE
.vscode/
Expand Down
77 changes: 41 additions & 36 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,24 @@
# make e2e-ark Run Ark E2E test
# make hardware Start regtest for hardware device (no Ark)
# make hardware-ark Start regtest for hardware device with Ark
# make flash Build & flash Pico 2 firmware (UF2)
# make hw-build Build HW Signer TrustZone firmware (Secure + NS)
# make hw-flash Flash HW Signer via debug probe
# make hw-test Smoke test HW Signer over USB HID
# make down Stop everything
# ═══════════════════════════════════════════════════════════════════════════════

.PHONY: e2e e2e-ark hardware hardware-ark flash down \
.PHONY: e2e e2e-ark hardware hardware-ark down \
ffi-build ffi-android threshold-ffi-build threshold-ffi-android ark-ffi-build ark-ffi-android \
cosigner-build server-build signer-build pico-build \
cosigner-build server-build signer-build \
hw-build hw-build-secure hw-build-ns hw-flash hw-flash-probe hw-test \
regtest-up regtest-down bitcoin-init mine-loop adb-reverse \
signer-run signer-stop server-run server-stop \
arkd-up arkd-down arkd-init \
proto threshold-test threshold-ffi-test pico-flash-probe pico-test \
proto threshold-test threshold-ffi-test \
flutter flutter-run ark-newaddress crypto-bench \
stress-test load-test \
signet-hardware-ark signet-down e2e-mutinynet e2e-mutinynet-ark \
e2e-test e2e-ark-test regtest regtest-ark regtest-hardware regtest-hardware-ark regtest-hardware-ark-down pico-flash
e2e-test e2e-ark-test regtest regtest-ark regtest-hardware regtest-hardware-ark regtest-hardware-ark-down

# ── Variables ─────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -96,24 +99,7 @@ hardware-ark: cosigner-build server-build ffi-build ffi-android
--wasm ../cosigner/target/wasm32-wasip1/release/cosigner.wasm \
--port 50051

# 5) Build and flash Pico 2 firmware via UF2 (hold BOOTSEL + plug in USB first)
flash: pico-build
@echo "Converting ELF to UF2..."
cp pico-signer/target/thumbv8m.main-none-eabihf/release/pico-signer pico-signer/pico-signer.elf
picotool uf2 convert pico-signer/pico-signer.elf pico-signer/pico-signer.uf2 --family rp2350-arm-s
@echo ""
@echo "==> Created pico-signer/pico-signer.uf2"
@echo "==> Copy to the RP2350 drive: cp pico-signer/pico-signer.uf2 /media/$$USER/RP2350/"
@echo ""
@if [ -d "/media/$$USER/RP2350" ]; then \
cp pico-signer/pico-signer.uf2 /media/$$USER/RP2350/ && \
echo "Copied! Pico will reboot with new firmware."; \
else \
echo "RP2350 drive not found. Hold BOOTSEL + plug in the Pico, then run:"; \
echo " cp pico-signer/pico-signer.uf2 /media/$$USER/RP2350/"; \
fi

# Stop everything (server, signer, mine loop, Docker)
# 5) Stop everything (server, signer, mine loop, Docker)
down:
@echo "Stopping all services..."
-pkill -f "target/release/server" || true
Expand All @@ -125,6 +111,38 @@ down:
sudo rm -rf $(DATA_DIR) 2>/dev/null || true
@echo "All stopped."

# ═══════════════════════════════════════════════════════════════════════════════
# HW SIGNER (TrustZone — Secure + Non-Secure worlds)
# ═══════════════════════════════════════════════════════════════════════════════

# Build Secure world (rp235x-hal, crypto, SAU — generates target/veneers.o)
hw-build-secure:
@echo "Building HW Signer Secure world..."
cd hwsigner-secure && cargo +nightly build --release

# Build Non-Secure world (Embassy, USB HID — links veneers.o from Secure build)
hw-build-ns: hw-build-secure
@echo "Building HW Signer Non-Secure world..."
cd hwsigner && cargo clean && cargo +nightly build --release

# Build both worlds
hw-build: hw-build-ns

# Flash both worlds via debug probe (requires SWD probe connected)
hw-flash: hw-build
@echo "Flashing via debug probe..."
cp hwsigner-secure/target/thumbv8m.main-none-eabihf/release/hwsigner-secure hwsigner-secure/hwsigner-secure.elf
cp hwsigner/target/thumbv8m.main-none-eabihf/release/hwsigner hwsigner/hwsigner.elf
probe-rs download --chip RP2350 hwsigner-secure/hwsigner-secure.elf
probe-rs download --chip RP2350 hwsigner/hwsigner.elf
probe-rs reset --chip RP2350
@echo "Flashed and reset!"

# Smoke test HW Signer over USB HID (no phone needed)
hw-test:
@echo "Testing HW Signer over USB HID..."
scripts/.venv/bin/python3 scripts/test_hwsigner.py $(ARGS)

# ═══════════════════════════════════════════════════════════════════════════════
# BUILD TARGETS
# ═══════════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -178,10 +196,6 @@ signer-build:
-sudo chown -R $(USER):$(USER) e2e/signer-server/target 2>/dev/null || true
cd e2e/signer-server && cargo build --release

pico-build:
@echo "Building Pico Signer firmware..."
cd pico-signer && cargo build --release

# ═══════════════════════════════════════════════════════════════════════════════
# INFRASTRUCTURE
# ═══════════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -268,14 +282,6 @@ threshold-ffi-test:
@echo "Running threshold-ffi tests..."
cd threshold-ffi && cargo test

pico-flash-probe: pico-build
@echo "Flashing via debug probe..."
cd pico-signer && cargo run --release

pico-test:
@echo "Testing Pico Signer over USB HID..."
scripts/.venv/bin/python3 scripts/test_pico.py $(ARGS)

flutter: ffi-android
cd ap && flutter run

Expand Down Expand Up @@ -354,4 +360,3 @@ regtest-down: down
regtest-hardware: hardware
regtest-hardware-ark: hardware-ark
regtest-hardware-ark-down: down
pico-flash: flash
58 changes: 29 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Merlin Wallet

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.
A **self-custodial Bitcoin wallet** powered by **FROST threshold signatures** and an **RP2350 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.

Expand All @@ -18,7 +18,7 @@ Three independent identities — your phone, a hardware signer, and a coordinati
+---------------------+---------------------+
| |
+-------------+--------------+ +--------------+-------------+
| Android Phone | USB OTG | Pico 2 Hardware Signer |
| Android Phone | USB OTG | HW Signer (RP2350) |
| Flutter App +-------------+ Embassy firmware |
| Identity 1/3 | HID 64B | Identity 2/3 |
| Signing + wallet logic | reports | Recovery key in flash |
Expand All @@ -28,7 +28,7 @@ Three independent identities — your phone, a hardware signer, and a coordinati
| Identity | Held by | Purpose |
|----------|---------|---------|
| **Signing** | Phone (local) | Day-to-day transaction signing |
| **Recovery** | Pico 2 (USB HID) | Policy changes, recovery operations |
| **Recovery** | HW Signer (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.
Expand All @@ -43,10 +43,10 @@ MPCWallet/
+-- 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)
+-- hwsigner/ Hardware signer firmware (Embassy + USB HID, RP2350)
+-- protocol/ gRPC stubs and proto definitions
+-- e2e/ End-to-end integration tests (includes signer-server)
+-- scripts/ Utilities (bitcoin.sh, test_pico.py, udev rules)
+-- scripts/ Utilities (bitcoin.sh, test_hwsigner.py, udev rules)
+-- docker-compose.yml Bitcoin regtest environment (bitcoind + electrs)
+-- Makefile Build, flash, and run targets
```
Expand All @@ -61,7 +61,7 @@ High-level Dart API that orchestrates the full MPC protocol. Manages two local i

### Threshold Library (`threshold/`)

`#![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`).
`#![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` (hwsigner), and Dart FFI (`libthreshold_ffi.so`).

### Threshold FFI (`threshold-ffi/`)

Expand All @@ -75,13 +75,13 @@ Rust gRPC server that participates as the third identity in DKG and signing. Eac

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.

### Pico Signer Firmware (`pico-signer/`)
### HW Signer Firmware (`hwsigner/`)

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

### Signer Server (`e2e/signer-server/`)

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.
Standalone Rust TCP server that implements the same JSON command protocol as the hwsigner firmware. Used in E2E and integration tests to simulate the hardware signer without physical hardware.

## Protocol

Expand All @@ -95,11 +95,11 @@ Any two identities can co-sign. Each generates an ephemeral nonce pair and excha

### Spending Policies

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.
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 hardware signer.

### USB HID Chunking

Messages between the phone and Pico are split into 64-byte HID reports:
Messages between the phone and hardware signer are split into 64-byte HID reports:

```
First report: [channel:2][cmd:1][seq:2][total_len:2][payload:57B]
Expand All @@ -120,12 +120,12 @@ Channel `0x0101`, command `0x05` (MSG). Sequence numbers are big-endian `u16`. L
```bash
# Install Rust targets
rustup target add wasm32-wasip1 # Cosigner WASM component
rustup target add thumbv8m.main-none-eabihf # Pico 2 firmware
rustup target add thumbv8m.main-none-eabihf # HW Signer firmware

# Install cargo-component for building WASI components
cargo install cargo-component

# Install probe-rs for Pico flashing (optional, UF2 also supported)
# Install probe-rs for HW Signer flashing (optional, UF2 also supported)
cargo install probe-rs-tools
```

Expand All @@ -152,15 +152,15 @@ In a second terminal:
cd ap && flutter run # Launch on emulator
```

### 3b. Physical device + Pico hardware signer
### 3b. Physical device + hardware signer

Flash the Pico (hold BOOTSEL, plug in USB, release):
Flash the device (hold BOOTSEL, plug in USB, release):

```bash
make pico-flash # Build firmware, convert to UF2, copy to RP2350 drive
make hw-flash # Build firmware, convert to UF2, copy to RP2350 drive
```

Connect your Android phone via ADB (wireless debugging recommended to free the USB port for the Pico):
Connect your Android phone via ADB (wireless debugging recommended to free the USB port for the signer):

```bash
adb pair <ip>:<pairing-port> # Pair once
Expand All @@ -174,22 +174,22 @@ In a second terminal:
cd ap && flutter run # Select "Hardware Signer (USB)" in onboarding
```

Connect the Pico to the phone via USB OTG adapter. The app will auto-discover it.
Connect the signer to the phone via USB OTG adapter. The app will auto-discover it.

### 4. Smoke test the Pico (no phone needed)
### 4. Smoke test the hardware signer (no phone needed)

```bash
# Quick: just get_info
make pico-test
make hw-test

# Full: 2-of-2 DKG + sign with signer-server as second participant
make pico-test ARGS="--full-dkg"
make hw-test ARGS="--full-dkg"
```

Requires the Python `hidapi` package (`pip install hidapi`) and the udev rule:

```bash
sudo cp scripts/99-pico-signer.rules /etc/udev/rules.d/
sudo cp scripts/99-hwsigner.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
```

Expand All @@ -205,8 +205,8 @@ 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"
# HW Signer firmware over USB HID
make hw-test ARGS="--full-dkg"

## Performance & Stress Testing

Expand Down Expand Up @@ -250,9 +250,9 @@ This target:
| `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) |
| `hw-build` | Build HW Signer firmware |
| `hw-flash` | Build, convert to UF2, and copy to RP2350 drive |
| `hw-test` | Test HW Signer 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 |
Expand All @@ -263,13 +263,13 @@ This target:
## Security Model

- 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 **hardware signer's secret share** never leaves the device's flash memory.
- The **server cannot unilaterally sign** — it always needs cooperation from the phone or hardware signer.
- 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.
- The hardware signer uses the RP2350's **hardware TRNG** for all randomness.

## References

Expand Down
10 changes: 5 additions & 5 deletions ap/android/app/src/main/kotlin/com/example/ap/UsbHidPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.Executors

/**
* Flutter platform channel plugin for USB HID communication with Pico Signer.
* Flutter platform channel plugin for USB HID communication with HW Signer.
*
* Exposes methods: enumerate, open, close, writeReport, readReport
* via MethodChannel "com.mpcwallet.ap/usb_hid".
Expand All @@ -31,7 +31,7 @@ class UsbHidPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private const val CHANNEL = "com.mpcwallet.ap/usb_hid"
private const val ACTION_USB_PERMISSION = "com.mpcwallet.ap.USB_PERMISSION"

// Pico Signer USB IDs (pid.codes open-source VID)
// HW Signer USB IDs (pid.codes open-source VID)
private const val VENDOR_ID = 0x1209
private const val PRODUCT_ID = 0x0001

Expand Down Expand Up @@ -77,7 +77,7 @@ class UsbHidPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}

/**
* List connected USB devices matching Pico Signer VID/PID.
* List connected USB devices matching HW Signer VID/PID.
*/
private fun enumerate(result: MethodChannel.Result) {
val manager = usbManager ?: run {
Expand All @@ -104,7 +104,7 @@ class UsbHidPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}

/**
* Open connection to the first matching Pico Signer device.
* Open connection to the first matching HW Signer device.
*/
private fun open(result: MethodChannel.Result) {
val manager = usbManager ?: run {
Expand All @@ -115,7 +115,7 @@ class UsbHidPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
val device = manager.deviceList.values.firstOrNull { d ->
d.vendorId == VENDOR_ID && d.productId == PRODUCT_ID
} ?: run {
result.error("USB_NOT_FOUND", "No Pico Signer device found", null)
result.error("USB_NOT_FOUND", "No HW Signer device found", null)
return
}

Expand Down
2 changes: 1 addition & 1 deletion ap/android/app/src/main/res/xml/device_filter.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Pico Signer: pid.codes open-source VID 0x1209, PID 0x0001 -->
<!-- HW Signer: pid.codes open-source VID 0x1209, PID 0x0001 -->
<usb-device vendor-id="4617" product-id="1" />
</resources>
Loading
Loading