Skip to content
Draft
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
43 changes: 43 additions & 0 deletions docs/miniscript-reference/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
> [!WARNING]
> **BITME-ONLY REFERENCE MATERIAL:** The policies and flows shown in this file (e.g., exactly one immediate path, server-side hold/veto) belong to Bitme, NOT Keeper.
> Keeper's actual wallet policy (including Miniscript inheritance paths) and server constraints differ. Any future implementation or tests for Keeper must use the actual Keeper Miniscript policy.

# Miniscript Coordinator Pattern — Reference Implementation

This directory contains reference material for the coordinator-side Miniscript approach, adapted from [TDH-Labs/Bitme](https://github.com/TDH-Labs/Bitme).

## The Pattern

Hardware wallets that don't natively understand Miniscript (Satochip, TAPSIGNER, Jade, Coldcard) can still participate in Miniscript wallets:

1. **Coordinator** (wallet app) parses the Miniscript descriptor
2. **Coordinator** determines which spending path applies
3. **Coordinator** constructs the PSBT with the correct `witness_script` for that path
4. **Hardware** signs the sighash digest (just a hash — no policy understanding needed)
5. **Coordinator** collects signatures and finalizes

The hardware never needs to understand the policy. It just signs what the coordinator gives it.

## Files

- **`invariants-reference.rs`**: Formal Miniscript invariant tests using rust-miniscript's `entails` algorithm. Proves:
- No single key can spend alone
- Intended signer combinations can spend (immediately or after timelock)
- Timelock boundaries hold exactly (not one block early)

Adapted from Bitme's `bitme-cosigner/src/invariants.rs`. Hardware references generalized from Satochip-specific to generic "hardware".

- **`descriptor-reference.rs`**: Miniscript descriptor construction with BIP389 multipath. Shows the pattern of building descriptors from labeled keys.

Adapted from Bitme's `bitme-cosigner/src/descriptor.rs`. Note: Bitme's specific policy (Hardware+Server as the HOT path) is NOT Keeper's policy — Keeper uses 2-of-3 Mobile + Hardware + Server as the base. This is the *construction pattern*, not the *policy*.

- **`coordinator-pattern.md`**: Excerpt explaining the coordinator flow — how the hardware and server never communicate directly, with the wallet app carrying the PSBT between them.

## What's Next

These are Rust references. For TypeScript implementation in SigningServer, we need to decide:
1. Which JS/TS Miniscript library to use
2. Where the TypeScript port should live (`src/miniscript/` vs `src/wallet/`)
3. Whether invariant tests run in CI or serve as reference

See the PR description for discussion.
55 changes: 55 additions & 0 deletions docs/miniscript-reference/coordinator-pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
> [!WARNING]
> **BITME-ONLY REFERENCE MATERIAL:** The policies and flows shown in this file (e.g., exactly one immediate path, server-side hold/veto) belong to Bitme, NOT Keeper.
> Keeper's actual wallet policy (including Miniscript inheritance paths) and server constraints differ. Any future implementation or tests for Keeper must use the actual Keeper Miniscript policy.


## How a spend actually works

**The Hardware never talks to the server. It has no networking - no WiFi, no Bluetooth, nothing
that reaches the internet.** It only ever talks to whatever taps it: your phone over NFC, or a
card reader plugged into a computer. Every "HARDWARE + SERVER" spend is really two separate,
disconnected steps stitched together by a PSBT file that your wallet app carries between them -
never a live connection between the card and this service.

The server never signs the moment you ask, either way. It queues, it tells you, it waits, and
only then does it sign - so a signature is never the first you hear about a transaction.

```mermaid
sequenceDiagram
participant SC as Your Hardware
participant You as Your wallet app
participant CS as Cosigner
participant Node as Your bitcoind
participant N as Notification

Note over SC,You: Tap to phone / card reader - purely local, no network
SC->>You: Partial signature (HARDWARE's share)
You->>CS: POST /sign_psbt (HARDWARE's signature already attached)
CS->>Node: Are these coins real? Whose are they?
Node-->>CS: UTXO details
Note over CS: Re-derives every address itself.<br/>Never trusts what the PSBT claims.
CS->>CS: Check against policy
alt Over your limits
CS-->>You: 422 denied — nothing queued, nothing signed
else Allowed
CS->>N: "Pending spend: 50,000 sat to bc1q… — signs in 15 min"
CS-->>You: 202 Accepted + id
Note over CS: Holding. You can POST /veto/{id}
alt You veto
CS->>CS: Cancelled permanently
else Hold elapses
CS->>CS: Re-check policy against live state
CS->>CS: Add the SERVER signature
CS-->>You: PSBT with both signatures — 2 of 3, spendable
end
end
Note over You: You broadcast. The server never does.<br/>Notice the Hardware only ever appears in the top line.
```

The cosigner never talks to the Hardware either — as far as this service is concerned, "HARDWARE
signed" just means *a PSBT arrived with a valid signature under HARDWARE's key already in it*. It
has no way to reach the card, ask it to sign, or know it exists except through that signature.
Your wallet app (Bitcoin Keeper, or Sparrow with a card reader) is the only thing that talks to
both sides — locally to the Hardware, over the network to the cosigner — and a PSBT file is the
only thing that ever crosses between them.

Loading