Formerly "Swap Key", the internal codename; protocol lineage v3.16
unchanged. This page mirrors docs/TESTER-GUIDE.md from the release
package — the package copy travels with the exact build you run.
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.
swap --make-onion, or discovery — section 6); Noise over clearnet
is still post-pre-alpha.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.holding the SL claim until height N for many blocks. That is the
privacy posture working — leave the wallet running.status annotates each unit with its maturity time and height gate; a
swap attempted early refuses cleanly and leaves the coin untouched, so retrying is
free. (The current signed manifest sets the testing-period delay; the
production-scale 24–72 h value returns after the testing round.)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.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
(github.com/abysal32-arch/switchbitcoin;
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. Cross-check that root against the
key published on the homepage. If you EVER see an
UNSHIPPABLE BUILD banner, stop and report it — that binary trusts a
public test key.
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.
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.switchbitcoin.toml and fill the
[node] section, or commands will refuse with this command needs a
node.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).onboard.onboard confirms, each coin matures after its own randomized
onboarding delay. status shows the unit as
PreEncumbrance/Unspent with a maturity annotation; a swap attempted
before both halves clear refuses cleanly and costs nothing.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.A swap needs a live TCP link between the two wallets. On one machine (loopback) or
one LAN this just works. Across the internet the maker (swap
--make) runs a listener the taker must reach, and home routers block
inbound connections by default.
host:port.
--make 0.0.0.0:9735 binds every interface but the printed ticket cannot
carry a dialable address. Put the address your partner can actually reach in the
host:port you pass.--connect-retries, default 4,
~2 s apart), so the taker can start a moment before the maker's listener is up.
Nothing is leased or funded until both sides are connected and negotiating —
a failed dial is free: fix the address and run again.connection refused → the maker isn't
listening yet. timed out / no taker reached … → the
address isn't reachable from the other side (NAT/firewall).
handshake: peer runs different signed params is NOT a connectivity
problem — see section 8.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 one is not being retired and is
still the lowest-metadata option there is (6.5).
skt1… ticket is a phone number — where to call, and
proof you're calling the right person;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.
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:
[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 genuinely blocks Tor,
bridges and pt_dir are in the config for that — but a Tor
that won't bootstrap is much more often a firewall on your own 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:
[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, disclosed rather
than quietly folded into the curated list; and 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 — but a power cut or a network outage at that end does not, because 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 us 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 round where everyone runs the same three, that trade is acceptable and stated.
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.
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
Tor comes up (about 65 s the first time, ~6 s afterwards), the relays connect (10–25 s over Tor), then a proof-of-work mint — 20 bits, seconds, not minutes — and the wallet waits for a taker.
Taker — look at what's on offer, then take one:
switchbitcoin-cli discover scan
switchbitcoin-cli discover take <offer_ref> --wait-secs 1200
Each acceptable offer prints a 64-hex reference plus its expiry, and you get a
tally of what was refused and why. Those refusals are healthy — the scan is
supposed to discard most of what it sees. Add --meet-only to the
take for a rehearsal that stops at the rendezvous ticket and settles nothing (no
coins, no lease) — good for a first run.
From there the 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 first. 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.
| 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 |
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 — 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 different: 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.
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. You can also have both:
swap --make-onion gives you a Tor rendezvous with a hand-delivered
ticket — no relays, no clearnet IP.
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 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 rehearsing.FEE WEATHER WARNING: live 193 sat/vB exceeds baked …. On
testnet4 this is usually an estimator with no data, not congestion — measured
returning an identical figure for every target while the mempool held nine
transactions and real blocks confirmed 1 sat/vB transactions. Don't stop on it.switchbitcoin-cli version on
both sides.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: the permanent
pre-alpha banner with build version, manifest version + id (with a LOUD warning on the
fingerprintable v0 baseline), refund reality explained rather than shown as a bare
"failed", claim holds rendered as privacy holds until a named block, alarms shown
prominently, and the active/max swap count. 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.
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
v3.manifest shipped in the package and
published on this site (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) — when
the operator publishes a new manifest, ingest it promptly.This is the best fund-safety demo you can run (order matters — rehearsed end-to-end on the shipped package, 2026-07-23):
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.switchbitcoin-cli restore --from wallet.skbak.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.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: 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).
watch holds the wallet's single-instance lock — it cannot share
a data dir with a running swap/serve. Second device
(or second dir from a restored bundle) is the design.swap/serve
from a stale restore without manifest ingest-ing the newest signed
manifest first.watch --once does one pass and exits — cron/Task-Scheduler
friendly.| 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: … HOLDING … |
A testnet4 reorg orphaned a confirmation your live swap depends on. The wallet holds until it re-confirms — the safety system working. Keep running; report only if it never clears. |
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) — or real file damage: restore keystore.bin from backup / init --restore from the mnemonic. |
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). |
coins become leasable after their decorrelation delay … / timelock/deadline invariant violated: … onboarding delay |
The onboarding privacy delay. NOT a bug and nothing is stuck (no lease, no broadcast) — status shows each unit's maturity; wait and re-run swap. |
handshake: peer runs different signed params (manifest mismatch) |
You and your partner are on different manifest versions. 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, wrong file, or the retired-key v1). Re-fetch the round's manifest. |
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 / could not reach the peer at … |
Reachability (NAT/firewall) — section 5. Nothing was locked; fix the address and retry. |
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 … |
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. On testnet4 this is usually spurious — an estimator with no data, not congestion. Note it, don't stop for it. |
ALARM — RefundStalledBelowFeeFloor: … |
Congestion + no usable reserve for the fee bump. The refund still fires; only confirmation waits. If it names the watchtower device, onboard a deposit there. |
standing down (completion-supersedes) |
The counterparty's completion confirmed, so the refund is moot. Correct behavior. |
ALARM: … quarantine / unreadable swap record |
A sealed record failed authentication. ALWAYS report this one with diag output. |
error: unknown flag … |
Typo protection — flags are strict on purpose. switchbitcoin-cli help. |
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 … |
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 … |
No Tor, or it didn't bootstrap. Check exe_path, that nothing else holds your SOCKS/control ports, and that a force-killed wallet didn't strand a Tor there. 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. |
discovery: offer's signed params differ from this wallet's manifest — refused before any contact |
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. |
discovery: wrap does not carry the required proof-of-work … |
Someone published without paying the PoW. Dropped. Your own mint takes seconds — if yours ever takes minutes, report it. |
discovery: this offer is in its take-and-vanish cooldown … |
Someone took this offer and never showed up. The maker reopens the window after the cooldown. Wait, or take another. |
the rendezvous window closed with nobody on it (take-and-vanish #N) — this offer waits 600s |
The maker's side of the row above. Somebody took your offer and never dialled. 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. |
the rendezvous reply reached only N of the 3 relays quorum needs (maker), or no rendezvous reply before the deadline (taker) |
Known and unexplained — please report it if you hit it. 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. We measured this in our own drills: offers reach every live relay reliably, rendezvous replies do not, and we do not yet know why. If it happens to you, note the time and which relays you had configured — that is exactly the data we are missing. |
discovery: the reply's ticket does not name a v3 onion … |
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 |
Normal for a fresh onion — the address has to propagate through Tor's directory first (9–46 s typical). If all twelve fail you get a clean refusal and nothing was leased. |
the rendezvous dial failed: abort to refund … then Nothing was leased and nothing is owed … |
The maker's onion never came up, or they quit. 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 runs during test windows) before assuming a bug. discover doctor first. |
The package copy of the guide carries the complete table, including watchtower- and fee-bump-specific lines.
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.docs/BUG-REPORT-TEMPLATE.md from the package (what you did,
what you expected, what happened, diag output, your Bitcoin Core
version, the failing command's full stderr).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.