# SwitchBitcoin — Tester Guide (PRE-ALPHA)

*(Protocol lineage v3.16, unchanged.)*

Welcome, and thank you for testing. This guide takes you from nothing to a
completed swap, covers the local UI and the second-device watchtower, and
tells you exactly how to report what breaks.

## 1. ⚠ LIMITATIONS BANNER — read this first, it is not optional

* **TESTNET/REGTEST ONLY — NO REAL FUNDS.** Mainnet has no config variant by
  construction; that stays true until the external cryptographer review.
  Treat every coin this wallet touches as expendable test money.
* **~HALF of swap attempts refuse and close through REFUNDS — by design.**
  The role↔CSV pre-commitment fix is deferred to the cryptographer review;
  the interim convention makes convention-mismatched attempts refuse at the
  CSV-binding guard. **You will see exactly this, and it is the good outcome:**

  ```
  [switchbitcoin] swap routed to the refund exit: role<->CSV guard refused this
  swap (the deferred pre-commitment stop-gate) - EXPECTED for ~half of
  pre-alpha attempts, NOT a bug: ...
  ```

  (That message is deliberately plain ASCII so it survives any console and
  pastes cleanly into a bug report.)

  Your funds come back automatically at the refund timelock — **roughly 30–45
  hours on testnet4**, depending on which side of the swap you were (the two
  escrows carry different CSV delays, 144 vs 216 blocks, at testnet4's
  measured ~12.3 min/block). A refunded swap is the safety system WORKING.
  Retry, don't panic.

  **If instead the reason begins `phase-A exchange failed`, that is NOT the
  by-design refusal** — it is a genuine exchange failure and is worth
  reporting with the bug template. Your coins are equally safe either way (both
  route to the same pre-armed refund), but only one of the two is expected.
* **Plaintext TCP peer transport on the MANUAL path** — loopback/LAN/VPN
  interop only; assume anyone on the path can watch the (non-secret)
  negotiation, and that your partner sees your IP. Tor is now available and
  removes both (`swap --make-onion`, or discovery — section 6); Noise over
  clearnet is still post-pre-alpha.
* **Discovery is TESTNET-ONLY and TOR-MANDATORY, and it does not start out of
  the box.** Finding a stranger on public Nostr relays works — it was proven
  live on 2026-07-27 — but the shipped relay list is one short of the signed
  quorum, so you must add the round's third relay before it will run at all.
  Section 6 has the exact config and the honest reason. **A *discovered* swap
  HAS now settled on chain** (2026-07-31, testnet4 blocks 146390/146397, both
  sides `Completed`, zero out-of-band bytes) — but that is **one** swap, which
  is evidence the path works and not a success rate: the ~half role↔CSV
  refusal above applies to discovered swaps exactly as it does to manual ones,
  and an attempt an hour earlier refused that way.
* **Software key custody** — a BIP39/BIP32 encrypted file, not a hardware
  enclave. Your passphrase and 24-word mnemonic are the whole defense.
* **`serve` has NO auth** — loopback only, and any local process can drive
  your wallet while it runs. Don't run it on a shared machine.
* **Claim-delay privacy holds are real**: after a swap settles you may see
  `holding the SL claim until height N` for many blocks. That is the privacy
  posture working — leave the wallet running.
* **Onboarding delays are real on testnet**: each newly onboarded coin
  becomes swappable only after its OWN randomized decorrelation delay,
  drawn per coin at onboard time from the signed manifest's bounds (so two
  units rarely mature together) — 1–2 h under the current v3 testing
  manifest; the production value (and the v0 compiled baseline) is
  24–72 h. `status`
  annotates each unit with its maturity time and height gate
  (`[onboarding delay: matures ~… & height ≥ …]`); a swap attempted early
  refuses cleanly and leaves the coin untouched, so retrying is free.
* **testnet4 REORGS are routine** — multi-block reorgs happen there (regtest
  never reorged). If a `reorg detected: … HOLDING …` line appears mid-swap,
  the wallet is deliberately pausing until the orphaned confirmation settles.
  A reorg can only ever DELAY an exit, never fire one early. Leave it running.

## 2. Install

**Option A — the release package** (`switchbitcoin-prealpha-<version>-<git>-<platform>/`):
verify `SHA256SUMS`, put the folder somewhere convenient, and use the two
binaries directly: `switchbitcoin-cli` (the wallet) and `switchbitcoin-manifest`
(operator tooling — testers normally never need it). `README-FIRST.txt`
points back here.

**Option B — build from source** (windows-gnu is the verified host):
Rust toolchain + a MinGW gcc (Windows), then `scripts/build-release.sh` —
it runs the full test gates, refuses to package a binary with a test trust
root, and drops the package under `dist/`. Linux/macOS: same script,
best-effort (untested hosts).

Check what you have — every bug report starts with this line:

```
switchbitcoin-cli version
```

It prints the build (crate version + git hash), the pre-alpha banner, and
the pinned manifest trust root. If you EVER see an `UNSHIPPABLE BUILD`
banner, stop and report it — that binary trusts a public test key.

## 3. A Bitcoin node

You need your own Bitcoin Core **28 or newer** (P2A/TRUC support), synced on
**testnet4** — or regtest for a purely local rehearsal: two wallets (two
data dirs, two configs) on one machine against `bitcoind -regtest`, same
quickstart flow with `--network regtest`, and you mine your own blocks.

```
bitcoind -testnet4 -server=1
```

There is deliberately **no hosted RPC option**: pointing your wallet at
someone else's node hands them your addresses, your timing, and the power to
lie to you about confirmations. Pre-alpha testing assumes your own node.
Windows note: no `-daemon` on Windows builds — give bitcoind its own
terminal. Cookie auth (the default) is the easiest hookup: point
`rpc_cookie_file` at `<bitcoind datadir>/testnet4/.cookie`.

## 4. Quickstart (zero → first swap)

Run `switchbitcoin-cli quickstart` — it prints the numbered walkthrough (init →
address → faucet → onboard → swap ticket) and every wallet command tells you
the next one. Highlights and traps:

* `init` shows your **24-word mnemonic exactly once** and makes you retype
  it, then demands the Phase-0 acknowledgement (type `ACCEPT`). Write the
  words down; the file `keystore.bin` + your passphrase is the only other
  copy of your keys.
* Edit the generated `switchbitcoin.toml` and fill the `[node]` section, or
  commands will refuse with `this command needs a node: add a [node]
  section to switchbitcoin.toml`.
* **Ingest the round's manifest BEFORE funding/onboard**:
  `switchbitcoin-cli manifest ingest docs/manifests/v3.manifest`. The v3
  testing round runs a 0.001 tBTC tier; a wallet still on the v0 compiled
  baseline (0.01 tier) would refuse a test-tier deposit at `onboard`, and
  the onboarding-delay draw is written at onboard time (a pre-ingest
  onboard draws the long production delay).
* Faucet amount: at least **0.0014 tBTC** (one 0.001-tier swap unit + fees
  + the CPFP reserve the split carves; ~0.004 funds three units — spare
  retry material for the by-design refusals). Wait for a confirmation
  before `onboard`.
* After `onboard` confirms, each coin matures after its own randomized
  delay — 1–2 h under the v3 testing manifest. `status` shows the unit as
  `PreEncumbrance/Unspent` with a
  maturity annotation (`[onboarding delay: matures ~<UTC> & height ≥ N]`,
  flipping to `delay elapsed` once the wall-clock half has passed). A swap
  attempted before both halves clear refuses cleanly and costs nothing (see
  Troubleshooting). (`coins: none — get an address…` means you skipped a step.)
* Swapping needs a partner and the **ticket flow** (one line of text):
  maker runs `swap --make <host:port>`, sends the printed `skt1…` line;
  taker runs `swap --take <ticket>`. Both sides stay running until
  `SWAP COMPLETED` or a refund resolution.

## 5. Connectivity (NAT & reachability)

A swap needs a live TCP link between the two wallets. On one machine (loopback)
or one LAN this just works. Across the internet — two homes, two offices — the
**maker** (`swap --make`, or `serve` + `/swap/offer`) runs a listener the
**taker** must reach, and home routers block inbound connections by default.

* **The ticket must advertise a REACHABLE `host:port`.** `--make 0.0.0.0:9735`
  binds every interface but the printed ticket cannot carry a dialable
  address — you'll see a `WARNING: --make advertises a non-dialable host` line.
  Put the address your partner can actually reach in the `host:port` you pass.
* **Make the maker reachable — pick ONE:**
  * **A shared LAN/VPN (simplest).** A mesh VPN (e.g. Tailscale/WireGuard) or
    both machines on the same Wi-Fi gives each a directly-dialable address —
    use that VPN/LAN IP in `--make`. No router config.
  * **Port-forward.** Forward the maker's listen port (TCP) on the router to the
    maker machine and advertise the maker's PUBLIC IP:port. Only the maker needs
    this; the taker dials out.
* **The taker retries a cold dial.** `--take` / `--connect` (and the UI's
  take/connect) redial a refused/timed-out/unreachable peer a few times
  (`--connect-retries`, default 4, ~2 s apart), so the taker can start a moment
  before the maker's listener is up. A retry only ever re-dials the FIRST
  connection — **nothing is leased or funded until both sides are connected and
  negotiating**, so a failed dial is free: fix the address and run again.
* **What a mid-swap drop means.** Once BOTH escrows fund, a dropped link cannot
  lose funds: the swap routes to its pre-armed refund and your coins come back
  at the CSV timelock (~24–36 h on testnet; banner item 2). A drop BEFORE
  funding just ends the attempt with nothing locked — retry.
* **Reading the failure:** `connection refused` → the maker isn't listening yet
  (retry, or start the maker first). `timed out` / `no taker reached …` → the
  address isn't reachable from the other side (NAT/firewall — use a VPN or
  port-forward). `handshake: peer runs different signed params` is NOT a
  connectivity problem — see section 8 (manifests).

## 6. Meeting a stranger (discovery) — TESTNET ONLY, TOR REQUIRED

Sections 4–5 assume you already know your partner: somebody hands you an
`skt1…` ticket and you paste it in. **Discovery** is the other way in — you
find a stranger on public Nostr relays and never exchange a single byte
outside the protocol.

Both paths are first-class. The manual ticket path is **not** being retired
and is still the lowest-metadata option there is (6.5).

### 6.1 What it actually is

Three plain-language pieces:

* the **`skt1…` ticket** is a phone number — where to call, and proof you're
  calling the right person;
* **Nostr relays** are public noticeboards. You pin a sealed note on several
  of them saying "someone wants to swap this tier"; it carries no name, and
  a fresh single-use key signs each one;
* **Tor** is the unlisted call. When a stranger answers your note, the two
  wallets meet on a one-shot onion address that exists for that rendezvous
  and nothing else.

Nothing about the *swap* goes near a relay. The relays carry the "want to
meet?" notes; everything from the handshake onwards runs over Tor directly
between the two wallets, through the same settlement code the manual path
uses. The settlement layer cannot even tell it was discovered.

### 6.2 Before your first discovery run — three prerequisites

**1. Tor.** Discovery hard-refuses without a bootstrapped Tor, and there is
no clearnet fallback, ever. The package does not ship a Tor binary — get the
pinned one yourself:

```
https://archive.torproject.org/tor-package-archive/torbrowser/15.0.19/tor-expert-bundle-windows-x86_64-15.0.19.tar.gz
```

Verify before extracting — this hash is the one the project measured, and
torproject.org's own signed and unsigned sums both agree with it:

```
6ac067402c7b4a3dc37887ed3754b3914b67fdc220c966190683e9ccf91abf0f
```

Extract, find `tor.exe` (it reports `Tor version 0.4.9.11`), and point the
config at it:

```toml
[tor]
mode = "external"
exe_path = 'C:\path\to\tor.exe'
socks_port = "19050"
control_port = "19051"
```

The ports deliberately avoid 9050/9051 so a Tor you already run is never
co-opted. **If you run two wallets on one machine, give the second one
different ports** (e.g. 19052/19053) or its Tor refuses to bind.

If your network blocks Tor outright, `bridges` and `pt_dir` are in the
config for that. Most networks don't — a Tor that refuses to bootstrap is
much more often a firewall on *your* box.

**2. Relays — read this, it is the one thing that will stop you.** Discovery
publishes to every configured relay and needs a quorum of them to accept the
write. The quorum comes from the signed manifest (`quorum_q`, currently
**3**), never from a flag.

The curated list this build ships holds **two** relays. That is not an
oversight: every free `ws://` onion relay on the canonical public list was
probed over live Tor three times each, and only two answered — eleven of the
thirteen are dead, and paid relays are structurally unusable here forever
(they authorize a *pubkey*, and every offer is signed by a fresh throwaway
key, so there is no identity to buy standing for). Re-measured 2026-07-29,
unchanged.

**So a wallet running the shipped list unmodified refuses to start
discovery**, loudly and on purpose:

```
discovery: fewer relays are configured than the signed quorum_q — configure at
least q relay endpoints (the publish quorum could never be met)
```

For this testing round, add the round's third relay to reach quorum:

```toml
[discovery]
relays = "ws://oxtrdevav64z64yb7x6rjg4ntzqjhedm5b5zjqulugknhzr46ny2qbad.onion;ws://gnostr2jnapk72mnagq3cuykfon73temzp77hcbncn4silgt77boruid.onion;ws://bbapfngyskmej5jqfnlhxadx7ir6a3r2nh7yx2wvaokjxc5cipe6uvad.onion"
```

Two things you are owed about that third one, plainly:

* **it is ours** — a small relay run by the project, not an independent
  public one. It exists because the public population is short, and it is
  disclosed rather than quietly folded into the curated list;
* **it runs continuously for this round, on one desktop, with no SLA.** A
  supervisor restarts it if it dies and brings it back at logon, so a crash
  or a reboot heals itself within about half a minute. A power cut or a
  network outage at that end does not — there is no second machine. If your
  scan reports fewer relays than you configured, the wallet names the one it
  could not reach (`relay unreachable: ws://…`); if the named one is ours,
  that is an outage on our side, not a bug in your wallet. Tell Joe and try
  again later rather than filing it.

Adding it makes your list **CUSTOM**, and the wallet will tell you so. The
design's anonymity property is that every wallet publishes to the *same*
list; a distinctive list is itself a fingerprint. For a hand-picked testing
round where everyone runs the same three, that trade is acceptable and
stated. `discover doctor` prints the same verdict.

**3. testnet4 and a current manifest.** Discovery refuses off testnet
(`discovery operates only against testnet signed params — refusing to
start`), and an offer whose signed params differ from yours is refused
**before any contact is made** — see 6.4.

Check all three at once — this spawns nothing and touches no network:

```
switchbitcoin-cli discover doctor
```

It prints your network, the quorum, every configured relay, whether your
list is CURATED or CUSTOM, your Tor config, and the gate verdict.

### 6.3 The walkthrough

One of you offers, the other scans and takes. Neither of you sends the other
anything by hand — that is the whole point, so **don't** paste the offer
reference from the maker's terminal into the taker's. The taker must read it
from its own scan.

**Maker** — publish an offer and serve the swap that comes back:

```
switchbitcoin-cli discover offer --serve --ttl-mins 60 --wait-secs 2400
```

What you'll see, roughly: Tor comes up (about 65 s the first time, ~6 s
afterwards — the data dir caches its state), the relays connect (10–25 s
over Tor), then a proof-of-work mint. The PoW is 20 bits and takes
**seconds, not minutes** — the whole mint-and-publish step measured 4–6 s.
Then the wallet waits for a taker.

**Taker** — look at what's on offer:

```
switchbitcoin-cli discover scan
```

Each acceptable offer prints a 64-hex reference plus its expiry, and you get
a tally of what was refused and why (`refused: Expired=1` and friends — those
are healthy; the scan is *supposed* to discard most of what it sees). Then:

```
switchbitcoin-cli discover take <offer_ref> --wait-secs 1200
```

Add `--meet-only` for a rehearsal that stops at the rendezvous ticket and
settles nothing — no coins, no lease. Good for a first run.

From there: the taker's take is published, the maker accepts it and arms one
ephemeral onion, the reply comes back encrypted with the `skt1…` ticket
inside, and the taker dials that onion over Tor. Allow up to a couple of
minutes for the dial — a fresh onion's address has to propagate through
Tor's directory before anyone can reach it (measured 9–46 s, and the retry
ladder is sized well past that). Once it connects, everything from the
handshake on is the ordinary swap you already know, including the ~50 %
by-design refund refusal from the limitations banner.

**Timings, so you can tell slow from broken:**

| Step | Expect |
|---|---|
| Tor bootstrap, first run / later runs | ~65 s / ~6 s |
| Relays connected (3, over Tor) | 10–25 s |
| PoW mint + publish (offer, take, or reply) | 4–8 s |
| Your take seen by the maker | up to ~15 s (the poll cadence) |
| Rendezvous onion dialable | 9–46 s, occasionally more |

### 6.4 What leaks, and what never does

Discovery is Tor-only precisely so this list can be short. Stated honestly,
including where it is weaker than you might assume:

**A relay operator sees:** that an encrypted note of a certain size class
exists, roughly when, that it carries proof-of-work, and that it came from a
Tor exit. It does **not** learn who published it (a fresh key each time,
over Tor), your home IP, the contents, the ticket, the onion address, or any
transaction id. No txid ever touches a relay — that one is enforced by a test
that inspects every relay-bound byte of a full swap.

**Anyone holding our source sees more than a relay does — this is the part
to be clear about.** Offers are encrypted to a *published* key, so the
encryption gives uniformity and opacity to a casual observer, **not
secrecy**. Someone with the source can decrypt an offer and read its
network, tier, params digest, expiry, and its throwaway maker key. That is
acceptable because an offer contains nothing private by construction — but
do not treat a published offer as confidential. Takes and replies are a
different matter: those are encrypted to real per-party keys, and the ticket
and onion inside them stay sealed.

**Your counterparty** learns what the protocol always revealed after you
connect — but not your IP address. On the manual path over clearnet, they see
it. That is a genuine improvement.

**What is weaker than it sounds:** the anonymity set. The property that
protects you is that every wallet publishes to the same full list, so no one
list identifies you — but with a curated population of two (plus the round
relay), that set is small today. Treat discovery as unlinkable-in-design and
small-in-practice for now.

**Never on a relay, and enforced by test:** the ticket, the onion, any
txid, any escrow outpoint, your session id, your IP.

### 6.5 The manual path is still the zero-metadata option

If you and your partner already have a private channel, use it. Trading an
`skt1…` ticket by hand publishes **nothing, anywhere** — no relay sees a note,
no proof-of-work is minted, no public infrastructure is touched at all.
Discovery's job is meeting a *stranger*; it necessarily leaves a little
public residue that the manual path does not. `swap --make` / `swap --take`
are supported forever and are the better choice when you don't need to be
found.

You can also have both: `swap --make-onion` gives you a Tor rendezvous with a
hand-delivered ticket — no relays, no clearnet IP.

### 6.6 Traps worth knowing before they surprise you

* **`discover offer` holds the wallet for the whole advertised window** — up
  to an hour on `--ttl-mins 60`, even after `--wait-secs` elapses. That is
  correct: it keeps the promise the offer made to any stranger who takes it.
  But it also holds the data dir's single-instance lock the whole time, so no
  other wallet command works on that directory until it exits. Use a short
  `--ttl-mins` when you're just rehearsing.
* **An abandoned offer stays discoverable for its full TTL.** If a maker
  quits, its offer sits on the relays until it expires, and scans will keep
  listing it. Take it and you'll spend a proof-of-work and get no reply. It
  costs you nothing but time — an offer can't be un-published.
* **A force-killed wallet strands its Tor process.** Exiting normally kills
  the Tor child; `Stop-Process -Force` (or a hard kill) does not. If a later
  run can't bind its Tor ports, clear the leftover process on 19050/19051.
* **The fee-weather warning fires spuriously on testnet4.** You will very
  likely see `FEE WEATHER WARNING: live 193 sat/vB exceeds baked …`. On
  testnet4 this is usually an estimator with no data, not congestion — it was
  measured returning an identical figure for every target while the mempool
  held nine transactions and real blocks were confirming 1 sat/vB
  transactions. Don't stop on it. (See the troubleshooting table.)
* **testnet4 inclusion is slow regardless of fee.** A Setup paying ~10 sat/vB
  sat unconfirmed through a block that carried three transactions and accepted
  1 sat/vB. That is chain flakiness, not your wallet.
* **Match builds with your partner.** `switchbitcoin-cli version` on both
  sides; a mismatched manifest refuses before dialing anyway, but a mismatched
  *build* is worth catching early.

## 7. The local UI (optional)

```
switchbitcoin-cli serve
```

then open `SwitchBitcoin-Wallet.html` (shipped in the package) in a browser. It
talks to `http://127.0.0.1:3316` — loopback only, **no auth** (limitation
banner applies). Onboarding, tickets, and live swap state all work from the
page; everything the UI does is also in the CLI.

When a backend is connected the page shows a **live status strip** that makes
the states testers most often misread legible — so you can tell a *normal*
state from a real problem before filing a bug:

* a permanent **PRE-ALPHA · TESTNET ONLY · NO REAL FUNDS** banner with the
  build version;
* **manifest** version + id, and a LOUD warning when you're on the v0
  provisional baseline (the fingerprintable partition — ingest a signed
  manifest);
* **refund reality** — a swap that routes to refund is explained, not shown as
  a bare "failed": ~half of swaps refund *by design* in this pre-alpha (a
  role↔CSV safety refusal); your funds return automatically at the CSV timeout,
  retry with another unit;
* a **claim hold** (`holding-claim`) rendered as a privacy hold until a named
  block — a deliberate delay, **not** a stuck swap;
* **alarms** shown prominently (not just in the trace), and the
  **active / max** swap count (new swaps 409 at the cap);
* the phase-0 provenance warning before your first onboard.

The page renders `/status` verbatim — it never invents state. If a surface
looks wrong, `switchbitcoin-cli status` / `manifest show` say the same thing.

**The UI has no discovery or Tor surface yet.** `/status` carries both (a
`discovery` object with the gate verdict and relay-list classification, and a
`tor` object), but this page does not render them, so a wallet whose discovery
is refused looks identical in the UI to one that is fine. That is a gap in the
page, not in the wallet — use `switchbitcoin-cli discover doctor` and
`switchbitcoin-cli diag` for discovery state until the page catches up.

## 8. Signed parameter manifests

Settlement parameters arrive on a signed, versioned manifest — never from
your config file. Two commands matter to a tester:

* `switchbitcoin-cli manifest show` — what you're running. A fresh wallet says
  version 0 with a WARNING: v0 wallets are a small, fingerprintable
  anonymity partition. Fix it by ingesting the current round's manifest:
* `switchbitcoin-cli manifest ingest <file>` — e.g. the `docs/manifests/v3.manifest`
  shipped in the package (v2's 0.01 production tier is superseded for this
  testing round; v1 was signed by the retired first operator key — since
  the 2026-07-16 key rotation it refuses with `signature does not verify`,
  by design). Two wallets on different manifests refuse each
  other with `handshake: peer runs different signed params (manifest
  mismatch)` — so when the operator publishes a new manifest, ingest it
  promptly (everyone in a test round runs the same version).

## 9. Backup, restore, and the second-device watchtower

This is the best fund-safety demo you can run (order matters — rehearsed
end-to-end on the shipped package, 2026-07-23):

1. Start a swap on the primary and kill it mid-swap: once the setup txids
   have printed (funding is on the wire), Ctrl-C / kill the process. The
   "device" is now dead with a funded escrow.
2. `switchbitcoin-cli backup wallet.skbak` on the dead primary. The order
   is load-bearing twice over: backup refuses while the wallet is running,
   and the bundle must POSTDATE the swap — the pre-armed refund is minted
   at negotiate time, so a bundle taken before the swap has nothing to
   guard.
3. Move the bundle to a second device (or second directory + own config),
   `switchbitcoin-cli restore --from wallet.skbak`.
4. `switchbitcoin-cli watch` on the second device — it prints `watchtower
   armed: guarding 1 escrow(s)`. If it says `no guardable swaps in the
   store`, your bundle predates the swap: re-backup, re-restore.
5. Watch the tower fire your pre-armed refund at CSV maturity — funds come
   back with the primary dead (`dead-device refund FIRED`, then on a later
   pass `pre-armed refund CONFIRMED — escrow reclaimed`, then the tower
   stands down by itself).

What a watchtower can and cannot do (from the `wallet::watch` design): it
can only ever broadcast **your own pre-armed refund** (single-signed at
negotiate time, pays your own key) and **a CPFP fee-bump of that refund** —
it holds no session key, never signs completions, never negotiates. It
cannot steal and cannot grief; two devices firing the same refund is
idempotent (same bytes, same txid).

Traps the guide must warn you about:

* `watch` holds the wallet's single-instance lock — it **cannot share a data
  dir with a running `swap`/`serve`**. If you try, you'll hit `another
  process holds this swap store (single-instance)`. Second device (or second
  dir from a restored bundle) is the design.
* A watch device with **no reserve** still fires refunds but cannot fee-bump
  under congestion — it logs `RefundStalledBelowFeeFloor: … NO leasable
  reserve exists on this watchtower device` and the refund confirms when the
  mempool clears (the timelock never expires; nothing is lost). To arm the
  silent CPFP there, onboard a small deposit on the watch device too.
* A **stale backup bundle rewinds the manifest version floor** on restore.
  Harmless for watch-only use (`watch` never negotiates), but do NOT run
  `swap`/`serve` from a stale restore without `manifest ingest`-ing the
  newest signed manifest first.
* The tower guards only swaps present in ITS OWN restored store — a bundle
  taken before a swap existed cannot guard it. Keep the watch device's
  bundle fresh: re-backup + re-restore after starting any swap you want
  tower-covered.
* `watch --once` does one pass and exits — cron/Task-Scheduler friendly.
  `watch` exits 0 when there's nothing guardable or every exit confirmed.

## 10. Troubleshooting (keyed to the real strings)

| You see | It means / do this |
|---|---|
| `swept escrow carries the wrong refund CSV (extract-and-race guard); refund` | The ~50% role/CSV convention mismatch (banner item 2). The swap closes through refunds. Retry a fresh swap. |
| `swap routed to the refund exit: …` then `refund path resolved` | Normal refund closure. Funds return at CSV maturity; leave the wallet running (or `recover` later). |
| `holding the SL claim until height N` | Privacy hold, not a hang. Keep running; on regtest, keep mining. |
| `reorg detected: … funding for … un-confirmed; HOLDING …` | A testnet4 reorg orphaned a confirmation your live swap depends on. The wallet holds until it re-confirms — the safety system working (a reorg can only delay an exit, never fire one early). Keep the wallet running; report only if it never clears. |
| `reorg detected: swept-escrow funding … re-confirmed at height N (was M)` | A reorg re-confirmed a funding at a new height. Chain-derived deadlines (the refund CSV) re-derive from the current chain automatically. Informational — keep running; include the line if you file a bug. |
| `another process holds this swap store (single-instance)` | Two processes on one data dir (often `serve` + `swap`, or `watch` on the primary dir). One wallet process per dir. |
| `wallet onboarding is incomplete — run switchbitcoin-cli init first` | `init` never finished (mnemonic retype/Phase-0). Re-run `init`. |
| `keystore: wrong passphrase or corrupted file` | Wrong passphrase (retype; NFC quirks are normalized) — or real file damage: restore `keystore.bin` from backup / `init --restore` from the mnemonic. |
| `keystore.bin is damaged but wallet data exists — do NOT delete it…` | Follow the message exactly. Never delete a keystore that guarded funds. |
| `this command needs a node: add a [node] section to switchbitcoin.toml` | Fill `[node]` in the config (section 3). |
| `deposit not found or not confirmed — wait for a confirmation` | The faucet tx hasn't confirmed, or the `<txid:vout>` is wrong (vout is the output INDEX paying your address). |
| `deposit does not pay any address of this wallet` | Wrong txid/vout, or coins sent to some other wallet's address. |
| `coins become leasable after their decorrelation delay (24-72h wall clock)` | The onboarding privacy delay (banner). Each unit's maturity is a separate random draw; `status` shows each unit's `matures ~…` annotation. Retry the `swap` once it reads `delay elapsed` — an early attempt refuses cleanly. |
| `timelock/deadline invariant violated: pre-encumbrance coins exist but are still in their onboarding delay` | The unit hasn't finished its randomized 24–72 h onboarding delay. NOT a bug and nothing is stuck (no lease, no broadcast) — `status` shows each unit's maturity time; wait and re-run `swap`. Units mature independently, so a spare may be ready before the first. |
| `handshake: peer runs different signed params (manifest mismatch)` | You and your partner are on different manifest versions/params. Both run `manifest show`, ingest the current manifest, retry. |
| `manifest REFUSED: … signature does not verify` | The file isn't signed by the pinned operator root (corrupt download or wrong file). Re-fetch the round's manifest. |
| `manifest REFUSED: … version must strictly increase` | You already run this (or a newer) version — probably fine; `manifest show` to confirm. |
| `ALARM (manifest open): ProvisionalFallback/RollbackDetected…` | The stored manifest was tampered/rolled back and quarantined; the wallet fell back to the compiled baseline. Re-ingest the current manifest and REPORT IT. |
| `no taker reached … before the accept timeout` / `ticket rendezvous failed` | (Maker) Partner never dialed, dialed the wrong ticket, or your `--make` host isn't reachable from their network. Re-check the advertised `host:port` and firewall — section 5 (Connectivity). |
| `could not reach the peer at …` / `dial … failed` | (Taker) The maker isn't listening yet, or its `host:port` isn't reachable from you (NAT/firewall). `--connect-retries` already redials a few times; if it still fails, fix reachability — section 5 (Connectivity). |
| `chain reconcile failed — fix the node/data dir before swapping` | The node is unreachable/out of sync. Fix `[node]`/bitcoind first; refusing to swap in that state is deliberate. |
| `FEE WEATHER WARNING: live N sat/vB exceeds baked …` | When your swap started, the live network feerate was above the baked Setup/settlement fees. NOT a hang and NOT an error — the swap proceeds; if a Setup or settlement then stalls, the reserve-CPFP backstop fee-bumps it automatically (the line names roughly how many reserve sats a bump could burn). The quiet variants `fee weather OK: …` and `fee weather: no live estimate …` need no action. **On testnet4 this warning is usually spurious** — it was measured firing at "193 sat/vB" while the mempool held nine transactions and `estimatesmartfee` returned an identical number for every target (an estimator with no data), with real blocks confirming 1 sat/vB transactions. Note it, don't stop for it. |
| `ALARM — RefundStalledBelowFeeFloor: …` | Congestion + no (usable) reserve for the fee bump. If it names `NO leasable reserve exists on this watchtower device`, onboard a deposit there. The refund still fires; only confirmation waits. |
| `reserve CPFP bumps are gated off` | Startup reconcile failed on the watch device — bumps disabled, refund fires unaffected. Fix the node connection to re-arm bumps. |
| `bump child already in flight on the anchor` | A fee bump is already pending (maybe from the other device). Not an error; wait. |
| `cannot guard <sid>: …` | That record can't be watched (e.g. pre-funding phase). The OTHER swaps still are; report if it names a funded swap. |
| `standing down (completion-supersedes)` | The counterparty's completion confirmed, so the refund is moot. Correct behavior. |
| `watch step failed (retrying next pass)` / `backstop pass failed (retrying next poll)` | Transient (usually RPC). It retries on the next poll; only report if it repeats forever. |
| `ALARM: node RPC unreachable (…)` | bitcoind stopped/crashed/restarting. `serve`/`watch` keep running and announce recovery (`node RPC recovered`) by themselves — restarting bitcoind is SAFE, the wallet re-reads the fresh RPC cookie automatically. During the outage `/status` shows `node_online:false` and the tip freezes at the last seen height (frozen time never fires a deadline early); nothing can broadcast until the node returns. Report only if `node RPC recovered` never appears after the node is back. |
| `ALARM: … quarantine` / `unreadable swap record` | A sealed record failed authentication. ALWAYS report this one with `diag` output. |
| `error: unknown flag …` / `use --flag value, not --flag=value` | Typo protection — flags are strict on purpose. `switchbitcoin-cli help`. |

### Discovery (section 6) — the exact strings

Most of these are the scan doing its job. A scan pass that refuses things is
**healthy**: it is supposed to discard almost everything it sees.

| You see | It means / do this |
|---|---|
| `discovery: fewer relays are configured than the signed quorum_q — configure at least q relay endpoints (the publish quorum could never be met)` | The shipped list is two relays and the signed quorum is three. Add the round's third relay — section 6.2. This is the single most likely thing to stop you. |
| `discovery requires a bootstrapped Tor — refusing to start (there is no clearnet fallback for discovered peers)` | No Tor, or Tor didn't bootstrap. Check `exe_path` points at a real `tor.exe`, that nothing else holds your `socks_port`/`control_port`, and that a previous force-killed wallet didn't strand a Tor on those ports. `discover doctor` tells you which. |
| `discovery operates only against testnet signed params — refusing to start` | You're not on testnet. Discovery is testnet-only, by construction; settlement and manual peering are unaffected. |
| Tor takes ~65 s the first time, then hangs on a later run | The first bootstrap is genuinely slow (cold data dir); ~6 s afterwards. A run that hangs *later* is usually a stranded Tor from a force-killed wallet holding the port. |
| `discovery: offer's signed params differ from this wallet's manifest — refused before any contact` | You and that maker run different signed manifests. Nothing was dialed, nothing leased — the refusal happens **before** any contact. Both run `manifest show`, ingest the round's manifest, rescan. |
| `discovery: offer/take/reply has expired` in a scan tally | Normal. Offers carry a TTL and abandoned ones linger until it runs out. |
| `the rendezvous reply reached only N of the 3 relays quorum needs` (maker), or `no rendezvous reply before the deadline` (taker) | **Cause found, and it was OURS — see below if you are on the `-e` package, where it is still present.** The maker got your take but could not publish its answer to enough relays, so it abandoned that rendezvous. Nothing was leased or is owed on either side; a retry is a fresh take and costs only a new proof of work. The reason offers went through and replies did not: our WebSocket client could not read a *fragmented* message, and one relay on the round's list fragments. A maker publishes its offer seconds after connecting, before any subscription traffic has arrived, and its reply minutes later on the same socket — by which time a fragmented delivery had broken it. Fixed in builds AFTER `-e` (DISCOVERY.md §19). On `-e` you will still see it, roughly one relay's worth of the time; it is bounded and costs nothing. Every build now also NAMES each relay and prints what it said — please include those lines if you report anything relay-shaped. |
| `the rendezvous window closed with nobody on it (take-and-vanish #N) — this offer waits 600s` | Somebody took your offer and never dialed. Your offer is fine and **keeps serving** — it just won't open another rendezvous for 10 minutes, so one stranger cannot make you mint onions endlessly. Nothing was leased. |
| `discovery: offer names an unknown tier` | An offer for a tier your manifest doesn't have. Normal on a shared relay. |
| `discovery: wrap does not carry the required proof-of-work (20 bits, committed and achieved)` | Someone published without paying the PoW. Dropped. Your own mint takes seconds — if *yours* ever takes minutes, report it. |
| `discovery: this scan pass reached the ingest cap — the excess events were dropped unprocessed` | Anti-flood cap hit; the pass processed what it could. Scan again. Report it if it happens every pass on a quiet relay. |
| `discovery: this offer is in its take-and-vanish cooldown — it will open another rendezvous window once the cooldown elapses` | You (or someone) took this offer and never showed up. The maker reopens the window after the cooldown. Wait, or take a different offer. |
| `discovery: the reply's ticket does not name a v3 onion — discovered peers are never dialed on clearnet` | A maker tried to hand you a non-onion address. Refused, correctly, and you are not at risk. **Report this one** — it should not happen between honest wallets. |
| `connect failed (host unreachable …); retry N/12 in 5s` on the rendezvous dial | Normal for a fresh onion: the address has to propagate through Tor's directory first (9–46 s typical). The ladder covers well past that. If all twelve fail you get a clean refusal and **nothing was leased** — just take a fresh offer. |
| `the rendezvous dial failed: abort to refund: peer connection failed` followed by `Nothing was leased and nothing is owed …` | The maker's onion never came up, or they quit. This is the free-failure zone: no coins moved. A retry is a FRESH take (new PoW). |
| A scan finds no offers at all, repeatedly | Check the round relay is up (section 6.2 — it is ours, and since 2026-07-29 it runs continuously rather than in windows, with a supervisor behind it but no SLA) before assuming a bug. `discover doctor` first, then ask Joe. |

## 11. Reporting bugs

1. Run `switchbitcoin-cli diag` and copy the WHOLE block. It is redacted by
   construction — no seed, mnemonic, passphrase, or RPC secrets — and names
   the exact build.
2. Fill in `docs/BUG-REPORT-TEMPLATE.md` (what you did, what you expected,
   what happened, `diag` output, your Bitcoin Core version, the failing
   command's full stderr).
3. Send it to Joe through the channel you were onboarded with (pre-alpha
   testers are hand-picked; there is no public tracker yet).

Wallet output never contains secrets by construction — but if you paste
anything else (configs, terminal scrollback from `init`), CHECK IT: the
mnemonic is displayed once during `init`, and that's on you to keep out of
reports.
