upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-08-26 22:41:53 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-26 22:41:53 +0000
commitbbe04dc8c96b4c4766d2771d3438233bf82687c0 (patch)
tree62e5a1febfb8e9bc38d133ac7981e9ef93918484
parent991853fa55a749d28d6f9e507ce72b289df92785 (diff)
parent9e39a50d1f110020b0841d31e4b931aa877cc448 (diff)
Merge upstream v1.2.19 into the fork
Merges upstream tag v1.2.19 (9e39a50) into merge/upstream-1.2.18 (991853f). Unlike the 1.2.15->1.2.18 replay, bc3de30 is a true ancestor of our branch via a9b4f89, so this is an ordinary three-way merge with merge-base bc3de30. Delta: 36 files, +3265/-163, the Tor onion transport (managed Tor, signed direct onion endpoints, onion-only dial mode) and the nHDNext restore fix. src/main.cpp, main.h, script.cpp/h and db.cpp/h are byte-identical between the two tags, and the poll() socket loop carries no diff hunks, so the merge is consensus-neutral and the soak-tested socket paths are unchanged. relay/ is untouched; the payout ledger (rpc.cpp) is untouched. 7 conflict blocks in 6 files, resolved as follows. Makefile -- VERSION. Ours: stays 1.2.19+sec with the naming comment updated, so artifacts from this tree cannot be mistaken for the upstream release. src/Makefile, src/makefile.mingw -- HEADERS list. Ours (no market.h) plus their tor.h. NODE_OBJS auto-merged correctly: no market.o, tor.o present. src/selftest.cpp -- known-tests list. Union: payout-ledger (ours) and managed-tor (theirs). src/main_gui.cpp -- one block. Our /dumpaddresses kept; their BtfStopManagedTor() on the StartNode failure path taken. The /rvrelay pin, /noannounce and the marketplace removal auto-merged and were verified intact. src/nostr.cpp -- two blocks in ResolveDescriptor. Signature takes both out-params (our fExchangeOk, their rawOut). The body keeps our drain-to-EOSE read -- returning early poisons the pooled socket for the next caller -- and adds their rawOut capture. Our TLS 1.2 floor (SSL_CTX_set_min_proto_version) is untouched by this merge; upstream 1.2.19 still has no floor of its own. Upstream's release commit also added four build logs and a compiled Python cache (build-1219.log, build-17.log, build-appimage.log, build-node.log, scripts/__pycache__/bitflash_chain.cpython-313.pyc). Removed here; they are build output, not source.
-rw-r--r--Makefile12
-rw-r--r--README.md63
-rw-r--r--docs/mining-calculator.md22
-rw-r--r--docs/pool-directory.md18
-rw-r--r--docs/release-verification.md18
-rw-r--r--docs/rendezvous.md72
-rw-r--r--docs/site.md115
-rw-r--r--docs/tor.md159
-rwxr-xr-xscripts/build-explorer.py128
-rwxr-xr-xscripts/make-release-checksums.sh2
-rwxr-xr-xscripts/package-windows-tor.sh187
-rw-r--r--scripts/smoke-managed-tor-onion.ps1174
-rw-r--r--src/Makefile5
-rw-r--r--src/btfaddr.cpp102
-rw-r--r--src/btfaddr.h8
-rw-r--r--src/main_gui.cpp96
-rw-r--r--src/makefile.mingw5
-rw-r--r--src/net.cpp145
-rw-r--r--src/net.h4
-rw-r--r--src/nostr.cpp88
-rw-r--r--src/nostr.h12
-rw-r--r--src/proxy.cpp51
-rw-r--r--src/readme.txt13
-rw-r--r--src/selftest.cpp77
-rw-r--r--src/sockcount.h1
-rw-r--r--src/test_btfaddr.cpp22
-rw-r--r--src/tor.cpp881
-rw-r--r--src/tor.h24
-rw-r--r--src/walletcmd.cpp62
-rw-r--r--src/walletcmd.h9
-rw-r--r--tests/scripts/test_bitflash_tools.py7
31 files changed, 2417 insertions, 165 deletions
diff --git a/Makefile b/Makefile
index 4fdab39..9f97b73 100644
--- a/Makefile
+++ b/Makefile
@@ -2,6 +2,7 @@
2# 2#
3# make linux build Bitflash-*-x86_64.AppImage 3# make linux build Bitflash-*-x86_64.AppImage
4# make windows build Bitflash-*-windows.zip (from MSYS2 UCRT64) 4# make windows build Bitflash-*-windows.zip (from MSYS2 UCRT64)
5# make windows-tor build Bitflash-*-windows-with-tor.zip
5# make tests build and run standalone unit tests 6# make tests build and run standalone unit tests
6# make checksums create SHA256SUMS for release assets 7# make checksums create SHA256SUMS for release assets
7# make verify-release TAG=v1.2.13 8# make verify-release TAG=v1.2.13
@@ -10,10 +11,10 @@
10ROOT := $(shell pwd) 11ROOT := $(shell pwd)
11NPROC := $(shell nproc 2>/dev/null || echo 2) 12NPROC := $(shell nproc 2>/dev/null || echo 2)
12SUDO := $(shell [ "$$(id -u)" = "0" ] && echo "" || echo "sudo") 13SUDO := $(shell [ "$$(id -u)" = "0" ] && echo "" || echo "sudo")
13# Not "1.2.18". This tree is upstream v1.2.18 plus 24 commits of our own and a 14# Not "1.2.19". This tree is upstream v1.2.19 plus our own hardening commits and
14# hand-merged payout ledger, so a binary built here must not be mistakable for 15# a hand-merged payout ledger, so a binary built here must not be mistakable for
15# the upstream release of that number -- it names every release artifact. 16# the upstream release of that number -- it names every release artifact.
16VERSION := 1.2.18+sec 17VERSION := 1.2.19+sec
17 18
18# ---- Pinned third-party sources ------------------------------------------- 19# ---- Pinned third-party sources -------------------------------------------
19# 20#
@@ -239,6 +240,9 @@ windows: deps-windows
239 @zip -r Bitflash-$(VERSION)-windows.zip Bitflash-$(VERSION)-windows/ 240 @zip -r Bitflash-$(VERSION)-windows.zip Bitflash-$(VERSION)-windows/
240 @echo "Built: Bitflash-$(VERSION)-windows.zip" 241 @echo "Built: Bitflash-$(VERSION)-windows.zip"
241 242
243windows-tor: windows
244 ./scripts/package-windows-tor.sh
245
242# ---- Clean ---------------------------------------------------------------- 246# ---- Clean ----------------------------------------------------------------
243 247
244clean: 248clean:
@@ -265,6 +269,6 @@ sign-checksums:
265verify-release: 269verify-release:
266 ./scripts/verify-release.sh $(if $(TAG),$(TAG),latest) 270 ./scripts/verify-release.sh $(if $(TAG),$(TAG),latest)
267 271
268.PHONY: linux windows clean appimage \ 272.PHONY: linux windows windows-tor clean appimage \
269 tests fuzz-net-message-smoke fuzz-script-smoke checksums sign-checksums verify-release \ 273 tests fuzz-net-message-smoke fuzz-script-smoke checksums sign-checksums verify-release \
270 deps-linux deps-windows deps-apt deps-secp256k1 deps-randomx 274 deps-linux deps-windows deps-apt deps-secp256k1 deps-randomx
diff --git a/README.md b/README.md
index ef4f39b..1782bc3 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Bitflash keeps Satoshi's original consensus rules and replaces two things:
22 22
23**RandomX proof of work.** Memory-hard algorithm used by Monero. A laptop competes equally with a server. ASICs and GPUs have no advantage. 23**RandomX proof of work.** Memory-hard algorithm used by Monero. A laptop competes equally with a server. ASICs and GPUs have no advantage.
24 24
25**Anonymous addressing.** Every node has a `.btf` address derived from its public key, similar to a Tor `.onion`. Nodes reach each other through encrypted rendezvous tunnels, so no port forwarding is needed and a node behind CGNAT works normally. 25**Anonymous addressing.** Every node has a `.btf` address derived from its public key, similar to a Tor `.onion`. Nodes reach each other through encrypted rendezvous tunnels by default, and can advertise a signed direct `.onion` endpoint for Tor peers.
26 26
27No premine. No ICO. 50 BTF per block, halving on schedule, 21M cap, ~2 minute blocks. 27No premine. No ICO. 50 BTF per block, halving on schedule, 21M cap, ~2 minute blocks.
28 28
@@ -30,14 +30,17 @@ No premine. No ICO. 50 BTF per block, halving on schedule, 21M cap, ~2 minute bl
30 30
31Worth being precise, because the difference matters if you are relying on it. 31Worth being precise, because the difference matters if you are relying on it.
32 32
33**A peer you reach over `.btf` does not learn your IP.** All outbound connections go through a rendezvous tunnel; there is no IP-based peer dialling left in the node. The relay forwards encrypted bytes and cannot read or alter them. 33**A peer you reach over `.btf` does not learn your IP.** Outbound peer discovery uses rendezvous tunnels by default. If both sides use Tor and the peer advertises a signed `.onion` endpoint, the node connects directly to that hidden service instead. In both cases the peer does not see your clearnet IP.
34 34
35**The rendezvous relay does see your IP.** It has to — it is the thing your TCP connection terminates on. Relays are run by volunteers, so treat that as a party who knows you are on the network. 35**The rendezvous relay sees your IP unless you use Tor.** It has to — it is the thing your TCP connection terminates on. With `-tor`, the relay sees Tor instead. With a signed direct `.onion` peer endpoint, the relay is not on that connection path at all.
36 36
37**The node still listens on 8433.** Nothing dials by IP any more, but the listener is still there, so anyone who already knows your address and can reach that port may connect directly. If that matters to you, firewall it. 37**The node still listens on 8433.** Nothing dials by IP any more, but the listener is still there, so anyone who already knows your address and can reach that port may connect directly. If that matters to you, firewall it.
38 38
39This is unlinkability between peers, not anonymity against a network observer. 39This is unlinkability between peers, not anonymity against a network observer.
40For Tor routing, start the node with `-tor`; see [Tor mode](docs/tor.md). 40For Tor routing and direct onion peer endpoints, start the node with `-tor`;
41see [Tor mode](docs/tor.md).
42For a plain SOCKS5 proxy without Tor-specific defaults, use
43`-socks=HOST:PORT`.
41 44
42--- 45---
43 46
@@ -49,6 +52,8 @@ configuration — it connects automatically and starts syncing.
49**Linux:** make the `.AppImage` executable and run it. 52**Linux:** make the `.AppImage` executable and run it.
50 53
51**Windows:** extract the `-windows.zip` and run `Bitflash.exe`. 54**Windows:** extract the `-windows.zip` and run `Bitflash.exe`.
55If you want managed Tor without a separate Tor install, use the
56`-windows-with-tor.zip` package and start Bitflash with `-managedtor`.
52 57
53Every release ships a `SHA256SUMS` covering the assets. Verifying takes a second 58Every release ships a `SHA256SUMS` covering the assets. Verifying takes a second
54and is worth doing: 59and is worth doing:
@@ -74,6 +79,14 @@ scripts/verify-release.sh latest
74See [release verification](docs/release-verification.md) for the full release 79See [release verification](docs/release-verification.md) for the full release
75audit flow and the maintainer signing step. 80audit flow and the maintainer signing step.
76 81
82Public mirrors should make the latest checksum files available both under the
83versioned release directory and at the release root:
84
85```text
86https://releases.bitflash.network/v1.2.19/SHA256SUMS
87https://releases.bitflash.network/SHA256SUMS # alias to latest
88```
89
77Bitflash also ships a deterministic UTXO-set commitment tool: 90Bitflash also ships a deterministic UTXO-set commitment tool:
78 91
79```bash 92```bash
@@ -113,6 +126,12 @@ python3 scripts/build-explorer.py --datadir ~/.bitflash ./explorer-out
113python3 scripts/build-explorer.py ~/.bitflash/blk0001.dat ~/.bitflash/blk0002.dat ./explorer-out 126python3 scripts/build-explorer.py ~/.bitflash/blk0001.dat ~/.bitflash/blk0002.dat ./explorer-out
114``` 127```
115 128
129The generated explorer is static and self-contained: `index.html`,
130`style.css`, `explorer.js`, `blocks.json`, `block/*.json`, and `logo.png`. It is
131safe to host behind a strict policy such as `default-src 'none'; img-src 'self';
132style-src 'self'; script-src 'self'; connect-src 'self'; base-uri 'none';
133form-action 'none'`.
134
116**Keep your node current.** Consensus rules have changed since the first 135**Keep your node current.** Consensus rules have changed since the first
117releases — 1.2.1 fixed a bug that let anyone spend anyone's coins, and 1.2.2 136releases — 1.2.1 fixed a bug that let anyone spend anyone's coins, and 1.2.2
118added a per-block signature-operation cap. A node on an older build will accept 137added a per-block signature-operation cap. A node on an older build will accept
@@ -265,6 +284,7 @@ default both are created in the data directory; use `-poolstatusfile=PATH` and
265`-poolroundsfile=PATH` to write them somewhere a dashboard or web sync can read. 284`-poolroundsfile=PATH` to write them somewhere a dashboard or web sync can read.
266The files are informational only: nodes still discover pools through Nostr and 285The files are informational only: nodes still discover pools through Nostr and
267`.btf`, not through any web domain. 286`.btf`, not through any web domain.
287See [public pool directory](docs/pool-directory.md) for the website/API shape.
268 288
269**Participant** — mine to someone else's pool. Enter or select the pool's `.btf` address and enable Start Mining. 289**Participant** — mine to someone else's pool. Enter or select the pool's `.btf` address and enable Start Mining.
270 290
@@ -319,8 +339,10 @@ Other options worth knowing:
319```bash 339```bash
320-datadir=PATH # wallet and chain data elsewhere 340-datadir=PATH # wallet and chain data elsewhere
321-port=N # P2P listen port, default 8433 341-port=N # P2P listen port, default 8433
322-socks=HOST:PORT # SOCKS5 for outbound Nostr and .btf rendezvous dials 342-socks=HOST:PORT # SOCKS5 for outbound Nostr, .btf relay, and onion peer dials
323-tor[=HOST:PORT] # Tor mode; default local Tor SOCKS5 proxy is 127.0.0.1:9050 343-tor[=HOST:PORT] # Tor mode; default local Tor SOCKS5 proxy is 127.0.0.1:9050
344-managedtor[=PATH] # start Tor, create a hidden service, advertise its onion
345-onionservice=HOST.onion:PORT # advertise this node's Tor hidden service
324-debug # verbose log; without it debug.log is nearly silent 346-debug # verbose log; without it debug.log is nearly silent
325-help # full list 347-help # full list
326``` 348```
@@ -329,15 +351,23 @@ Other options worth knowing:
329needed — the data directory takes an exclusive lock, so a second node pointed at 351needed — the data directory takes an exclusive lock, so a second node pointed at
330the same one will refuse to start. 352the same one will refuse to start.
331 353
332`-socks=127.0.0.1:9050` routes outbound Nostr discovery and `.btf` rendezvous 354`-socks=127.0.0.1:9050` routes outbound Nostr discovery, `.btf` rendezvous
333dials through a local SOCKS5 proxy such as Tor. When it is set, the node skips 355dials, and direct `.onion` peer dials through a local SOCKS5 proxy such as Tor.
356When it is set, the node skips
334plain HTTP external-IP probes instead of leaking a direct request outside the 357plain HTTP external-IP probes instead of leaking a direct request outside the
335proxy. IPv6 proxy endpoints use brackets, for example `-socks=[::1]:9050`. 358proxy. IPv6 proxy endpoints use brackets, for example `-socks=[::1]:9050`.
336 359
337`-tor` is a privacy shorthand for the usual local Tor SOCKS5 listener at 360`-tor` is a privacy shorthand for the usual local Tor SOCKS5 listener at
338`127.0.0.1:9050`. It routes outbound Nostr discovery and `.btf` rendezvous 361`127.0.0.1:9050`. It routes outbound Nostr discovery, `.btf` rendezvous dials,
339dials through Tor, refuses direct `.onion` resolution, and keeps external-IP 362and signed direct `.onion` peer dials through Tor, and keeps external-IP probes
340probes disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. 363disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else.
364
365`-managedtor[=PATH]` starts a Tor process for this node, writes a local `torrc`,
366creates a v3 hidden service for port `8433`, routes outbound discovery through
367that Tor instance, and signs the generated `.onion:8433` endpoint into the
368node's `.btf` descriptor. Without `PATH`, Bitflash looks for `tor/tor.exe`,
369`tor.exe`, or `tor` beside the binary and then on `PATH`. The hidden-service key
370is stored under the Bitflash data directory in `managed-tor/onion-service/`.
341 371
342### When something looks wrong 372### When something looks wrong
343 373
@@ -424,6 +454,19 @@ make windows
424``` 454```
425Installs deps via pacman, produces `Bitflash-*-windows.zip`. 455Installs deps via pacman, produces `Bitflash-*-windows.zip`.
426 456
457To build the Windows package that also carries the Tor Expert Bundle:
458
459```bash
460make windows-tor
461```
462
463That produces `Bitflash-*-windows-with-tor.zip`. The release script downloads
464the Tor Expert Bundle from the official Tor archive, verifies its pinned SHA256,
465optionally verifies the Tor Project GPG signature when `gpg` is available, and
466places `tor/tor.exe` beside `Bitflash.exe` so `-managedtor` works without a
467separate Tor install. For release builds, use `TOR_VERIFY_GPG=required make
468windows-tor` to require the extra signature check.
469
427--- 470---
428 471
429## At a glance 472## At a glance
diff --git a/docs/mining-calculator.md b/docs/mining-calculator.md
index 352eea1..e6cb61b 100644
--- a/docs/mining-calculator.md
+++ b/docs/mining-calculator.md
@@ -84,3 +84,25 @@ The script is intentionally standalone so the same math can later back a
84website widget or status dashboard. Until a public price exists, a website 84website widget or status dashboard. Until a public price exists, a website
85calculator should keep price as a manual field and label fiat profit as 85calculator should keep price as a manual field and label fiat profit as
86speculative. 86speculative.
87
88## Website Fields
89
90A public calculator should ask for:
91
92- miner hashrate and unit;
93- power draw in watts;
94- electricity price per kWh;
95- pool fee percent;
96- optional BTF price;
97- either current Bitflash difficulty or current network hashrate.
98
99The output should separate:
100
101- estimated BTF per day;
102- estimated gross fiat revenue, if a manual price was provided;
103- electricity cost;
104- net profit/loss;
105- the difficulty/hashrate assumptions used.
106
107Never present the fiat number as a market quote unless it comes from a real
108public market. Before that exists, label it as a manual scenario.
diff --git a/docs/pool-directory.md b/docs/pool-directory.md
index 76269b9..ff0640a 100644
--- a/docs/pool-directory.md
+++ b/docs/pool-directory.md
@@ -26,6 +26,10 @@ The generated output contains:
26- `index.html`: human-readable ranking page. 26- `index.html`: human-readable ranking page.
27- `pools.json`: machine-readable API for `api.bitflash.network/pools.json`. 27- `pools.json`: machine-readable API for `api.bitflash.network/pools.json`.
28 28
29The website is a convenience layer, not a consensus or discovery dependency.
30Nodes discover pools through Nostr descriptors and `.btf`; a missing website
31must not stop mining or pool operation.
32
29When a source is an HTTP(S) `pool_status.json`, the directory links to a 33When a source is an HTTP(S) `pool_status.json`, the directory links to a
30`pool_rounds.json` proof file next to it. That file records the block height, 34`pool_rounds.json` proof file next to it. That file records the block height,
31block hash, coinbase txid, coinbase value, pool fee, total shares, per-address 35block hash, coinbase txid, coinbase value, pool fee, total shares, per-address
@@ -41,3 +45,17 @@ Ranking order is intentionally simple:
41 45
42An entry is considered online when the source says the pool is running and its 46An entry is considered online when the source says the pool is running and its
43status file is no more than 120 seconds old. 47status file is no more than 120 seconds old.
48
49## Recommended Hostnames
50
51Use:
52
53```text
54pool.bitflash.network human-readable pool directory
55api.bitflash.network/pools.json
56status.bitflash.network relay/node health, not pool ranking
57docs.bitflash.network operator setup guide
58```
59
60The GUI may use the API as a shortcut for humans, but it should keep the
61existing decentralized pool discovery path as the source of truth.
diff --git a/docs/release-verification.md b/docs/release-verification.md
index 32de74f..b8eaf5e 100644
--- a/docs/release-verification.md
+++ b/docs/release-verification.md
@@ -34,6 +34,16 @@ From a shell with `curl` or `wget`, `sha256sum` and `gpg`:
34scripts/verify-release.sh latest 34scripts/verify-release.sh latest
35``` 35```
36 36
37The public release host should expose latest-file aliases for convenience:
38
39```text
40https://releases.bitflash.network/SHA256SUMS
41https://releases.bitflash.network/SHA256SUMS.asc
42```
43
44Those aliases must point at the current versioned directory. They do not replace
45signature verification; they only make the common manual path shorter.
46
37For a specific release: 47For a specific release:
38 48
39```bash 49```bash
@@ -71,6 +81,14 @@ Publish, alongside the assets, `SHA256SUMS` and `SHA256SUMS.asc`, and update
71is ever rotated, announce the new fingerprint clearly and update the embedded 81is ever rotated, announce the new fingerprint clearly and update the embedded
72key in `verify-release.sh`. 82key in `verify-release.sh`.
73 83
84For Windows releases with managed Tor built in, run `make windows-tor`. It
85creates `Bitflash-*-windows-with-tor.zip` by downloading the Tor Expert Bundle,
86checking the pinned SHA256 in `scripts/package-windows-tor.sh`, optionally
87verifying the Tor Project `.asc` signature, and placing `tor/tor.exe` next to
88`Bitflash.exe`. For release builds, run `TOR_VERIFY_GPG=required make
89windows-tor` so the signature check is mandatory. Include that zip in
90`SHA256SUMS` like any other release asset.
91
74## Why this matters 92## Why this matters
75 93
76`SHA256SUMS` alone protects against a broken download, but not against someone 94`SHA256SUMS` alone protects against a broken download, but not against someone
diff --git a/docs/rendezvous.md b/docs/rendezvous.md
index ac8e598..68b5df3 100644
--- a/docs/rendezvous.md
+++ b/docs/rendezvous.md
@@ -18,11 +18,16 @@ A connection to a `.btf` service has three layers:
181. **Discovery (Nostr).** The service publishes a signed descriptor as a Nostr 181. **Discovery (Nostr).** The service publishes a signed descriptor as a Nostr
19 event. A caller fetches it by the service public key and verifies the 19 event. A caller fetches it by the service public key and verifies the
20 signature. The descriptor says which rendezvous relay the service is waiting 20 signature. The descriptor says which rendezvous relay the service is waiting
21 at and which encryption key to use. 21 at, which encryption key to use, and optionally which Tor hidden service can
22 be dialed directly.
222. **Rendezvous (TCP).** Caller and service both connect out to the same relay 232. **Rendezvous (TCP).** Caller and service both connect out to the same relay
23 and are paired by public key. Neither needs an inbound port or a public IP. 24 and are paired by public key. Neither needs an inbound port or a public IP.
24 After pairing the relay is a blind byte pipe. 25 After pairing the relay is a blind byte pipe.
253. **Encrypted channel (X25519 + XChaCha20-Poly1305).** Over the paired pipe the 263. **Direct onion fallback bypass.** If the descriptor carries a valid signed
27 `.onion:port` endpoint and the caller is running with Tor mode, the caller
28 tries that endpoint before using rendezvous. This removes the rendezvous
29 relay from the data path for peers that choose to run hidden services.
304. **Encrypted channel (X25519 + XChaCha20-Poly1305).** Over the paired pipe the
26 caller and service run an ephemeral to static handshake and then exchange 31 caller and service run an ephemeral to static handshake and then exchange
27 authenticated frames. The relay sees only ciphertext. 32 authenticated frames. The relay sees only ciphertext.
28 33
@@ -107,6 +112,8 @@ The default rendezvous port is `8434`.
107The descriptor is a JSON object, signed with the identity key over a domain 112The descriptor is a JSON object, signed with the identity key over a domain
108separated digest of its own fields. 113separated digest of its own fields.
109 114
115Legacy descriptor:
116
110```json 117```json
111{ 118{
112 "v": 2, 119 "v": 2,
@@ -118,7 +125,22 @@ separated digest of its own fields.
118} 125}
119``` 126```
120 127
121The signed digest is: 128Onion-capable descriptor:
129
130```json
131{
132 "v": 3,
133 "pubkey": "<hex 32-byte x-only identity key>",
134 "enc": "<hex 32-byte X25519 public key>",
135 "meeting_node": "host:port",
136 "onion": "<56-char-v3-name>.onion:8433",
137 "created": <unix seconds>,
138 "sig": "<legacy v2 signature>",
139 "sig3": "<signature over the onion-capable digest>"
140}
141```
142
143The legacy signed digest is:
122 144
123``` 145```
124digest = SHA256( ".btf-descriptor-v2" 146digest = SHA256( ".btf-descriptor-v2"
@@ -128,17 +150,37 @@ digest = SHA256( ".btf-descriptor-v2"
128 || created (8 bytes, big endian) ) 150 || created (8 bytes, big endian) )
129``` 151```
130 152
131The signature is a deterministic BIP340 Schnorr signature (no auxiliary 153`sig` is kept on v3 descriptors so old nodes can still resolve and dial through
132randomness) over `digest` by the identity key. 154the rendezvous relay. The onion-capable digest is:
155
156```
157digest3 = SHA256( ".btf-descriptor-v3-onion"
158 || pubkey[32]
159 || enc (raw bytes of the string)
160 || meeting_node (raw bytes of the string)
161 || onion (raw bytes of the normalized string)
162 || created (8 bytes, big endian) )
163```
164
165`sig` and `sig3` are deterministic BIP340 Schnorr signatures (no auxiliary
166randomness) over their respective digests by the identity key.
133 167
134**Verification** (untrusted input, must never throw): parse the JSON, require all 168**Verification** (untrusted input, must never throw): parse the JSON, require all
135fields with the right types, confirm `pubkey` equals the address being resolved, 169fields with the right types, confirm `pubkey` equals the address being resolved,
136recompute the digest, and verify the Schnorr signature. Any failure rejects the 170recompute the digest, and verify the Schnorr signature. Any failure rejects the
137descriptor. 171descriptor.
138 172
173If `onion` is present, readers accept it only when it is a normalized v3 onion
174endpoint and `sig3` verifies. A bad or tampered onion field is ignored; the v2
175descriptor can still be used for rendezvous fallback if `sig` verifies.
176
139`created` lets a reader prefer the freshest descriptor when more than one is 177`created` lets a reader prefer the freshest descriptor when more than one is
140seen. A node only publishes a descriptor once it has actually registered at a 178seen. A node without a direct onion endpoint only publishes a descriptor once it
141`meeting_node`, so it never advertises a relay it cannot be reached at. 179has actually registered at a `meeting_node`, so it never advertises a relay it
180cannot be reached at. A node with a direct onion endpoint may publish before
181rendezvous registration using `meeting_node = "rendezvous-pending"`; Tor peers
182can still dial the signed onion endpoint, and rendezvous becomes fallback once
183registration succeeds.
142 184
143## 6. Rendezvous transport 185## 6. Rendezvous transport
144 186
@@ -243,8 +285,9 @@ tunnel.
243 not published anywhere. 285 not published anywhere.
244- **Forward secrecy.** The caller's per connection ephemeral key means a later 286- **Forward secrecy.** The caller's per connection ephemeral key means a later
245 compromise of the service encryption key does not decrypt past sessions. 287 compromise of the service encryption key does not decrypt past sessions.
246- **IP privacy.** Neither side needs a public IP or an inbound port. The observable 288- **IP privacy.** Neither side needs a public IP or an inbound port for
247 network fact is that each side has a TCP connection to a relay. 289 rendezvous. With a signed `.onion` endpoint, the reachable side exposes only a
290 Tor hidden service and the relay is not in the peer connection path.
248 291
249## 10. Threat model and limitations 292## 10. Threat model and limitations
250 293
@@ -265,6 +308,11 @@ tunnel.
265 unreachable, discovery stalls until one is reachable again. Relay TLS is 308 unreachable, discovery stalls until one is reachable again. Relay TLS is
266 verified, which closes a man in the middle path on discovery, but availability 309 verified, which closes a man in the middle path on discovery, but availability
267 still rests on the relay set. 310 still rests on the relay set.
311- **Direct onion needs Tor.** With `-managedtor`, Bitflash starts a Tor process,
312 writes a local hidden-service config, and advertises the generated onion
313 endpoint. With `-onionservice`, an operator can still run Tor manually and
314 provide the endpoint. In both cases Tor owns the hidden-service private key and
315 uptime.
268 316
269## 11. Wire reference 317## 11. Wire reference
270 318
@@ -273,9 +321,9 @@ tunnel.
273| `.btf` Base32 alphabet | `abcdefghijklmnopqrstuvwxyz234567`, no padding | 321| `.btf` Base32 alphabet | `abcdefghijklmnopqrstuvwxyz234567`, no padding |
274| Address payload | `pubkey[32] || checksum[2]`, 55 Base32 chars | 322| Address payload | `pubkey[32] || checksum[2]`, 55 Base32 chars |
275| Address checksum | `SHA256(".btf-checksum-v1" || pubkey)[0..2]` | 323| Address checksum | `SHA256(".btf-checksum-v1" || pubkey)[0..2]` |
276| Descriptor domain sep | `.btf-descriptor-v2` | 324| Descriptor domain sep | `.btf-descriptor-v2`; optional onion sep `.btf-descriptor-v3-onion` |
277| Descriptor version | `2` | 325| Descriptor version | `2`; `3` when a signed onion endpoint is present |
278| Descriptor signature | BIP340 Schnorr, deterministic (no aux rand) | 326| Descriptor signature | BIP340 Schnorr, deterministic (no aux rand), v3 keeps `sig` plus `sig3` |
279| Nostr descriptor event | kind `38501`, `d = btf-descriptor-2` | 327| Nostr descriptor event | kind `38501`, `d = btf-descriptor-2` |
280| Nostr relay event | kind `38502`, `d = btf-relay-2` | 328| Nostr relay event | kind `38502`, `d = btf-relay-2` |
281| Nostr pool event | kind `38503`, `d = btf-pool-2` | 329| Nostr pool event | kind `38503`, `d = btf-pool-2` |
diff --git a/docs/site.md b/docs/site.md
new file mode 100644
index 0000000..9526a61
--- /dev/null
+++ b/docs/site.md
@@ -0,0 +1,115 @@
1# Public Site Contract
2
3The public Bitflash sites are generated from this repository. If a deployed
4page and the source disagree, the deployed page is stale.
5
6## Canonical Navigation
7
8Every public page should use the same top navigation:
9
10| label | target |
11|---|---|
12| home | `https://bitflash.network/` |
13| code | `https://git.bitflash.network/bitflash/bitflash` |
14| docs | `https://docs.bitflash.network/` |
15| downloads | `https://releases.bitflash.network/` |
16| explorer | `https://explorer.bitflash.network/` |
17| status | `https://status.bitflash.network/` |
18
19The `code` link intentionally points at the repository, not the Forgejo
20instance root. A visitor choosing "code" wants the source tree.
21
22## Documentation Pages To Publish
23
24The docs index should expose these source files:
25
26| page | source | why it matters |
27|---|---|---|
28| Manual | `README.md` | run, mine, recover, operate |
29| Key derivation and address format | `docs/derivation.md` | independent wallet recovery |
30| Release verification | `docs/release-verification.md` | pinned signing-key flow |
31| Dependencies | `docs/dependencies.md` | pinned secp256k1/RandomX supply chain |
32| Rendezvous and .btf addressing | `docs/rendezvous.md` | network design and caveats |
33| Tor mode | `docs/tor.md` | `-tor`, `-socks`, onion relay operation |
34| Fair launch verifier | `docs/fair-launch.md` | no-premine launch audit |
35| UTXO set commitment | `docs/utxo-commitment.md` | deterministic supply/root audit |
36| UTXO inclusion proofs | `docs/utxo-proofs.md` | proof for one unspent output |
37| Mining calculator | `docs/mining-calculator.md` | local WhatToMine-style estimates |
38| Public pool directory | `docs/pool-directory.md` | `pool_status.json`, `pools.json`, ranking |
39
40## Release Mirror Aliases
41
42Each release remains under its versioned directory:
43
44```text
45https://releases.bitflash.network/v1.2.19/SHA256SUMS
46https://releases.bitflash.network/v1.2.19/SHA256SUMS.asc
47```
48
49The release root should also alias the latest files:
50
51```text
52https://releases.bitflash.network/SHA256SUMS
53https://releases.bitflash.network/SHA256SUMS.asc
54https://releases.bitflash.network/Bitflash-latest-windows.zip
55https://releases.bitflash.network/Bitflash-latest-x86_64.AppImage
56https://releases.bitflash.network/bitflash-node-latest-x86_64
57```
58
59The aliases are convenience only. Verification must still require the pinned
60release key documented in `docs/release-verification.md`.
61
62## Static Explorer
63
64`scripts/build-explorer.py` writes a static explorer with no backend:
65
66```text
67index.html
68style.css
69explorer.js
70logo.png
71blocks.json
72block/<height>.json
73```
74
75Serve it with a strict content policy:
76
77```text
78default-src 'none';
79img-src 'self';
80style-src 'self';
81script-src 'self';
82connect-src 'self';
83base-uri 'none';
84form-action 'none';
85frame-ancestors 'none';
86```
87
88The viewer must never use `innerHTML` for chain data. A malicious transaction
89must be rendered as text, not interpreted as markup.
90
91## Cache Policy
92
93Recommended defaults:
94
95| path | cache |
96|---|---|
97| HTML pages | `public, max-age=0, must-revalidate` |
98| `status.json` | `no-store` |
99| `blocks.json`, `block/*.json` | `public, max-age=0, must-revalidate` |
100| release assets | `public, max-age=14400, must-revalidate` |
101| `SHA256SUMS`, signatures, keys | `public, max-age=0, must-revalidate` |
102
103## Security Headers
104
105Recommended for all static pages:
106
107```text
108Strict-Transport-Security: max-age=31536000; includeSubDomains
109X-Content-Type-Options: nosniff
110Referrer-Policy: no-referrer
111Permissions-Policy: interest-cohort=(), geolocation=(), camera=(), microphone=()
112```
113
114Forgejo has its own application headers and should not blindly inherit the
115static-site CSP.
diff --git a/docs/tor.md b/docs/tor.md
index 087c822..0016f8b 100644
--- a/docs/tor.md
+++ b/docs/tor.md
@@ -25,15 +25,106 @@ bitflash -tor=127.0.0.1:9150
25bitflash -tor=[::1]:9050 25bitflash -tor=[::1]:9050
26``` 26```
27 27
28For a SOCKS5 proxy that is not Tor, use `-socks` directly:
29
30```bash
31bitflash -socks=127.0.0.1:9050
32bitflash -socks=[::1]:9050
33```
34
35If both `-tor` and `-socks` are present, Tor mode wins and the node prints a
36warning. That keeps a command line with `-tor` from silently becoming a custom
37proxy route.
38
39## Managed Tor mode
40
41`-managedtor` starts a Tor process for this node, writes a local `torrc`, creates
42a v3 hidden service for the normal Bitflash P2P listener, routes outbound
43discovery through that Tor instance, and signs the generated `.onion:8433`
44endpoint into the node's `.btf` descriptor.
45
46```bash
47bitflash -managedtor
48```
49
50Without a value, Bitflash looks for Tor in this order:
51
521. `tor/tor.exe` or `tor/tor` beside the Bitflash binary;
532. `tor.exe` or `tor` beside the Bitflash binary;
543. `tor.exe` or `tor` on `PATH`.
55
56Windows releases can also be built as `Bitflash-*-windows-with-tor.zip`. That
57package includes the official Tor Expert Bundle under `tor/`, so the first path
58above exists immediately after extraction and `bitflash -managedtor` needs no
59separate Tor install.
60
61The bundled-Tor package is created by:
62
63```bash
64make windows-tor
65```
66
67The packaging script verifies a pinned SHA256 for the downloaded Tor Expert
68Bundle before copying it into the release directory. When `gpg` is available it
69also verifies the matching `.asc` signature against the pinned Tor Browser
70Developers signing-key fingerprint. Release builds should use
71`TOR_VERIFY_GPG=required make windows-tor` to make that second check mandatory.
72The Tor binary is bundled; the runtime data directory, hidden-service key, and
73control cookie are still created under the user's Bitflash data directory.
74
75To point at a specific Tor executable:
76
77```bash
78bitflash -managedtor=/opt/tor/bin/tor
79bitflash -managedtor=C:\Tor Browser\Browser\TorBrowser\Tor\tor.exe
80```
81
82Managed Tor writes its state under the Bitflash data directory:
83
84```text
85managed-tor/
86 torrc
87 data/
88 onion-service/
89```
90
91The private key for the onion address lives in `managed-tor/onion-service/`.
92Back up that directory if you want the same onion address after moving the node.
93Delete it only if you intentionally want a fresh onion endpoint.
94
95`-managedtor` takes precedence over `-tor` and `-socks`, because it must choose
96the local SOCKS port it starts. Diagnostics report the current managed Tor
97state, including the SOCKS port and onion endpoint once Tor has generated it.
98
28When Tor mode is enabled: 99When Tor mode is enabled:
29 100
30- outbound Nostr relay dials use Tor; 101- outbound Nostr relay dials use Tor;
31- outbound `.btf` rendezvous relay dials use Tor; 102- outbound `.btf` rendezvous relay dials use Tor;
103- outbound direct peer dials prefer signed `.onion` endpoints when a peer
104 advertises one;
32- destination names are sent to Tor as SOCKS5 domain-name CONNECT requests, so 105- destination names are sent to Tor as SOCKS5 domain-name CONNECT requests, so
33 the local DNS resolver does not see them; 106 the local DNS resolver does not see them;
34- direct `.onion` dials are refused unless a SOCKS5/Tor proxy is enabled; 107- direct `.onion` dials are refused unless a SOCKS5/Tor proxy is enabled;
35- plain HTTP external-IP probes are skipped. 108- plain HTTP external-IP probes are skipped.
36 109
110Tor mode affects outbound discovery and relay dials. It does not encrypt
111`wallet.dat`, hide mining rewards on-chain, or change the `.btf` identity. A
112relay still sees the TCP client that connected to it; with Tor mode that client
113is the Tor exit or onion circuit endpoint instead of the user's direct IP.
114
115## What to test
116
117Use the public status page only as a rough health check. To prove the local node
118is using Tor/SOCKS, test from the node machine:
119
120```bash
121bitflash -tor -debug
122```
123
124Then confirm in `debug.log` that Nostr and rendezvous dials report the proxy
125path. A DNS leak test should show no local resolver lookup for relay hostnames:
126the SOCKS5 request uses the domain-name form and asks the proxy to resolve it.
127
37## Onion rendezvous relay 128## Onion rendezvous relay
38 129
39An operator can expose a Bitflash rendezvous relay as a Tor hidden service. The 130An operator can expose a Bitflash rendezvous relay as a Tor hidden service. The
@@ -69,16 +160,72 @@ bitflash -operator -tor \
69 -announcerelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434 160 -announcerelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434
70``` 161```
71 162
72## Full-node hidden service 163## Manual direct full-node hidden service
73 164
74The legacy P2P listener still binds to port `8433`. You can expose it through 165Managed Tor is the preferred path for ordinary nodes. Operators can still expose
75Tor manually: 166the P2P listener through a separately managed Tor service:
76 167
77```text 168```text
78HiddenServiceDir /var/lib/tor/bitflash-node/ 169HiddenServiceDir /var/lib/tor/bitflash-node/
79HiddenServicePort 8433 127.0.0.1:8433 170HiddenServicePort 8433 127.0.0.1:8433
80``` 171```
81 172
82Current automatic peer discovery is `.btf`/rendezvous based, not onion-address 173After restarting Tor, read the generated onion name:
83gossip. Treat direct full-node onion access as an operator/admin tool unless a 174
84future release adds first-class onion peer advertisements. 175```bash
176sudo cat /var/lib/tor/bitflash-node/hostname
177```
178
179Then start Bitflash with Tor outbound enabled and advertise that hidden service:
180
181```bash
182bitflash -tor \
183 -onionservice=exampleexampleexampleexampleexampleexampleexampleexample.onion:8433
184```
185
186That onion endpoint is signed into this node's self-certifying `.btf`
187descriptor. New peers that resolve the descriptor and also run with `-tor` try
188the `.onion:8433` P2P connection first. If it fails, they fall back to the
189ordinary encrypted rendezvous path. Older nodes ignore the onion field and still
190use the rendezvous relay because the descriptor keeps the legacy signature too.
191
192If the node has `-onionservice` but has not registered at any rendezvous relay
193yet, it can still publish an onion-capable descriptor with
194`meeting_node = rendezvous-pending`. Tor-capable peers can dial the hidden
195service directly from that descriptor. Rendezvous becomes a fallback, not a
196precondition for being discoverable.
197
198This means rendezvous relays stop being the only way to reach a node that has
199published a hidden service. They remain useful bootstrap/fallback infrastructure,
200but an already-discovered onion-capable peer can be reached directly over Tor.
201
202Tor owns the private key under `HiddenServiceDir`; back it up if you want the
203onion name to stay stable.
204
205## Release smoke test
206
207After building a Windows binary with managed Tor support, run the two-node smoke
208test before cutting a release:
209
210```powershell
211powershell -ExecutionPolicy Bypass -File scripts\smoke-managed-tor-onion.ps1 `
212 -Bitflash .\src\bitflash.exe `
213 -Tor C:\path\to\tor.exe
214```
215
216The script creates two temporary datadirs, starts node A with `-managedtor`,
217waits for it to publish its `.btf` identity and generated `.onion` endpoint,
218then starts node B with its own `-managedtor`, `-oniononly`, and
219`-connectbtf=<node-a>`. It passes only when node B logs that it connected to
220node A `via direct onion`; rendezvous fallback cannot satisfy the test.
221
222By default the script waits 120 seconds after node A's descriptor self-resolves
223before it starts node B. Tor can create the local `hostname` before the hidden
224service descriptor is visible enough for client circuits, and early dials often
225fail with SOCKS5 reply `0x01`. Override this with `-OnionWarmupSec N` when
226testing a slow or already-warmed Tor environment.
227
228This test still uses Nostr for descriptor discovery, because that is how nodes
229learn each other's signed `.btf` descriptors. It does not require a Bitflash
230rendezvous relay to carry the P2P connection: once the descriptor includes the
231signed onion endpoint, the actual node connection goes directly through Tor.
diff --git a/scripts/build-explorer.py b/scripts/build-explorer.py
index 97a7f4e..dbb8e17 100755
--- a/scripts/build-explorer.py
+++ b/scripts/build-explorer.py
@@ -4,6 +4,7 @@
4import argparse 4import argparse
5import hashlib 5import hashlib
6import json 6import json
7import shutil
7import sys 8import sys
8from pathlib import Path 9from pathlib import Path
9 10
@@ -148,6 +149,11 @@ def build_explorer(raw_blocks, out_dir):
148 encoding="ascii", 149 encoding="ascii",
149 ) 150 )
150 (out / "index.html").write_text(INDEX_HTML, encoding="utf-8") 151 (out / "index.html").write_text(INDEX_HTML, encoding="utf-8")
152 (out / "style.css").write_text(STYLE_CSS, encoding="utf-8")
153 (out / "explorer.js").write_text(EXPLORER_JS, encoding="utf-8")
154 logo = Path(__file__).resolve().parents[1] / "docs" / "logo.png"
155 if logo.exists():
156 shutil.copyfile(logo, out / "logo.png")
151 return len(blocks), len(blocks) - 1 157 return len(blocks), len(blocks) - 1
152 158
153 159
@@ -167,43 +173,26 @@ INDEX_HTML = r"""<!doctype html>
167<meta charset="utf-8"> 173<meta charset="utf-8">
168<meta name="viewport" content="width=device-width, initial-scale=1"> 174<meta name="viewport" content="width=device-width, initial-scale=1">
169<title>Bitflash Block Explorer</title> 175<title>Bitflash Block Explorer</title>
170<style> 176<meta name="description" content="Read-only Bitflash block explorer generated from local blk*.dat files.">
171 :root { color-scheme: light dark; --bg:#fff; --fg:#111; --mut:#666; --line:#e2e2e2; --card:#fafafa; --acc:#2b6cb0; } 177<link rel="icon" href="logo.png" type="image/png">
172 @media (prefers-color-scheme: dark){ :root{ --bg:#0e0f11; --fg:#e6e6e6; --mut:#9aa0a6; --line:#26282c; --card:#16181b; --acc:#63b3ed; } } 178<link rel="stylesheet" href="style.css">
173 * { box-sizing: border-box; } 179<script src="explorer.js" defer></script>
174 body { font: 15px/1.5 system-ui, sans-serif; margin:0; background:var(--bg); color:var(--fg); }
175 header { padding:20px 24px; border-bottom:1px solid var(--line); display:flex; gap:16px; align-items:baseline; flex-wrap:wrap; }
176 h1 { font-size:18px; margin:0; }
177 h2 { font-size:16px; }
178 .mut { color:var(--mut); }
179 .wrap { padding:16px 24px; max-width:1100px; margin:0 auto; }
180 input { width:100%; max-width:420px; padding:9px 12px; border:1px solid var(--line); border-radius:8px; background:var(--card); color:var(--fg); font-size:14px; }
181 table { width:100%; border-collapse:collapse; margin-top:12px; }
182 th,td { text-align:left; padding:9px 10px; border-bottom:1px solid var(--line); font-variant-numeric:tabular-nums; }
183 th { font-size:12px; text-transform:uppercase; letter-spacing:.05em; color:var(--mut); }
184 tr.blk { cursor:pointer; }
185 tr.blk:hover td { background:var(--card); }
186 code { font-size:12px; word-break:break-all; }
187 .scroll { overflow-x:auto; }
188 .card { border:1px solid var(--line); border-radius:10px; background:var(--card); padding:14px 16px; margin-top:12px; }
189 .kv { display:grid; grid-template-columns:150px 1fr; gap:4px 12px; font-size:13px; }
190 .kv div:nth-child(odd){ color:var(--mut); }
191 .tx { border-top:1px solid var(--line); padding:10px 0; }
192 .io { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
193 @media (max-width:640px){ .io{ grid-template-columns:1fr; } .kv{ grid-template-columns:110px 1fr; } }
194 .pill { display:inline-block; font-size:11px; padding:1px 7px; border-radius:99px; border:1px solid var(--line); color:var(--mut); }
195 button.link { background:none; border:none; color:var(--acc); cursor:pointer; font:inherit; padding:0; }
196 .note { color:var(--mut); font-size:13px; max-width:820px; }
197</style>
198</head> 180</head>
199<body> 181<body>
200<header> 182<header class="topbar">
201 <h1>Bitflash Block Explorer</h1> 183 <a class="brand" href="https://bitflash.network/"><img src="logo.png" width="22" height="22" alt="">Bitflash</a>
202 <span class="mut" id="tip"></span> 184 <nav aria-label="Bitflash sites">
203 <span style="flex:1"></span> 185 <a href="https://bitflash.network/">home</a>
204 <button class="link" id="home" style="display:none">back to blocks</button> 186 <a href="https://git.bitflash.network/bitflash/bitflash">code</a>
187 <a href="https://docs.bitflash.network/">docs</a>
188 <a href="https://releases.bitflash.network/">downloads</a>
189 <a href="https://explorer.bitflash.network/" aria-current="page">explorer</a>
190 <a href="https://status.bitflash.network/">status</a>
191 </nav>
192 <span class="meta" id="tip"></span>
205</header> 193</header>
206<div class="wrap"> 194<div class="wrap">
195 <h1>Block explorer <button class="link" id="home" hidden>back to blocks</button></h1>
207 <p class="note">Read-only view of the main chain: blocks, their transactions, and the coinbase payout. There is no mempool and no per-address history here.</p> 196 <p class="note">Read-only view of the main chain: blocks, their transactions, and the coinbase payout. There is no mempool and no per-address history here.</p>
208 <div id="listview"> 197 <div id="listview">
209 <input id="q" placeholder="Search by height or block hash"> 198 <input id="q" placeholder="Search by height or block hash">
@@ -213,9 +202,63 @@ INDEX_HTML = r"""<!doctype html>
213 </table></div> 202 </table></div>
214 <p class="mut" id="more"></p> 203 <p class="mut" id="more"></p>
215 </div> 204 </div>
216 <div id="detail" style="display:none"></div> 205 <div id="detail" hidden></div>
217</div> 206</div>
218<script> 207</body>
208</html>
209"""
210
211
212STYLE_CSS = r""":root{
213 color-scheme: light dark;
214 --bg:#fff; --bg-2:#f6f6f6; --fg:#111; --dim:#6b6b6b; --line:#d7d7d7; --line-hard:#111;
215 --accent:#0b7a34; --accent-2:#12a047; --bar:3rem;
216 --mono: ui-monospace,"Cascadia Mono","Segoe UI Mono",Consolas,"DejaVu Sans Mono","Liberation Mono","Courier New",monospace;
217}
218@media (prefers-color-scheme: dark){:root{
219 --bg:#000; --bg-2:#0b0b0b; --fg:#dcdcdc; --dim:#8a8a8a; --line:#242424; --line-hard:#4a4a4a;
220 --accent:#2ee06a; --accent-2:#6bff9f;
221}}
222*{box-sizing:border-box;}
223html,body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.55 var(--mono);-webkit-text-size-adjust:100%;}
224a{color:var(--accent);}
225.topbar{display:flex;align-items:center;gap:1rem;flex-wrap:wrap;min-height:var(--bar);
226 padding:.5rem clamp(.9rem,2.5vw,3rem);border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:5;}
227.brand{display:flex;align-items:center;gap:.6rem;text-decoration:none;color:var(--fg);font-weight:bold;}
228.brand img{width:22px;height:22px;display:block;}
229.topbar nav{display:flex;flex-wrap:wrap;gap:.15rem 1.4rem;font-size:.86rem;}
230.topbar nav a{text-decoration:none;color:var(--dim);}
231.topbar nav a::before{content:"/";color:var(--line-hard);margin-right:.3rem;}
232.topbar nav a:hover{color:var(--fg);text-decoration:underline;}
233.topbar nav a[aria-current="page"]{color:var(--accent);font-weight:bold;}
234.topbar nav a[aria-current="page"]::before{color:var(--accent);}
235.topbar .meta{margin-left:auto;font-size:.76rem;color:var(--dim);letter-spacing:.06em;}
236.wrap{padding:1.4rem clamp(.9rem,2.5vw,3rem) 4rem;}
237h1{font-size:1rem;margin:0 0 .3rem;letter-spacing:.02em;}
238h2{font-size:.95rem;margin:1.2rem 0 .4rem;}
239.mut{color:var(--dim);}
240.note{color:var(--dim);font-size:.82rem;max-width:60rem;margin:.2rem 0 1rem;}
241input{width:100%;max-width:28rem;padding:.5rem .7rem;border:1px solid var(--line);background:var(--bg-2);color:var(--fg);font:inherit;font-size:.86rem;}
242input:focus{outline:none;border-color:var(--accent);}
243.scroll{overflow-x:auto;}
244table{width:100%;border-collapse:collapse;margin-top:.8rem;font-size:.86rem;}
245th,td{text-align:left;padding:.5rem .6rem;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums;}
246th{font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;color:var(--dim);font-weight:normal;}
247tr.blk{cursor:pointer;}
248tr.blk:hover td{background:var(--bg-2);}
249code{font-size:.8rem;word-break:break-all;color:var(--fg);}
250.card{border:1px solid var(--line);background:var(--bg-2);padding:.9rem 1rem;margin-top:.8rem;}
251.kv{display:grid;grid-template-columns:11rem 1fr;gap:.25rem .8rem;font-size:.82rem;}
252.kv div:nth-child(odd){color:var(--dim);}
253.tx{border-top:1px solid var(--line);padding:.7rem 0;}
254.io{display:grid;grid-template-columns:1fr 1fr;gap:1rem;}
255@media (max-width:640px){.io{grid-template-columns:1fr;}.kv{grid-template-columns:7rem 1fr;}.topbar .meta{margin-left:0;width:100%;}}
256.pill{display:inline-block;font-size:.68rem;padding:.05rem .5rem;border:1px solid var(--line);color:var(--dim);text-transform:uppercase;letter-spacing:.05em;}
257button.link{background:none;border:none;color:var(--accent);cursor:pointer;font:inherit;padding:0;text-decoration:underline;}
258"""
259
260
261EXPLORER_JS = r"""
219var SUM = [], SHOWN = 200; 262var SUM = [], SHOWN = 200;
220function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; } 263function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; }
221function ts(t){ return new Date(t*1000).toISOString().replace('T',' ').replace('.000Z',' UTC'); } 264function ts(t){ return new Date(t*1000).toISOString().replace('T',' ').replace('.000Z',' UTC'); }
@@ -310,25 +353,22 @@ function openBlock(h){
310 })); 353 }));
311 append(tx, head, io); append(detail, tx); 354 append(tx, head, io); append(detail, tx);
312 }); 355 });
313 document.getElementById('listview').style.display='none'; 356 document.getElementById('listview').hidden = true;
314 detail.style.display='block'; 357 detail.hidden = false;
315 document.getElementById('home').style.display='inline'; 358 document.getElementById('home').hidden = false;
316 window.scrollTo(0,0); 359 window.scrollTo(0,0);
317 }); 360 });
318} 361}
319document.getElementById('home').onclick=function(){ 362document.getElementById('home').onclick=function(){
320 document.getElementById('detail').style.display='none'; 363 document.getElementById('detail').hidden = true;
321 document.getElementById('listview').style.display='block'; 364 document.getElementById('listview').hidden = false;
322 this.style.display='none'; 365 this.hidden = true;
323}; 366};
324document.getElementById('q').oninput=function(){ renderList(this.value); }; 367document.getElementById('q').oninput=function(){ renderList(this.value); };
325fetch('blocks.json').then(function(r){return r.json();}).then(function(d){ 368fetch('blocks.json').then(function(r){return r.json();}).then(function(d){
326 SUM=d.blocks; document.getElementById('tip').textContent='tip height '+d.tipHeight+' - '+d.count+' blocks'; 369 SUM=d.blocks; document.getElementById('tip').textContent='tip height '+d.tipHeight+' - '+d.count+' blocks';
327 renderList(''); 370 renderList('');
328}); 371});
329</script>
330</body>
331</html>
332""" 372"""
333 373
334 374
diff --git a/scripts/make-release-checksums.sh b/scripts/make-release-checksums.sh
index 5b29e2d..9c4f45a 100755
--- a/scripts/make-release-checksums.sh
+++ b/scripts/make-release-checksums.sh
@@ -10,6 +10,7 @@ Usage: scripts/make-release-checksums.sh [--sign] [--local-user KEYID]
10 10
11Creates SHA256SUMS for release assets in the current directory: 11Creates SHA256SUMS for release assets in the current directory:
12 Bitflash-*-windows.zip 12 Bitflash-*-windows.zip
13 Bitflash-*-windows-with-tor.zip
13 Bitflash-*-x86_64.AppImage 14 Bitflash-*-x86_64.AppImage
14 bitflash-node-*-x86_64 15 bitflash-node-*-x86_64
15 16
@@ -44,6 +45,7 @@ fi
44assets=() 45assets=()
45for pattern in \ 46for pattern in \
46 "Bitflash-*-windows.zip" \ 47 "Bitflash-*-windows.zip" \
48 "Bitflash-*-windows-with-tor.zip" \
47 "Bitflash-*-x86_64.AppImage" \ 49 "Bitflash-*-x86_64.AppImage" \
48 "bitflash-node-*-x86_64" 50 "bitflash-node-*-x86_64"
49do 51do
diff --git a/scripts/package-windows-tor.sh b/scripts/package-windows-tor.sh
new file mode 100755
index 0000000..c8cca3b
--- /dev/null
+++ b/scripts/package-windows-tor.sh
@@ -0,0 +1,187 @@
1#!/usr/bin/env bash
2set -euo pipefail
3
4ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5VERSION="${VERSION:-$(sed -n 's/^VERSION[[:space:]]*:= *//p' "$ROOT/Makefile" | head -1)}"
6
7TOR_VERSION="${TOR_VERSION:-15.0.19}"
8TOR_ARCHIVE_NAME="tor-expert-bundle-windows-x86_64-${TOR_VERSION}.tar.gz"
9TOR_URL="${TOR_URL:-https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/${TOR_ARCHIVE_NAME}}"
10TOR_SHA256="${TOR_SHA256:-6AC067402C7B4A3DC37887ED3754B3914B67FDC220C966190683E9CCF91ABF0F}"
11TOR_ASC_URL="${TOR_ASC_URL:-${TOR_URL}.asc}"
12TOR_SIGNING_KEY_FINGERPRINT="${TOR_SIGNING_KEY_FINGERPRINT:-EF6E286DDA85EA2A4BA7DE684E2C6E8793298290}"
13TOR_VERIFY_GPG="${TOR_VERIFY_GPG:-auto}"
14
15BUNDLE_DIR="${BUNDLE_DIR:-$ROOT/Bitflash-${VERSION}-windows}"
16OUT_ZIP="${OUT_ZIP:-$ROOT/Bitflash-${VERSION}-windows-with-tor.zip}"
17CACHE_DIR="${CACHE_DIR:-$ROOT/.cache}"
18ARCHIVE="${TOR_ARCHIVE:-$CACHE_DIR/$TOR_ARCHIVE_NAME}"
19ASC_FILE="${TOR_ASC:-$ARCHIVE.asc}"
20
21usage() {
22 cat <<EOF
23Usage: scripts/package-windows-tor.sh
24
25Creates Bitflash-${VERSION}-windows-with-tor.zip from an existing
26Bitflash-${VERSION}-windows/ release directory.
27
28Environment overrides:
29 VERSION Bitflash release version, default read from Makefile
30 TOR_VERSION Tor Expert Bundle version, default ${TOR_VERSION}
31 TOR_URL Download URL, default official Tor archive URL
32 TOR_ASC_URL Signature URL, default TOR_URL.asc
33 TOR_SHA256 Expected archive SHA256
34 TOR_ARCHIVE Use an existing archive instead of downloading
35 TOR_ASC Use an existing detached signature
36 TOR_VERIFY_GPG auto, required, or off; default auto
37 BUNDLE_DIR Existing unpacked Windows release directory
38 OUT_ZIP Output zip path
39EOF
40}
41
42if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
43 usage
44 exit 0
45fi
46
47need() {
48 command -v "$1" >/dev/null 2>&1 || {
49 echo "missing required command: $1" >&2
50 exit 1
51 }
52}
53
54need sha256sum
55need tar
56need zip
57
58download() {
59 local url="$1"
60 local out="$2"
61 if command -v curl >/dev/null 2>&1; then
62 curl -L --fail --output "$out" "$url"
63 elif command -v wget >/dev/null 2>&1; then
64 wget -O "$out" "$url"
65 else
66 echo "missing required command: curl or wget" >&2
67 exit 1
68 fi
69}
70
71if [ ! -d "$BUNDLE_DIR" ]; then
72 echo "missing Windows release directory: $BUNDLE_DIR" >&2
73 echo "run 'make windows' first, or set BUNDLE_DIR" >&2
74 exit 1
75fi
76if [ ! -f "$BUNDLE_DIR/Bitflash.exe" ]; then
77 echo "missing Bitflash.exe in $BUNDLE_DIR" >&2
78 exit 1
79fi
80BUNDLE_PARENT="$(cd "$(dirname "$BUNDLE_DIR")" && pwd)"
81BUNDLE_NAME="$(basename "$BUNDLE_DIR")"
82OUT_PARENT="$(mkdir -p "$(dirname "$OUT_ZIP")" && cd "$(dirname "$OUT_ZIP")" && pwd)"
83OUT_NAME="$(basename "$OUT_ZIP")"
84OUT_ZIP_ABS="$OUT_PARENT/$OUT_NAME"
85
86mkdir -p "$CACHE_DIR"
87if [ ! -f "$ARCHIVE" ]; then
88 download "$TOR_URL" "$ARCHIVE"
89fi
90
91got="$(sha256sum "$ARCHIVE" | awk '{print tolower($1)}')"
92want="$(printf '%s' "$TOR_SHA256" | tr 'A-F' 'a-f')"
93if [ "$got" != "$want" ]; then
94 echo "Tor archive SHA256 mismatch" >&2
95 echo " expected: $want" >&2
96 echo " got: $got" >&2
97 exit 1
98fi
99
100tmp="$(mktemp -d "${TMPDIR:-/tmp}/bitflash-tor-package.XXXXXX")"
101trap 'rm -rf "$tmp"' EXIT
102
103case "$TOR_VERIFY_GPG" in
104 auto|required|off) ;;
105 *) echo "TOR_VERIFY_GPG must be auto, required, or off" >&2; exit 2 ;;
106esac
107
108if [ "$TOR_VERIFY_GPG" != "off" ]; then
109 if command -v gpg >/dev/null 2>&1; then
110 if [ ! -f "$ASC_FILE" ]; then
111 download "$TOR_ASC_URL" "$ASC_FILE"
112 fi
113 gnupg_home="$tmp/gnupg"
114 mkdir -p "$gnupg_home"
115 chmod 0700 "$gnupg_home"
116 gpg --homedir "$gnupg_home" --batch --auto-key-locate nodefault,wkd,keyserver \
117 --locate-keys torbrowser@torproject.org >/dev/null 2>&1 || {
118 if [ "$TOR_VERIFY_GPG" = "required" ]; then
119 echo "could not import Tor Browser Developers signing key" >&2
120 exit 1
121 fi
122 echo "WARNING: skipped Tor GPG signature verification; signing key import failed" >&2
123 }
124 if gpg --homedir "$gnupg_home" --batch --with-colons --fingerprint "$TOR_SIGNING_KEY_FINGERPRINT" \
125 2>/dev/null | grep -q "^fpr:::::::::${TOR_SIGNING_KEY_FINGERPRINT}:"; then
126 gpg --homedir "$gnupg_home" --batch --verify "$ASC_FILE" "$ARCHIVE" >/dev/null 2>&1 || {
127 echo "Tor archive GPG signature verification failed" >&2
128 exit 1
129 }
130 echo "Tor archive GPG signature verified against ${TOR_SIGNING_KEY_FINGERPRINT}"
131 elif [ "$TOR_VERIFY_GPG" = "required" ]; then
132 echo "Tor signing key fingerprint mismatch" >&2
133 exit 1
134 else
135 echo "WARNING: skipped Tor GPG signature verification; signing key fingerprint was not available" >&2
136 fi
137 elif [ "$TOR_VERIFY_GPG" = "required" ]; then
138 echo "missing required command for TOR_VERIFY_GPG=required: gpg" >&2
139 exit 1
140 else
141 echo "WARNING: gpg not found; relying on pinned SHA256 only" >&2
142 fi
143fi
144
145mkdir -p "$tmp/extract"
146tar -xzf "$ARCHIVE" -C "$tmp/extract"
147
148if [ ! -f "$tmp/extract/tor/tor.exe" ]; then
149 echo "Tor archive did not contain tor/tor.exe" >&2
150 exit 1
151fi
152
153rm -rf "$BUNDLE_DIR/tor"
154mkdir -p "$BUNDLE_DIR/tor"
155cp -R "$tmp/extract/tor/." "$BUNDLE_DIR/tor/"
156if [ -d "$tmp/extract/docs" ]; then
157 mkdir -p "$BUNDLE_DIR/tor/docs"
158 cp -R "$tmp/extract/docs/." "$BUNDLE_DIR/tor/docs/"
159fi
160if [ -d "$tmp/extract/data" ]; then
161 mkdir -p "$BUNDLE_DIR/tor/data-defaults"
162 cp -R "$tmp/extract/data/." "$BUNDLE_DIR/tor/data-defaults/"
163fi
164
165cat > "$BUNDLE_DIR/TOR-EXPERT-BUNDLE.txt" <<EOF
166Bitflash managed Tor bundle
167
168This Windows package includes the Tor Expert Bundle so -managedtor can start
169Tor without requiring a separate Tor install.
170
171Tor Expert Bundle: ${TOR_VERSION}
172Archive URL: ${TOR_URL}
173Archive SHA256: ${TOR_SHA256}
174
175Bitflash uses tor/tor.exe from this package. Runtime state, hidden-service keys,
176and control authentication cookies are written under the user's Bitflash data
177directory, not inside this release directory.
178EOF
179
180rm -f "$OUT_ZIP_ABS"
181(
182 cd "$BUNDLE_PARENT"
183 zip -qr "$OUT_ZIP_ABS" "$BUNDLE_NAME"
184)
185
186echo "Bundled Tor Expert Bundle ${TOR_VERSION}"
187echo "Built: $OUT_ZIP_ABS"
diff --git a/scripts/smoke-managed-tor-onion.ps1 b/scripts/smoke-managed-tor-onion.ps1
new file mode 100644
index 0000000..70658c7
--- /dev/null
+++ b/scripts/smoke-managed-tor-onion.ps1
@@ -0,0 +1,174 @@
1param(
2 [string]$Bitflash,
3 [string]$Tor,
4 [string]$WorkDir,
5 [int]$TimeoutSec = 600,
6 [int]$OnionWarmupSec = 120
7)
8
9$ErrorActionPreference = "Stop"
10
11function Resolve-RepoRoot {
12 $scriptDir = Split-Path -Parent $PSCommandPath
13 return (Resolve-Path (Join-Path $scriptDir "..")).Path
14}
15
16function Get-FreeTcpPort {
17 $listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0)
18 $listener.Start()
19 try {
20 return ([Net.IPEndPoint]$listener.LocalEndpoint).Port
21 }
22 finally {
23 $listener.Stop()
24 }
25}
26
27function Wait-RegexInFile {
28 param(
29 [string]$Path,
30 [string]$Pattern,
31 [datetime]$Deadline,
32 [string]$Label
33 )
34
35 while ((Get-Date) -lt $Deadline) {
36 if (Test-Path $Path) {
37 $text = Get-Content -Raw -ErrorAction SilentlyContinue $Path
38 if ($null -ne $text) {
39 $m = [regex]::Match($text, $Pattern)
40 if ($m.Success) {
41 return $m
42 }
43 }
44 }
45 Start-Sleep -Milliseconds 500
46 }
47 throw "timed out waiting for $Label"
48}
49
50function Stop-TestProcesses {
51 param(
52 [System.Diagnostics.Process[]]$Processes,
53 [string[]]$DataDirKeys
54 )
55
56 foreach ($p in $Processes) {
57 try {
58 $p.Refresh()
59 if (!$p.HasExited) {
60 Stop-Process -Id $p.Id -Force
61 }
62 }
63 catch {}
64 }
65
66 foreach ($key in $DataDirKeys) {
67 if ([string]::IsNullOrWhiteSpace($key)) {
68 continue
69 }
70 try {
71 Get-CimInstance Win32_Process -Filter "name = 'tor.exe'" |
72 Where-Object { $_.CommandLine -like "*$key*" } |
73 ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
74 }
75 catch {}
76 }
77}
78
79$repo = Resolve-RepoRoot
80if ([string]::IsNullOrWhiteSpace($Bitflash)) {
81 $Bitflash = Join-Path $repo "src\bitflash.exe"
82}
83if ([string]::IsNullOrWhiteSpace($Tor)) {
84 $candidates = @(
85 (Join-Path $repo "src\tor\tor.exe"),
86 (Join-Path $repo "Bitflash-$(Select-String -Path (Join-Path $repo 'Makefile') -Pattern '^VERSION[ ]*:=' | ForEach-Object { ($_.Line -split ':=')[1].Trim() })-windows\tor\tor.exe")
87 )
88 foreach ($candidate in $candidates) {
89 if (Test-Path $candidate) {
90 $Tor = $candidate
91 break
92 }
93 }
94}
95if ([string]::IsNullOrWhiteSpace($WorkDir)) {
96 $WorkDir = Join-Path $env:TEMP ("bitflash-managed-tor-two-node-" + (Get-Date -Format "yyyyMMddHHmmss"))
97}
98
99if (!(Test-Path $Bitflash)) {
100 throw "Bitflash binary not found: $Bitflash"
101}
102if (!(Test-Path $Tor)) {
103 throw "Tor binary not found. Pass -Tor or build the Windows-with-Tor package."
104}
105
106New-Item -ItemType Directory -Force -Path $WorkDir | Out-Null
107$dataA = Join-Path $WorkDir "node-a"
108$dataB = Join-Path $WorkDir "node-b"
109New-Item -ItemType Directory -Force -Path $dataA | Out-Null
110New-Item -ItemType Directory -Force -Path $dataB | Out-Null
111
112$portA = Get-FreeTcpPort
113$portB = Get-FreeTcpPort
114$deadline = (Get-Date).AddSeconds($TimeoutSec)
115$oldPath = $env:PATH
116$env:PATH = "C:\msys64\ucrt64\bin;C:\msys64\usr\bin;" + $env:PATH
117
118$procA = $null
119$procB = $null
120$dataKeys = @((Split-Path $WorkDir -Leaf))
121
122try {
123 $argsA = @(
124 "-datadir=$dataA",
125 "-port=$portA",
126 "-managedtor=$Tor",
127 "-nogui",
128 "-debug",
129 "-checkblocks=0"
130 )
131 $procA = Start-Process -FilePath $Bitflash -ArgumentList $argsA -WorkingDirectory (Split-Path $Bitflash -Parent) -PassThru -WindowStyle Hidden
132 $logA = Join-Path $dataA "debug.log"
133
134 $btfMatch = Wait-RegexInFile -Path $logA -Pattern "Nostr: node \.btf address = ([a-z2-7]+\.btf)" -Deadline $deadline -Label "node A .btf address"
135 $btfA = $btfMatch.Groups[1].Value
136
137 $onionMatch = Wait-RegexInFile -Path $logA -Pattern "Managed Tor hidden service ready: ([a-z2-7]{56}\.onion:\d+)" -Deadline $deadline -Label "node A managed onion"
138 $onionA = $onionMatch.Groups[1].Value
139
140 Wait-RegexInFile -Path $logA -Pattern "Nostr: \.btf self-resolve OK via .*" -Deadline $deadline -Label "node A descriptor self-resolve" | Out-Null
141 if ($OnionWarmupSec -gt 0) {
142 Write-Host "Waiting $OnionWarmupSec second(s) for Tor hidden-service descriptor propagation"
143 Start-Sleep -Seconds $OnionWarmupSec
144 }
145
146 $argsB = @(
147 "-datadir=$dataB",
148 "-port=$portB",
149 "-managedtor=$Tor",
150 "-oniononly",
151 "-connectbtf=$btfA",
152 "-nogui",
153 "-debug",
154 "-checkblocks=0"
155 )
156 $procB = Start-Process -FilePath $Bitflash -ArgumentList $argsB -WorkingDirectory (Split-Path $Bitflash -Parent) -PassThru -WindowStyle Hidden
157 $logB = Join-Path $dataB "debug.log"
158
159 Wait-RegexInFile -Path $logB -Pattern ("connected " + [regex]::Escape($btfA) + " via direct onion ") -Deadline $deadline -Label "node B direct onion connection to node A" | Out-Null
160
161 Write-Host "Managed Tor two-node onion smoke test passed"
162 Write-Host " node A .btf: $btfA"
163 Write-Host " node A onion: $onionA"
164 Write-Host " node A port: $portA"
165 Write-Host " node B port: $portB"
166 Write-Host " work dir: $WorkDir"
167}
168finally {
169 $env:PATH = $oldPath
170 $procs = @()
171 if ($null -ne $procA) { $procs += $procA }
172 if ($null -ne $procB) { $procs += $procB }
173 Stop-TestProcesses -Processes $procs -DataDirKeys $dataKeys
174}
diff --git a/src/Makefile b/src/Makefile
index 89ba559..b068376 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -53,7 +53,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \
53HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 53HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
54 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 54 key.h bignum.h script.h db.h base58.h net.h nostr.h \
55 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ 55 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \
56 selftest.h walletcmd.h compat.h sockcount.h proxy.h font_roboto.h 56 selftest.h walletcmd.h compat.h sockcount.h proxy.h tor.h font_roboto.h
57 57
58IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ 58IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
59 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o 59 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o
@@ -61,7 +61,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
61NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 61NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
62 obj/sha.o obj/nostr.o obj/randomx_pow.o \ 62 obj/sha.o obj/nostr.o obj/randomx_pow.o \
63 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 63 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
64 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o 64 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o
65 65
66GUI_OBJS = obj/gui.o obj/main_gui.o 66GUI_OBJS = obj/gui.o obj/main_gui.o
67 67
@@ -195,5 +195,6 @@ tests: test_bip32 bitflash-node
195 ./bitflash-node -selftest=payout-ledger 195 ./bitflash-node -selftest=payout-ledger
196 ./bitflash-node -selftest=parse-money 196 ./bitflash-node -selftest=parse-money
197 ./bitflash-node -selftest=socks5-proxy 197 ./bitflash-node -selftest=socks5-proxy
198 ./bitflash-node -selftest=managed-tor
198 199
199.PHONY: tests fuzz-net-message-smoke fuzz-script-smoke 200.PHONY: tests fuzz-net-message-smoke fuzz-script-smoke
diff --git a/src/btfaddr.cpp b/src/btfaddr.cpp
index 6edcc91..ce2ac86 100644
--- a/src/btfaddr.cpp
+++ b/src/btfaddr.cpp
@@ -19,6 +19,8 @@
19#include <cstring> 19#include <cstring>
20#include <algorithm> 20#include <algorithm>
21#include <cctype> 21#include <cctype>
22#include <cerrno>
23#include <cstdlib>
22 24
23using json = nlohmann::json; 25using json = nlohmann::json;
24 26
@@ -166,10 +168,47 @@ static bool HexDecode(const std::string& s, unsigned char* out, size_t n)
166 return true; 168 return true;
167} 169}
168 170
171bool NormalizeOnionEndpoint(const std::string& in, std::string& out)
172{
173 out.clear();
174 std::string s = ToLowerTrim(in);
175 size_t colon = s.rfind(':');
176 if (colon == std::string::npos || colon == 0 || colon + 1 >= s.size())
177 return false;
178
179 std::string host = s.substr(0, colon);
180 std::string port = s.substr(colon + 1);
181 while (!host.empty() && host.back() == '.') host.pop_back();
182 static const char* suffix = ".onion";
183 const size_t suffixLen = strlen(suffix);
184 if (host.size() != 56 + suffixLen)
185 return false;
186 if (host.compare(host.size() - suffixLen, suffixLen, suffix) != 0)
187 return false;
188 for (size_t i = 0; i < 56; i++)
189 {
190 char c = host[i];
191 if (!((c >= 'a' && c <= 'z') || (c >= '2' && c <= '7')))
192 return false;
193 }
194 for (size_t i = 0; i < port.size(); i++)
195 if (!isdigit((unsigned char)port[i]))
196 return false;
197
198 errno = 0;
199 char* end = NULL;
200 long nPort = strtol(port.c_str(), &end, 10);
201 if (errno == ERANGE || end == NULL || *end != '\0' || nPort <= 0 || nPort > 65535)
202 return false;
203
204 out = host + ":" + port;
205 return true;
206}
207
169// Digest signed over the descriptor fields (domain-separated). 208// Digest signed over the descriptor fields (domain-separated).
170static void DescriptorDigest(const unsigned char pubkey[32], const std::string& enc, 209static void DescriptorDigestV2(const unsigned char pubkey[32], const std::string& enc,
171 const std::string& meeting_node, uint64_t created, 210 const std::string& meeting_node, uint64_t created,
172 unsigned char out[32]) 211 unsigned char out[32])
173{ 212{
174 static const char* DS = ".btf-descriptor-v2"; 213 static const char* DS = ".btf-descriptor-v2";
175 SHA256_CTX ctx; 214 SHA256_CTX ctx;
@@ -184,9 +223,27 @@ static void DescriptorDigest(const unsigned char pubkey[32], const std::string&
184 SHA256_Final(out, &ctx); 223 SHA256_Final(out, &ctx);
185} 224}
186 225
226static void DescriptorDigestV3(const unsigned char pubkey[32], const std::string& enc,
227 const std::string& meeting_node, const std::string& onion,
228 uint64_t created, unsigned char out[32])
229{
230 static const char* DS = ".btf-descriptor-v3-onion";
231 SHA256_CTX ctx;
232 SHA256_Init(&ctx);
233 SHA256_Update(&ctx, (const unsigned char*)DS, strlen(DS));
234 SHA256_Update(&ctx, pubkey, 32);
235 SHA256_Update(&ctx, (const unsigned char*)enc.data(), enc.size());
236 SHA256_Update(&ctx, (const unsigned char*)meeting_node.data(), meeting_node.size());
237 SHA256_Update(&ctx, (const unsigned char*)onion.data(), onion.size());
238 unsigned char be[8];
239 for (int i = 0; i < 8; i++) be[i] = (unsigned char)((created >> (8 * (7 - i))) & 0xff);
240 SHA256_Update(&ctx, be, 8);
241 SHA256_Final(out, &ctx);
242}
243
187std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], 244std::string SignDescriptor(void* ctxv, const unsigned char seckey[32],
188 const std::string& enc, const std::string& meeting_node, 245 const std::string& enc, const std::string& meeting_node,
189 uint64_t created) 246 uint64_t created, const std::string& onion)
190{ 247{
191 secp256k1_context* ctx = (secp256k1_context*)ctxv; 248 secp256k1_context* ctx = (secp256k1_context*)ctxv;
192 secp256k1_keypair kp; 249 secp256k1_keypair kp;
@@ -197,17 +254,28 @@ std::string SignDescriptor(void* ctxv, const unsigned char seckey[32],
197 secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xpub); 254 secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xpub);
198 255
199 unsigned char digest[32]; 256 unsigned char digest[32];
200 DescriptorDigest(pubkey, enc, meeting_node, created, digest); 257 DescriptorDigestV2(pubkey, enc, meeting_node, created, digest);
201 258
202 unsigned char sig[64]; 259 unsigned char sig[64];
203 // Deterministic signature (NULL aux_rand), matching Itzal's no-aux-rand signing. 260 // Deterministic signature (NULL aux_rand), matching Itzal's no-aux-rand signing.
204 if (!secp256k1_schnorrsig_sign32(ctx, sig, digest, &kp, NULL)) return std::string(); 261 if (!secp256k1_schnorrsig_sign32(ctx, sig, digest, &kp, NULL)) return std::string();
205 262
206 json v = json::object(); 263 json v = json::object();
207 v["v"] = 2; 264 v["v"] = onion.empty() ? 2 : 3;
208 v["pubkey"] = HexEncode(pubkey, 32); 265 v["pubkey"] = HexEncode(pubkey, 32);
209 v["enc"] = enc; 266 v["enc"] = enc;
210 v["meeting_node"] = meeting_node; 267 v["meeting_node"] = meeting_node;
268 if (!onion.empty())
269 {
270 std::string onionNorm;
271 if (!NormalizeOnionEndpoint(onion, onionNorm)) return std::string();
272 unsigned char digest3[32];
273 DescriptorDigestV3(pubkey, enc, meeting_node, onionNorm, created, digest3);
274 unsigned char sig3[64];
275 if (!secp256k1_schnorrsig_sign32(ctx, sig3, digest3, &kp, NULL)) return std::string();
276 v["onion"] = onionNorm;
277 v["sig3"] = HexEncode(sig3, 64);
278 }
211 v["created"] = created; 279 v["created"] = created;
212 v["sig"] = HexEncode(sig, 64); 280 v["sig"] = HexEncode(sig, 64);
213 return v.dump(); 281 return v.dump();
@@ -242,15 +310,35 @@ bool VerifyDescriptor(void* ctxv, const std::string& jsonStr,
242 if (!HexDecode(v["sig"].get<std::string>(), sig, 64)) return false; 310 if (!HexDecode(v["sig"].get<std::string>(), sig, 64)) return false;
243 311
244 unsigned char digest[32]; 312 unsigned char digest[32];
245 DescriptorDigest(expect_pubkey, enc, meeting_node, created, digest); 313 DescriptorDigestV2(expect_pubkey, enc, meeting_node, created, digest);
246 314
247 secp256k1_xonly_pubkey xpub; 315 secp256k1_xonly_pubkey xpub;
248 if (!secp256k1_xonly_pubkey_parse(ctx, &xpub, expect_pubkey)) return false; 316 if (!secp256k1_xonly_pubkey_parse(ctx, &xpub, expect_pubkey)) return false;
249 if (!secp256k1_schnorrsig_verify(ctx, sig, digest, 32, &xpub)) return false; 317 if (!secp256k1_schnorrsig_verify(ctx, sig, digest, 32, &xpub)) return false;
250 318
319 std::string onion;
320 if (v.contains("onion"))
321 {
322 if (v["onion"].is_string() && v.contains("sig3") && v["sig3"].is_string())
323 {
324 std::string onionNorm;
325 std::string onionRaw = v["onion"].get<std::string>();
326 if (NormalizeOnionEndpoint(onionRaw, onionNorm) && onionNorm == onionRaw)
327 {
328 unsigned char sig3[64];
329 unsigned char digest3[32];
330 DescriptorDigestV3(expect_pubkey, enc, meeting_node, onionNorm, created, digest3);
331 if (HexDecode(v["sig3"].get<std::string>(), sig3, 64) &&
332 secp256k1_schnorrsig_verify(ctx, sig3, digest3, 32, &xpub))
333 onion = onionNorm;
334 }
335 }
336 }
337
251 memcpy(out.pubkey, expect_pubkey, 32); 338 memcpy(out.pubkey, expect_pubkey, 32);
252 out.enc = enc; 339 out.enc = enc;
253 out.meeting_node = meeting_node; 340 out.meeting_node = meeting_node;
341 out.onion = onion;
254 out.created = created; 342 out.created = created;
255 return true; 343 return true;
256 } 344 }
diff --git a/src/btfaddr.h b/src/btfaddr.h
index 1addf8f..55a3ebd 100644
--- a/src/btfaddr.h
+++ b/src/btfaddr.h
@@ -52,15 +52,21 @@ struct Descriptor
52 unsigned char pubkey[32]; 52 unsigned char pubkey[32];
53 std::string enc; // service x25519 public key (hex) for the E2E channel 53 std::string enc; // service x25519 public key (hex) for the E2E channel
54 std::string meeting_node; // rendezvous node the service is registered at 54 std::string meeting_node; // rendezvous node the service is registered at
55 std::string onion; // optional direct Tor hidden-service endpoint (host.onion:port)
55 uint64_t created; 56 uint64_t created;
56}; 57};
57 58
59// Normalize and validate a direct Tor hidden-service endpoint. Only v3 onion
60// names are accepted: 56 base32 chars plus ".onion", followed by ":PORT".
61bool NormalizeOnionEndpoint(const std::string& in, std::string& out);
62
58// Sign a descriptor. `ctx` is a secp256k1_context* (passed as void* to keep this 63// Sign a descriptor. `ctx` is a secp256k1_context* (passed as void* to keep this
59// header free of secp headers); `seckey` is the 32-byte service secret key. 64// header free of secp headers); `seckey` is the 32-byte service secret key.
60// Returns the descriptor JSON as a string, or "" on failure. 65// Returns the descriptor JSON as a string, or "" on failure.
61std::string SignDescriptor(void* ctx, const unsigned char seckey[32], 66std::string SignDescriptor(void* ctx, const unsigned char seckey[32],
62 const std::string& enc, const std::string& meeting_node, 67 const std::string& enc, const std::string& meeting_node,
63 uint64_t created); 68 uint64_t created,
69 const std::string& onion = std::string());
64 70
65// Verify a descriptor is validly signed by `expect_pubkey` (the key the resolved 71// Verify a descriptor is validly signed by `expect_pubkey` (the key the resolved
66// `.btf` address encodes). Returns true and fills `out` on success. 72// `.btf` address encodes). Returns true and fills `out` on success.
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index e941522..e8dacd3 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -1,8 +1,10 @@
1// Bitflash entry point -- starts node threads then runs GUI (or headless). 1// Bitflash entry point -- starts node threads then runs GUI (or headless).
2 2
3#include "headers_core.h" 3#include "headers_core.h"
4#include "nostr.h"
4#include "proxy.h" 5#include "proxy.h"
5#include "selftest.h" 6#include "selftest.h"
7#include "tor.h"
6#include "walletcmd.h" 8#include "walletcmd.h"
7#include <thread> // hardware_concurrency, to sanity-check /genproclimit 9#include <thread> // hardware_concurrency, to sanity-check /genproclimit
8#ifndef _WIN32 10#ifndef _WIN32
@@ -112,7 +114,7 @@ static void PrintUsage()
112 printf(" /gen\n"); 114 printf(" /gen\n");
113 printf(" /nogui or /daemon\n"); 115 printf(" /nogui or /daemon\n");
114 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n"); 116 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n");
115 printf(" net-message, consensus-limits, pool-stratum, parse-money, or socks5-proxy\n"); 117 printf(" net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, or managed-tor\n");
116 printf("\n"); 118 printf("\n");
117 printf("Mining mode:\n"); 119 printf("Mining mode:\n");
118 printf(" /operator\n"); 120 printf(" /operator\n");
@@ -137,11 +139,15 @@ static void PrintUsage()
137 printf(" /connectbtf=PEER_BTF_ADDRESS\n"); 139 printf(" /connectbtf=PEER_BTF_ADDRESS\n");
138 printf(" /rvrelay=HOST:PORT\n"); 140 printf(" /rvrelay=HOST:PORT\n");
139 printf(" /announcerelay=HOST:PORT\n"); 141 printf(" /announcerelay=HOST:PORT\n");
142 printf(" /onionservice=HOST.onion:PORT (advertise this node's Tor hidden service)\n");
140 printf("\n"); 143 printf("\n");
141 printf("Network:\n"); 144 printf("Network:\n");
142 printf(" /port=N (P2P listen port, default 8433)\n"); 145 printf(" /port=N (P2P listen port, default 8433)\n");
143 printf(" /socks=HOST:PORT (SOCKS5 proxy for outbound Nostr and .btf relay dials)\n"); 146 printf(" /socks=HOST:PORT (SOCKS5 proxy for Nostr, .btf relay, and onion dials)\n");
144 printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n"); 147 printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n");
148 printf(" /managedtor[=PATH] (start Tor, create a hidden service, advertise its onion)\n");
149 printf(" /nomanagedtor (disable automatic bundled Tor startup)\n");
150 printf(" /oniononly (do not fall back to rendezvous when a .btf onion dial fails)\n");
145 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); 151 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n");
146 printf("\n"); 152 printf("\n");
147 printf("Wallet:\n"); 153 printf("Wallet:\n");
@@ -295,6 +301,7 @@ static void ParseStartupArguments(int argc, char* argv[])
295 string btfConnect = argval2(argc, argv, "/connectbtf", "-connectbtf"); 301 string btfConnect = argval2(argc, argv, "/connectbtf", "-connectbtf");
296 if (!btfConnect.empty()) 302 if (!btfConnect.empty())
297 strBtfConnect = btfConnect; 303 strBtfConnect = btfConnect;
304 fBtfOnionOnly = arg(argc, argv, "/oniononly") || arg(argc, argv, "-oniononly");
298 305
299 string rvRelay = argval2(argc, argv, "/rvrelay", "-rvrelay"); 306 string rvRelay = argval2(argc, argv, "/rvrelay", "-rvrelay");
300 if (!rvRelay.empty()) 307 if (!rvRelay.empty())
@@ -315,14 +322,67 @@ static void ParseStartupArguments(int argc, char* argv[])
315 } 322 }
316 } 323 }
317 324
325 // net.cpp has described nListenPort as "tunable via /port" since it was
326 // written, but nothing ever read the option, so the port was fixed at 8433
327 // and a second node could not start on a machine already running one. Parse
328 // it before Tor setup so a managed hidden service maps the real listener.
329 string strPort = argval2(argc, argv, "/port", "-port");
330 if (!strPort.empty())
331 {
332 int nPort = atoi(strPort.c_str());
333 if (nPort <= 0 || nPort > 65535)
334 fprintf(stderr, "Ignoring /port=%s: not a port number\n", strPort.c_str());
335 else
336 {
337 nListenPort = htons((unsigned short)nPort);
338 // Or we would announce a port we never bound.
339 addrLocalHost.port = nListenPort;
340 }
341 }
342
318 string announceRelay = argval2(argc, argv, "/announcerelay", "-announcerelay"); 343 string announceRelay = argval2(argc, argv, "/announcerelay", "-announcerelay");
319 if (!announceRelay.empty()) 344 if (!announceRelay.empty())
320 strBtfAnnounceRelay = announceRelay; 345 strBtfAnnounceRelay = announceRelay;
321 346
347 string onionService = argval2(argc, argv, "/onionservice", "-onionservice");
348 if (!onionService.empty())
349 {
350 string err;
351 if (!BtfSetLocalOnionEndpoint(onionService, err))
352 fprintf(stderr, "Ignoring /onionservice=%s: %s\n", onionService.c_str(), err.c_str());
353 else
354 fprintf(stderr, "Direct onion peer endpoint advertised: %s\n",
355 BtfLocalOnionEndpoint().c_str());
356 }
357
358 bool fManagedTor = arg(argc, argv, "/managedtor") || arg(argc, argv, "-managedtor");
359 bool fNoManagedTor = arg(argc, argv, "/nomanagedtor") || arg(argc, argv, "-nomanagedtor");
322 bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor"); 360 bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor");
323 if (fTorMode) 361 string socksProxy = argval2(argc, argv, "/socks", "-socks");
362 string bundledTorPath;
363 bool fAutoManagedTor = !fManagedTor && !fNoManagedTor && !fTorMode &&
364 socksProxy.empty() && BtfBundledTorPath(bundledTorPath);
365 if (fManagedTor || fAutoManagedTor)
366 {
367 if (fManagedTor && (fTorMode || !socksProxy.empty()))
368 fprintf(stderr, "Warning: /managedtor takes precedence over /tor and /socks\n");
369
370 string torPath = fManagedTor ? argval2(argc, argv, "/managedtor", "-managedtor") : bundledTorPath;
371 string err;
372 if (!BtfStartManagedTor(torPath, err))
373 {
374 if (fAutoManagedTor)
375 fprintf(stderr, "Bundled Tor not enabled automatically: %s\n", err.c_str());
376 else
377 fprintf(stderr, "Ignoring /managedtor=%s: %s\n", torPath.c_str(), err.c_str());
378 }
379 else
380 fprintf(stderr, "Managed Tor enabled%s: %s\n",
381 fAutoManagedTor ? " automatically" : "",
382 BtfManagedTorStatus().c_str());
383 }
384 else if (fTorMode)
324 { 385 {
325 string socksProxy = argval2(argc, argv, "/socks", "-socks");
326 if (!socksProxy.empty()) 386 if (!socksProxy.empty())
327 fprintf(stderr, "Warning: /tor takes precedence over /socks=%s\n", 387 fprintf(stderr, "Warning: /tor takes precedence over /socks=%s\n",
328 socksProxy.c_str()); 388 socksProxy.c_str());
@@ -337,7 +397,6 @@ static void ParseStartupArguments(int argc, char* argv[])
337 } 397 }
338 else 398 else
339 { 399 {
340 string socksProxy = argval2(argc, argv, "/socks", "-socks");
341 if (!socksProxy.empty()) 400 if (!socksProxy.empty())
342 { 401 {
343 string err; 402 string err;
@@ -348,6 +407,8 @@ static void ParseStartupArguments(int argc, char* argv[])
348 BtfSocks5ProxyName().c_str()); 407 BtfSocks5ProxyName().c_str());
349 } 408 }
350 } 409 }
410 if (fBtfOnionOnly && !BtfSocks5ProxyEnabled())
411 fprintf(stderr, "Warning: /oniononly without /tor, /managedtor, or /socks cannot dial .onion peers\n");
351 412
352 // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for 413 // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for
353 // testing the seed path and for private networks that ship no compiled list. 414 // testing the seed path and for private networks that ship no compiled list.
@@ -368,22 +429,6 @@ static void ParseStartupArguments(int argc, char* argv[])
368 vBtfExtraSeeds.push_back(make_pair(val.substr(0, colon), val.substr(colon + 1))); 429 vBtfExtraSeeds.push_back(make_pair(val.substr(0, colon), val.substr(colon + 1)));
369 } 430 }
370 431
371 // net.cpp has described nListenPort as "tunable via /port" since it was
372 // written, but nothing ever read the option, so the port was fixed at 8433
373 // and a second node could not start on a machine already running one.
374 string strPort = argval2(argc, argv, "/port", "-port");
375 if (!strPort.empty())
376 {
377 int nPort = atoi(strPort.c_str());
378 if (nPort <= 0 || nPort > 65535)
379 fprintf(stderr, "Ignoring /port=%s: not a port number\n", strPort.c_str());
380 else
381 {
382 nListenPort = htons((unsigned short)nPort);
383 // Or we would announce a port we never bound.
384 addrLocalHost.port = nListenPort;
385 }
386 }
387} 432}
388 433
389// Report a startup failure somewhere the user will actually see it. 434// Report a startup failure somewhere the user will actually see it.
@@ -760,7 +805,12 @@ int main(int argc, char* argv[])
760 return 0; 805 return 0;
761 } 806 }
762 807
763 if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } 808 if (!StartNode(strErrors))
809 {
810 fprintf(stderr,"StartNode: %s\n",strErrors.c_str());
811 BtfStopManagedTor();
812 return 1;
813 }
764 814
765 if (nMineMode == MINE_OPERATOR) { 815 if (nMineMode == MINE_OPERATOR) {
766 gPoolServerRunning = true; 816 gPoolServerRunning = true;
@@ -793,6 +843,7 @@ int main(int argc, char* argv[])
793 printf("Running headless. Ctrl-C to stop.\n"); 843 printf("Running headless. Ctrl-C to stop.\n");
794 while (!fShutdown) Sleep(500); 844 while (!fShutdown) Sleep(500);
795 StopNode(); 845 StopNode();
846 BtfStopManagedTor();
796 // Checkpoints, releases wallet.dat from this directory's Berkeley DB 847 // Checkpoints, releases wallet.dat from this directory's Berkeley DB
797 // environment, and closes it. Declared since 2009 and never once 848 // environment, and closes it. Declared since 2009 and never once
798 // called here, which is why a wallet.dat copied elsewhere would not 849 // called here, which is why a wallet.dat copied elsewhere would not
@@ -812,6 +863,7 @@ int main(int argc, char* argv[])
812 int ret = RunGUI(argc, argv); 863 int ret = RunGUI(argc, argv);
813 fShutdown = true; 864 fShutdown = true;
814 StopNode(); 865 StopNode();
866 BtfStopManagedTor();
815 DBFlush(true); 867 DBFlush(true);
816 return ret; 868 return ret;
817#endif 869#endif
diff --git a/src/makefile.mingw b/src/makefile.mingw
index 8b49cac..0ba1d76 100644
--- a/src/makefile.mingw
+++ b/src/makefile.mingw
@@ -37,7 +37,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
37NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 37NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
38 obj/sha.o obj/nostr.o obj/randomx_pow.o \ 38 obj/sha.o obj/nostr.o obj/randomx_pow.o \
39 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 39 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
40 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o 40 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o
41 41
42GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o 42GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
43 43
@@ -51,7 +51,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
51HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 51HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
52 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 52 key.h bignum.h script.h db.h base58.h net.h nostr.h \
53 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ 53 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \
54 selftest.h walletcmd.h sockcount.h proxy.h compat.h font_roboto.h 54 selftest.h walletcmd.h sockcount.h proxy.h tor.h compat.h font_roboto.h
55 55
56all: bitflash.exe 56all: bitflash.exe
57 57
@@ -113,3 +113,4 @@ tests: test_bip32.exe bitflash.exe
113 ./bitflash.exe -selftest=consensus-limits 113 ./bitflash.exe -selftest=consensus-limits
114 ./bitflash.exe -selftest=pool-stratum 114 ./bitflash.exe -selftest=pool-stratum
115 ./bitflash.exe -selftest=socks5-proxy 115 ./bitflash.exe -selftest=socks5-proxy
116 ./bitflash.exe -selftest=managed-tor
diff --git a/src/net.cpp b/src/net.cpp
index bb39122..35baf21 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -17,9 +17,12 @@
17#include <poll.h> 17#include <poll.h>
18#endif 18#endif
19#include <openssl/rand.h> 19#include <openssl/rand.h>
20#include <cerrno>
21#include <cstdlib>
20#include "btfaddr.h" 22#include "btfaddr.h"
21#include "btftunnel.h" 23#include "btftunnel.h"
22#include "proxy.h" 24#include "proxy.h"
25#include "tor.h"
23 26
24void ThreadReconnectCachedBtfPeers(void* parg); 27void ThreadReconnectCachedBtfPeers(void* parg);
25void ThreadMessageHandler2(void* parg); 28void ThreadMessageHandler2(void* parg);
@@ -62,6 +65,8 @@ deque<pair<int64, CInv> > vRelayExpiration;
62CCriticalSection cs_mapRelay; 65CCriticalSection cs_mapRelay;
63map<CInv, int64> mapAlreadyAskedFor; 66map<CInv, int64> mapAlreadyAskedFor;
64string strBtfConnect; // .btf peer to keep connected to (from /connectbtf) 67string strBtfConnect; // .btf peer to keep connected to (from /connectbtf)
68bool fBtfOnionOnly = false; // require direct onion transport for .btf peers
69static const int BTF_ONION_CONNECT_TIMEOUT_SECS = 120;
65 70
66int nPeersWatched = 0; 71int nPeersWatched = 0;
67int64 nBlocksReceived = 0; 72int64 nBlocksReceived = 0;
@@ -170,7 +175,7 @@ static const char* pszSockSite[SOCK_SITES] = {
170 "external-ip probe", "listen socket", "inbound accept", 175 "external-ip probe", "listen socket", "inbound accept",
171 "rendezvous dial", "rendezvous listen", "rendezvous accept", 176 "rendezvous dial", "rendezvous listen", "rendezvous accept",
172 "loopback pair listener", "loopback pair app end", "loopback pair pump end", 177 "loopback pair listener", "loopback pair app end", "loopback pair pump end",
173 "nostr relay", "stratum bridge" 178 "nostr relay", "direct onion peer", "stratum bridge"
174}; 179};
175 180
176static string SockAccountingText() 181static string SockAccountingText()
@@ -263,15 +268,21 @@ string GetDiagnosticsText()
263 int64 nNow = GetTime(); 268 int64 nNow = GetTime();
264 string str; 269 string str;
265 270
266 int nInbound = 0, nHeld = 0; 271 int nInbound = 0, nHeld = 0, nDirectOnion = 0, nRendezvous = 0;
267 vector<CNode*> vCopy; 272 vector<CNode*> vCopy;
268 CRITICAL_BLOCK(cs_vNodes) 273 CRITICAL_BLOCK(cs_vNodes)
269 { 274 {
270 vCopy = vNodes; 275 vCopy = vNodes;
271 nHeld = (int)vNodes.size(); 276 nHeld = (int)vNodes.size();
272 foreach(CNode* pnode, vNodes) 277 foreach(CNode* pnode, vNodes)
278 {
273 if (pnode->fInbound) 279 if (pnode->fInbound)
274 nInbound++; 280 nInbound++;
281 if (pnode->strBtfMeeting.find(".onion:") != string::npos)
282 nDirectOnion++;
283 else if (!pnode->strBtfMeeting.empty())
284 nRendezvous++;
285 }
275 } 286 }
276 287
277 int nMedian = GetPeerMedianHeight(); 288 int nMedian = GetPeerMedianHeight();
@@ -294,6 +305,23 @@ string GetDiagnosticsText()
294 str += strprintf(" peers watched %d of %d held by poll%s\n", 305 str += strprintf(" peers watched %d of %d held by poll%s\n",
295 nPeersWatched, nHeld, 306 nPeersWatched, nHeld,
296 nHeld > nPeersWatched ? " <-- NOT ALL PEERS ARE BEING READ" : ""); 307 nHeld > nPeersWatched ? " <-- NOT ALL PEERS ARE BEING READ" : "");
308 str += strprintf(" managed Tor %s\n", BtfManagedTorStatus().c_str());
309
310 string strTorMode = "disabled";
311 if (BtfManagedTorEnabled())
312 strTorMode = "managed";
313 else if (BtfTorProxyEnabled())
314 strTorMode = "external";
315 else if (BtfSocks5ProxyEnabled())
316 strTorMode = "socks5";
317 string strOnion = BtfLocalOnionEndpoint();
318 str += strprintf(" Tor mode %s%s\n",
319 strTorMode.c_str(),
320 fBtfOnionOnly ? " (onion-only)" : "");
321 str += strprintf(" onion endpoint %s\n",
322 strOnion.empty() ? "none" : strOnion.c_str());
323 str += strprintf(" .btf peers %d direct onion, %d rendezvous\n",
324 nDirectOnion, nRendezvous);
297 325
298 if (nBlocksReceived > 0) 326 if (nBlocksReceived > 0)
299 str += strprintf(" blocks received %lld (%lld arrived without a parent, %.1f%%)\n", 327 str += strprintf(" blocks received %lld (%lld arrived without a parent, %.1f%%)\n",
@@ -544,6 +572,7 @@ struct CachedBtfPeer
544{ 572{
545 string btfAddr; 573 string btfAddr;
546 string meeting; 574 string meeting;
575 string onion;
547 string encHex; 576 string encHex;
548 int64 lastSeen; 577 int64 lastSeen;
549 // The peer's own signed descriptor, as it announced itself. Kept verbatim 578 // The peer's own signed descriptor, as it announced itself. Kept verbatim
@@ -602,6 +631,8 @@ void LoadCachedBtfPeers(vector<CachedBtfPeer>& out)
602 CachedBtfPeer p; 631 CachedBtfPeer p;
603 p.btfAddr = o["btf"].get<string>(); 632 p.btfAddr = o["btf"].get<string>();
604 p.meeting = o["meeting"].get<string>(); 633 p.meeting = o["meeting"].get<string>();
634 if (o.contains("onion") && o["onion"].is_string())
635 btf::NormalizeOnionEndpoint(o["onion"].get<string>(), p.onion);
605 p.encHex = o["enc"].get<string>(); 636 p.encHex = o["enc"].get<string>();
606 p.lastSeen = o.value("seen", (int64)0); 637 p.lastSeen = o.value("seen", (int64)0);
607 if (o.contains("desc") && o["desc"].is_string()) 638 if (o.contains("desc") && o["desc"].is_string())
@@ -625,7 +656,8 @@ static CCriticalSection cs_btfPeerCache;
625// learned about by exchange must not cost us the ability to pass it on. 656// learned about by exchange must not cost us the ability to pass it on.
626static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting, 657static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
627 const unsigned char enc_pub[32], 658 const unsigned char enc_pub[32],
628 const string& strDesc = string()) 659 const string& strDesc = string(),
660 const string& strOnion = string())
629{ 661{
630 CRITICAL_BLOCK(cs_btfPeerCache) 662 CRITICAL_BLOCK(cs_btfPeerCache)
631 { 663 {
@@ -633,11 +665,15 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
633 LoadCachedBtfPeers(peers); 665 LoadCachedBtfPeers(peers);
634 666
635 string strKeepDesc = strDesc; 667 string strKeepDesc = strDesc;
668 string strKeepOnion;
669 btf::NormalizeOnionEndpoint(strOnion, strKeepOnion);
636 for (size_t i = 0; i < peers.size(); i++) 670 for (size_t i = 0; i < peers.size(); i++)
637 if (peers[i].btfAddr == strBtfAddr) 671 if (peers[i].btfAddr == strBtfAddr)
638 { 672 {
639 if (strKeepDesc.empty()) 673 if (strKeepDesc.empty())
640 strKeepDesc = peers[i].desc; 674 strKeepDesc = peers[i].desc;
675 if (strKeepOnion.empty())
676 strKeepOnion = peers[i].onion;
641 peers.erase(peers.begin() + i); 677 peers.erase(peers.begin() + i);
642 break; 678 break;
643 } 679 }
@@ -645,6 +681,7 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
645 CachedBtfPeer p; 681 CachedBtfPeer p;
646 p.btfAddr = strBtfAddr; 682 p.btfAddr = strBtfAddr;
647 p.meeting = strMeeting; 683 p.meeting = strMeeting;
684 p.onion = strKeepOnion;
648 p.encHex = BytesToHex(enc_pub, 32); 685 p.encHex = BytesToHex(enc_pub, 32);
649 p.lastSeen = GetTime(); 686 p.lastSeen = GetTime();
650 p.desc = strKeepDesc; 687 p.desc = strKeepDesc;
@@ -659,6 +696,8 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
659 nlohmann::json o; 696 nlohmann::json o;
660 o["btf"] = q.btfAddr; 697 o["btf"] = q.btfAddr;
661 o["meeting"] = q.meeting; 698 o["meeting"] = q.meeting;
699 if (!q.onion.empty())
700 o["onion"] = q.onion;
662 o["enc"] = q.encHex; 701 o["enc"] = q.encHex;
663 o["seen"] = q.lastSeen; 702 o["seen"] = q.lastSeen;
664 if (!q.desc.empty()) 703 if (!q.desc.empty())
@@ -747,7 +786,7 @@ static int TryBtfSeeds()
747 foreach(const string& strRelay, relays) 786 foreach(const string& strRelay, relays)
748 { 787 {
749 if (fShutdown) return nConnected; 788 if (fShutdown) return nConnected;
750 if (ConnectNodeBtfResolved(s.first, strRelay, enc)) 789 if (ConnectNodeBtfResolved(s.first, strRelay, "", enc))
751 { 790 {
752 LogPrint("net", "btfseed: reached %s via %s\n", 791 LogPrint("net", "btfseed: reached %s via %s\n",
753 s.first.c_str(), strRelay.c_str()); 792 s.first.c_str(), strRelay.c_str());
@@ -785,7 +824,7 @@ void ThreadReconnectCachedBtfPeers(void* parg)
785 if (fShutdown) return; 824 if (fShutdown) return;
786 unsigned char enc[32]; 825 unsigned char enc[32];
787 if (!HexToBytes(p.encHex, enc, 32)) continue; 826 if (!HexToBytes(p.encHex, enc, 32)) continue;
788 if (ConnectNodeBtfResolved(p.btfAddr, p.meeting, enc)) 827 if (ConnectNodeBtfResolved(p.btfAddr, p.meeting, p.onion, enc))
789 { 828 {
790 nConnected++; 829 nConnected++;
791 if (nConnected >= 8) break; // enough to bootstrap; the rest can wait 830 if (nConnected >= 8) break; // enough to bootstrap; the rest can wait
@@ -876,14 +915,37 @@ int BtfPexAccept(const vector<string>& vDesc)
876 if (!HexToBytes(d.enc, enc, 32)) 915 if (!HexToBytes(d.enc, enc, 32))
877 continue; 916 continue;
878 917
879 RememberBtfPeer(strAddr, d.meeting_node, enc, strDesc); 918 RememberBtfPeer(strAddr, d.meeting_node, enc, strDesc, d.onion);
880 nKept++; 919 nKept++;
881 } 920 }
882 return nKept; 921 return nKept;
883} 922}
884 923
924static bool SplitHostPort(const string& strHostPort, string& strHost, unsigned short& nPort)
925{
926 size_t colon = strHostPort.rfind(':');
927 if (colon == string::npos || colon == 0 || colon + 1 >= strHostPort.size())
928 return false;
929 string port = strHostPort.substr(colon + 1);
930 for (size_t i = 0; i < port.size(); i++)
931 if (!isdigit((unsigned char)port[i]))
932 return false;
933
934 errno = 0;
935 char* end = NULL;
936 long n = strtol(port.c_str(), &end, 10);
937 if (errno == ERANGE || end == NULL || *end != '\0' || n <= 0 || n > 65535)
938 return false;
939
940 strHost = strHostPort.substr(0, colon);
941 nPort = (unsigned short)n;
942 return true;
943}
944
885static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32], 945static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32],
886 const string& strMeeting, const unsigned char enc_pub[32]) 946 const string& strMeeting, const string& strOnion,
947 const unsigned char enc_pub[32],
948 const string& strDesc = string())
887{ 949{
888 CAddress addr = BtfMarkerAddr(pk); 950 CAddress addr = BtfMarkerAddr(pk);
889 CNode* pnode = FindNode(addr.ip); 951 CNode* pnode = FindNode(addr.ip);
@@ -893,16 +955,54 @@ static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char p
893 return pnode; 955 return pnode;
894 } 956 }
895 957
896 BtfChurnNoteDialAttempt(strBtfAddr, strMeeting); 958 string strOnionNorm;
897 size_t colon = strMeeting.rfind(':'); 959 if (BtfSocks5ProxyEnabled() && btf::NormalizeOnionEndpoint(strOnion, strOnionNorm))
898 if (colon == string::npos)
899 { 960 {
900 BtfChurnNoteDialResult(strBtfAddr, strMeeting, false); 961 string strOnionHost;
962 unsigned short nOnionPort = 0;
963 if (SplitHostPort(strOnionNorm, strOnionHost, nOnionPort))
964 {
965 BtfChurnNoteDialAttempt(strBtfAddr, strOnionNorm);
966 SOCKET hOnionSocket = BtfConnectSocket(strOnionHost, nOnionPort, SOCK_ONION_PEER,
967 BTF_ONION_CONNECT_TIMEOUT_SECS);
968 if (hOnionSocket != INVALID_SOCKET)
969 {
970 if (fDebug)
971 LogPrint("net", "connected %s via direct onion %s\n",
972 strBtfAddr.c_str(), strOnionNorm.c_str());
973 BtfChurnNoteDialResult(strBtfAddr, strOnionNorm, true);
974 RememberBtfPeer(strBtfAddr, strMeeting, enc_pub, strDesc, strOnionNorm);
975
976 pnode = new CNode(hOnionSocket, addr, false);
977 pnode->strBtfAddr = strBtfAddr;
978 pnode->strBtfMeeting = string("onion ") + strOnionNorm;
979 pnode->AddRef();
980 CRITICAL_BLOCK(cs_vNodes)
981 vNodes.push_back(pnode);
982 return pnode;
983 }
984 if (fDebug)
985 LogPrint("net", "ConnectNodeBtf: direct onion to %s at %s failed%s\n",
986 strBtfAddr.c_str(), strOnionNorm.c_str(),
987 fBtfOnionOnly ? "" : ", falling back to rendezvous");
988 BtfChurnNoteDialResult(strBtfAddr, strOnionNorm, false);
989 if (fBtfOnionOnly)
990 return NULL;
991 }
992 }
993
994 if (fBtfOnionOnly)
995 {
996 if (fDebug)
997 LogPrint("net", "ConnectNodeBtf: onion-only mode refused rendezvous fallback for %s\n",
998 strBtfAddr.c_str());
901 return NULL; 999 return NULL;
902 } 1000 }
903 string strHost = strMeeting.substr(0, colon); 1001
904 int nPort = atoi(strMeeting.substr(colon + 1).c_str()); 1002 BtfChurnNoteDialAttempt(strBtfAddr, strMeeting);
905 if (nPort <= 0 || nPort > 65535) 1003 string strHost;
1004 unsigned short nPort = 0;
1005 if (!SplitHostPort(strMeeting, strHost, nPort))
906 { 1006 {
907 BtfChurnNoteDialResult(strBtfAddr, strMeeting, false); 1007 BtfChurnNoteDialResult(strBtfAddr, strMeeting, false);
908 return NULL; 1008 return NULL;
@@ -922,7 +1022,7 @@ static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char p
922 BtfChurnNoteDialResult(strBtfAddr, strMeeting, true); 1022 BtfChurnNoteDialResult(strBtfAddr, strMeeting, true);
923 1023
924 // This one answered -- worth trying first next time we start. 1024 // This one answered -- worth trying first next time we start.
925 RememberBtfPeer(strBtfAddr, strMeeting, enc_pub); 1025 RememberBtfPeer(strBtfAddr, strMeeting, enc_pub, strDesc, strOnionNorm);
926 1026
927 // Add node 1027 // Add node
928 pnode = new CNode(hSocket, addr, false); 1028 pnode = new CNode(hSocket, addr, false);
@@ -952,9 +1052,10 @@ CNode* ConnectNodeBtf(const string& strBtfAddr)
952 LogPrint("net", "trying %s\n", strBtfAddr.c_str()); 1052 LogPrint("net", "trying %s\n", strBtfAddr.c_str());
953 1053
954 string strMeeting; 1054 string strMeeting;
1055 string strOnion;
955 unsigned char enc_pub[32]; 1056 unsigned char enc_pub[32];
956 BtfChurnNoteResolveAttempt(); 1057 BtfChurnNoteResolveAttempt();
957 if (!BtfResolve(strBtfAddr, strMeeting, enc_pub)) 1058 if (!BtfResolve(strBtfAddr, strMeeting, enc_pub, &strOnion))
958 { 1059 {
959 BtfChurnNoteResolveResult(false); 1060 BtfChurnNoteResolveResult(false);
960 if (fDebug) 1061 if (fDebug)
@@ -962,7 +1063,7 @@ CNode* ConnectNodeBtf(const string& strBtfAddr)
962 return NULL; 1063 return NULL;
963 } 1064 }
964 BtfChurnNoteResolveResult(true); 1065 BtfChurnNoteResolveResult(true);
965 return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, enc_pub); 1066 return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, strOnion, enc_pub);
966} 1067}
967 1068
968// Same as ConnectNodeBtf, but skips the Nostr resolve step because the caller 1069// Same as ConnectNodeBtf, but skips the Nostr resolve step because the caller
@@ -971,7 +1072,8 @@ CNode* ConnectNodeBtf(const string& strBtfAddr)
971// address). Makes zero relay connections -- only talks to the peer's 1072// address). Makes zero relay connections -- only talks to the peer's
972// rendezvous meeting node. 1073// rendezvous meeting node.
973CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, 1074CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting,
974 const unsigned char enc_pub[32]) 1075 const string& strOnion, const unsigned char enc_pub[32],
1076 const string& strDesc)
975{ 1077{
976 unsigned char pk[32]; 1078 unsigned char pk[32];
977 if (!btf::ParseAddress(strBtfAddr, pk)) 1079 if (!btf::ParseAddress(strBtfAddr, pk))
@@ -981,7 +1083,7 @@ CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting
981 } 1083 }
982 if (BtfLocalAddress() == strBtfAddr) 1084 if (BtfLocalAddress() == strBtfAddr)
983 return NULL; // ourselves 1085 return NULL; // ourselves
984 return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, enc_pub); 1086 return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, strOnion, enc_pub, strDesc);
985} 1087}
986 1088
987// Anonymous inbound listener (this node's `.btf` hidden service). Registers at 1089// Anonymous inbound listener (this node's `.btf` hidden service). Registers at
@@ -1936,6 +2038,11 @@ bool StartNode(string& strError)
1936 // IP address, and port for the socket that is being bound 2038 // IP address, and port for the socket that is being bound
1937 int nRetryLimit = 15; 2039 int nRetryLimit = 15;
1938 struct sockaddr_in sockaddr = addrLocalHost.GetSockAddr(); 2040 struct sockaddr_in sockaddr = addrLocalHost.GetSockAddr();
2041 // Keep addrLocalHost as this node's advertised address, but listen on all
2042 // local interfaces. Managed Tor forwards hidden-service traffic to
2043 // 127.0.0.1:<port>, and binding only to the LAN address made that onion
2044 // endpoint unreachable even though the P2P listener was running.
2045 sockaddr.sin_addr.s_addr = INADDR_ANY;
1939 if (bind(hListenSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR) 2046 if (bind(hListenSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR)
1940 { 2047 {
1941 int nErr = WSAGetLastError(); 2048 int nErr = WSAGetLastError();
diff --git a/src/net.h b/src/net.h
index 653a015..9f3c7e4 100644
--- a/src/net.h
+++ b/src/net.h
@@ -26,10 +26,12 @@ enum
26bool GetMyExternalIP(unsigned int& ipRet); 26bool GetMyExternalIP(unsigned int& ipRet);
27CNode* ConnectNodeBtf(const string& strBtfAddr); 27CNode* ConnectNodeBtf(const string& strBtfAddr);
28CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, 28CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting,
29 const unsigned char enc_pub[32]); 29 const string& strOnion, const unsigned char enc_pub[32],
30 const string& strDesc=string());
30void ThreadBtfAccept(void* parg); 31void ThreadBtfAccept(void* parg);
31void ThreadBtfConnect(void* parg); 32void ThreadBtfConnect(void* parg);
32extern string strBtfConnect; 33extern string strBtfConnect;
34extern bool fBtfOnionOnly;
33// Bootstrap seeds added at runtime with /btfseed=ADDRESS:ENCHEX (repeatable). 35// Bootstrap seeds added at runtime with /btfseed=ADDRESS:ENCHEX (repeatable).
34// The compiled-in list lives in net.cpp; these are appended to it. 36// The compiled-in list lives in net.cpp; these are appended to it.
35extern std::vector<std::pair<std::string, std::string> > vBtfExtraSeeds; 37extern std::vector<std::pair<std::string, std::string> > vBtfExtraSeeds;
diff --git a/src/nostr.cpp b/src/nostr.cpp
index 32cea18..e2d839c 100644
--- a/src/nostr.cpp
+++ b/src/nostr.cpp
@@ -132,6 +132,8 @@ vector<string> vBtfMeetingRelays = {
132bool fBtfRelayPinned = false; 132bool fBtfRelayPinned = false;
133string strBtfActiveRelay; 133string strBtfActiveRelay;
134static CCriticalSection cs_activeRelay; 134static CCriticalSection cs_activeRelay;
135static string strBtfOnionEndpoint;
136static CCriticalSection cs_onionEndpoint;
135 137
136// Only the rendezvous we actually registered at (BtfSetActiveRelay, called from 138// Only the rendezvous we actually registered at (BtfSetActiveRelay, called from
137// ThreadBtfAccept once registration succeeds). Empty until then, on purpose: it 139// ThreadBtfAccept once registration succeeds). Empty until then, on purpose: it
@@ -153,6 +155,26 @@ void BtfSetActiveRelay(const string& relay)
153 strBtfActiveRelay = relay; 155 strBtfActiveRelay = relay;
154} 156}
155 157
158bool BtfSetLocalOnionEndpoint(const string& endpoint, string& errOut)
159{
160 string norm;
161 if (!btf::NormalizeOnionEndpoint(endpoint, norm))
162 {
163 errOut = "expected v3 onion endpoint HOST.onion:PORT";
164 return false;
165 }
166 CRITICAL_BLOCK(cs_onionEndpoint)
167 strBtfOnionEndpoint = norm;
168 return true;
169}
170
171string BtfLocalOnionEndpoint()
172{
173 CRITICAL_BLOCK(cs_onionEndpoint)
174 return strBtfOnionEndpoint;
175 return "";
176}
177
156// A relay this node advertises on Nostr for others to discover (/announcerelay). 178// A relay this node advertises on Nostr for others to discover (/announcerelay).
157string strBtfAnnounceRelay; 179string strBtfAnnounceRelay;
158// Relays learned from other operators' Nostr announcements. 180// Relays learned from other operators' Nostr announcements.
@@ -392,10 +414,16 @@ std::string BtfLocalDescriptor()
392 string meeting; 414 string meeting;
393 CRITICAL_BLOCK(cs_activeRelay) 415 CRITICAL_BLOCK(cs_activeRelay)
394 meeting = strBtfActiveRelay; 416 meeting = strBtfActiveRelay;
417 string onion = BtfLocalOnionEndpoint();
395 if (meeting.empty()) 418 if (meeting.empty())
396 return ""; 419 {
420 if (onion.empty())
421 return "";
422 meeting = "rendezvous-pending";
423 }
397 return btf::SignDescriptor(g_nostrKey.ctx, g_nostrKey.seckey, 424 return btf::SignDescriptor(g_nostrKey.ctx, g_nostrKey.seckey,
398 g_nostrKey.EncPubHex(), meeting, (uint64_t)GetTime()); 425 g_nostrKey.EncPubHex(), meeting, (uint64_t)GetTime(),
426 onion);
399} 427}
400 428
401// The shared secp256k1 context, so net.cpp can verify descriptors that arrive 429// The shared secp256k1 context, so net.cpp can verify descriptors that arrive
@@ -854,17 +882,21 @@ static void PublishDescriptor(CWebSocket& ws, CNostrKey& key)
854 // meeting_node is the rendezvous relay this node's hidden service 882 // meeting_node is the rendezvous relay this node's hidden service
855 // (ThreadBtfAccept in net.cpp) is registered at -- where clients dial us. 883 // (ThreadBtfAccept in net.cpp) is registered at -- where clients dial us.
856 string meeting_node = BtfActiveRelay(); 884 string meeting_node = BtfActiveRelay();
885 string onion = BtfLocalOnionEndpoint();
857 if (meeting_node.empty()) 886 if (meeting_node.empty())
858 { 887 {
859 // Not registered anywhere yet. Publishing now would advertise a meeting 888 if (onion.empty())
860 // node we can't be reached at, and peers would waste dials on it. The 889 {
861 // ThreadNostrSeed loop republishes on every pass, so skipping costs 890 // Not registered anywhere yet, and no direct onion endpoint exists.
862 // nothing but the delay until ThreadBtfAccept registers. 891 // Publishing now would advertise a meeting node we can't be reached at.
863 LogPrint("nostr", "Nostr: skipping descriptor publish, no rendezvous registered yet\n"); 892 LogPrint("nostr", "Nostr: skipping descriptor publish, no rendezvous registered yet\n");
864 return; 893 return;
894 }
895 meeting_node = "rendezvous-pending";
865 } 896 }
866 string desc = btf::SignDescriptor(key.ctx, key.seckey, key.EncPubHex(), 897 string desc = btf::SignDescriptor(key.ctx, key.seckey, key.EncPubHex(),
867 meeting_node, (uint64_t)GetTime()); 898 meeting_node, (uint64_t)GetTime(),
899 onion);
868 if (desc.empty()) 900 if (desc.empty())
869 return; 901 return;
870 json tags = json::array({ json::array({ "d", BTF_DESC_DTAG }) }); 902 json tags = json::array({ json::array({ "d", BTF_DESC_DTAG }) });
@@ -1070,7 +1102,8 @@ static void HandlePoolAnnouncement(const json& ev)
1070// this socket for reuse, and only a completed exchange leaves it in a state 1102// this socket for reuse, and only a completed exchange leaves it in a state
1071// the next caller can trust. 1103// the next caller can trust.
1072static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, 1104static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1073 btf::Descriptor& out, bool& fExchangeOk) 1105 btf::Descriptor& out, bool& fExchangeOk,
1106 string* rawOut = NULL)
1074{ 1107{
1075 fExchangeOk = false; 1108 fExchangeOk = false;
1076 unsigned char pk[32]; 1109 unsigned char pk[32];
@@ -1106,7 +1139,11 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1106 const json& ev = j[2]; 1139 const json& ev = j[2];
1107 if (!fFound && ev.contains("content") && 1140 if (!fFound && ev.contains("content") &&
1108 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) 1141 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out))
1142 {
1109 fFound = true; 1143 fFound = true;
1144 if (rawOut)
1145 *rawOut = ev["content"].get<string>();
1146 }
1110 // Deliberately keep reading to EOSE rather than returning here. 1147 // Deliberately keep reading to EOSE rather than returning here.
1111 // Returning early leaves the rest of the relay's answer queued on a 1148 // Returning early leaves the rest of the relay's answer queued on a
1112 // socket that goes straight back into the pool, and the next caller 1149 // socket that goes straight back into the pool, and the next caller
@@ -1126,8 +1163,10 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1126// public discovery relays for the owner's self-certified descriptor. Used by 1163// public discovery relays for the owner's self-certified descriptor. Used by
1127// ConnectNodeBtf in net.cpp. 1164// ConnectNodeBtf in net.cpp.
1128bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, 1165bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
1129 unsigned char enc_pub[32]) 1166 unsigned char enc_pub[32], std::string* onionHostPort)
1130{ 1167{
1168 if (onionHostPort)
1169 onionHostPort->clear();
1131 if (!EnsureNostrKey()) 1170 if (!EnsureNostrKey())
1132 return false; 1171 return false;
1133 for (int i = 0; i < nNostrRelays && !fShutdown; i++) 1172 for (int i = 0; i < nNostrRelays && !fShutdown; i++)
@@ -1155,10 +1194,15 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
1155 } 1194 }
1156 lease.fOk = fExchangeOk; 1195 lease.fOk = fExchangeOk;
1157 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") 1196 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending")
1158 continue; // descriptor predates the owner's relay config 1197 {
1198 if (!onionHostPort || d.onion.empty())
1199 continue; // caller needs rendezvous, or descriptor has no direct path
1200 }
1159 if (!HexDecode(d.enc, enc_pub, 32)) 1201 if (!HexDecode(d.enc, enc_pub, 32))
1160 continue; 1202 continue;
1161 meetingHostPort = d.meeting_node; 1203 meetingHostPort = d.meeting_node;
1204 if (onionHostPort)
1205 *onionHostPort = d.onion;
1162 return true; 1206 return true;
1163 } 1207 }
1164 CATCH_PRINT_EXCEPTION("BtfResolve") 1208 CATCH_PRINT_EXCEPTION("BtfResolve")
@@ -1247,12 +1291,15 @@ bool BtfResolveMany(const std::vector<std::string>& btfAddrs,
1247 btf::Descriptor d; 1291 btf::Descriptor d;
1248 if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get<string>(), pk, d)) 1292 if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get<string>(), pk, d))
1249 continue; 1293 continue;
1250 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") 1294 if ((d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") &&
1295 d.onion.empty())
1251 continue; 1296 continue;
1252 BtfResolvedPeer rp; 1297 BtfResolvedPeer rp;
1253 if (!HexDecode(d.enc, rp.enc_pub, 32)) 1298 if (!HexDecode(d.enc, rp.enc_pub, 32))
1254 continue; 1299 continue;
1255 rp.meetingHostPort = d.meeting_node; 1300 rp.meetingHostPort = d.meeting_node;
1301 rp.onionHostPort = d.onion;
1302 rp.descriptor = ev["content"].get<string>();
1256 out[ai->second] = rp; 1303 out[ai->second] = rp;
1257 pending.erase(ai->second); 1304 pending.erase(ai->second);
1258 } 1305 }
@@ -1729,11 +1776,13 @@ static void ConnectDiscoveredBtfPeers()
1729 DialCtx* ctx = (DialCtx*)arg; 1776 DialCtx* ctx = (DialCtx*)arg;
1730 string addr = ctx->addr; 1777 string addr = ctx->addr;
1731 string meeting = ctx->rp.meetingHostPort; 1778 string meeting = ctx->rp.meetingHostPort;
1779 string onion = ctx->rp.onionHostPort;
1780 string desc = ctx->rp.descriptor;
1732 unsigned char enc_pub[32]; 1781 unsigned char enc_pub[32];
1733 memcpy(enc_pub, ctx->rp.enc_pub, 32); 1782 memcpy(enc_pub, ctx->rp.enc_pub, 32);
1734 delete ctx; 1783 delete ctx;
1735 1784
1736 CNode* pnode = ConnectNodeBtfResolved(addr, meeting, enc_pub); 1785 CNode* pnode = ConnectNodeBtfResolved(addr, meeting, onion, enc_pub, desc);
1737 CRITICAL_BLOCK(cs_btfPeerState) 1786 CRITICAL_BLOCK(cs_btfPeerState)
1738 { 1787 {
1739 if (pnode) 1788 if (pnode)
@@ -1795,11 +1844,12 @@ void ThreadNostrSeed(void* parg)
1795 LogPrint("nostr", "Nostr: node pubkey = %s\n", key.PubKeyHex().c_str()); 1844 LogPrint("nostr", "Nostr: node pubkey = %s\n", key.PubKeyHex().c_str());
1796 LogPrint("nostr", "Nostr: node .btf address = %s\n", key.BtfAddress().c_str()); 1845 LogPrint("nostr", "Nostr: node .btf address = %s\n", key.BtfAddress().c_str());
1797 1846
1798 // The self-test below publishes a real descriptor, and PublishDescriptor now 1847 // The self-test below publishes a real descriptor. Rendezvous-only nodes
1799 // refuses to publish before ThreadBtfAccept has registered a rendezvous. Give 1848 // need ThreadBtfAccept to register first; Tor nodes can publish as soon as
1800 // registration a bounded head start so the test measures the publish/resolve 1849 // their signed onion endpoint exists, with rendezvous marked pending.
1801 // chain instead of losing the race against it. 1850 for (int i = 0; i < 60 && !fShutdown &&
1802 for (int i = 0; i < 60 && !fShutdown && BtfActiveRelay().empty(); i++) 1851 BtfActiveRelay().empty() &&
1852 BtfLocalOnionEndpoint().empty(); i++)
1803 Sleep(1000); 1853 Sleep(1000);
1804 1854
1805 // One-time self-test of the rendezvous: publish our descriptor to a relay, 1855 // One-time self-test of the rendezvous: publish our descriptor to a relay,
diff --git a/src/nostr.h b/src/nostr.h
index 827b2f3..6dbbe26 100644
--- a/src/nostr.h
+++ b/src/nostr.h
@@ -40,6 +40,13 @@ std::string BtfActiveRelay();
40// Record the relay this node just registered its service at (thread-safe). 40// Record the relay this node just registered its service at (thread-safe).
41void BtfSetActiveRelay(const std::string& relay); 41void BtfSetActiveRelay(const std::string& relay);
42 42
43// Optional Tor hidden-service endpoint for this node's normal P2P listener.
44// Set by /onionservice=HOST.onion:PORT. The endpoint is signed into this node's
45// .btf descriptor, so peers with -tor can dial it directly before falling back
46// to rendezvous relays.
47bool BtfSetLocalOnionEndpoint(const std::string& endpoint, std::string& errOut);
48std::string BtfLocalOnionEndpoint();
49
43// If set (via /announcerelay=host:port), this node announces that relay on 50// If set (via /announcerelay=host:port), this node announces that relay on
44// Nostr so other nodes discover it automatically -- no manual seed-list edit. 51// Nostr so other nodes discover it automatically -- no manual seed-list edit.
45extern std::string strBtfAnnounceRelay; 52extern std::string strBtfAnnounceRelay;
@@ -102,12 +109,15 @@ void* BtfSecpContext();
102// self-certified descriptor and return its meeting node ("host:port") and 109// self-certified descriptor and return its meeting node ("host:port") and
103// x25519 public key for the end-to-end channel. 110// x25519 public key for the end-to-end channel.
104bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, 111bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
105 unsigned char enc_pub[32]); 112 unsigned char enc_pub[32],
113 std::string* onionHostPort = NULL);
106 114
107// Result of resolving one .btf address via BtfResolveMany. 115// Result of resolving one .btf address via BtfResolveMany.
108struct BtfResolvedPeer 116struct BtfResolvedPeer
109{ 117{
110 std::string meetingHostPort; 118 std::string meetingHostPort;
119 std::string onionHostPort;
120 std::string descriptor;
111 unsigned char enc_pub[32]; 121 unsigned char enc_pub[32];
112}; 122};
113 123
diff --git a/src/proxy.cpp b/src/proxy.cpp
index 7d30911..eed054c 100644
--- a/src/proxy.cpp
+++ b/src/proxy.cpp
@@ -13,6 +13,7 @@ static bool g_fTorProxy = false;
13static std::string g_socks5Host; 13static std::string g_socks5Host;
14static unsigned short g_socks5Port = 0; 14static unsigned short g_socks5Port = 0;
15static const char* DEFAULT_TOR_SOCKS5_PROXY = "127.0.0.1:9050"; 15static const char* DEFAULT_TOR_SOCKS5_PROXY = "127.0.0.1:9050";
16static const int TOR_CONNECT_TIMEOUT_SECS = 120;
16 17
17static void SetSocketTimeout(SOCKET s, int nTimeoutSecs) 18static void SetSocketTimeout(SOCKET s, int nTimeoutSecs)
18{ 19{
@@ -297,10 +298,25 @@ static bool Socks5Connect(SOCKET s, const std::string& destHost,
297 298
298 unsigned char hello[3] = { 0x05, 0x01, 0x00 }; // SOCKS5, one method, no auth 299 unsigned char hello[3] = { 0x05, 0x01, 0x00 }; // SOCKS5, one method, no auth
299 if (!WriteN(s, hello, sizeof(hello))) 300 if (!WriteN(s, hello, sizeof(hello)))
301 {
302 error("SOCKS5 hello write failed for %s:%u\n",
303 destHost.c_str(), (unsigned)destPort);
300 return false; 304 return false;
305 }
301 unsigned char choice[2] = { 0, 0 }; 306 unsigned char choice[2] = { 0, 0 };
302 if (!ReadN(s, choice, sizeof(choice)) || choice[0] != 0x05 || choice[1] != 0x00) 307 if (!ReadN(s, choice, sizeof(choice)))
308 {
309 error("SOCKS5 method selection read failed for %s:%u\n",
310 destHost.c_str(), (unsigned)destPort);
311 return false;
312 }
313 if (choice[0] != 0x05 || choice[1] != 0x00)
314 {
315 error("SOCKS5 proxy rejected no-auth method for %s:%u (version=0x%02x method=0x%02x)\n",
316 destHost.c_str(), (unsigned)destPort,
317 (unsigned)choice[0], (unsigned)choice[1]);
303 return false; 318 return false;
319 }
304 320
305 std::vector<unsigned char> req; 321 std::vector<unsigned char> req;
306 req.reserve(7 + destHost.size()); 322 req.reserve(7 + destHost.size());
@@ -313,11 +329,25 @@ static bool Socks5Connect(SOCKET s, const std::string& destHost,
313 req.push_back((unsigned char)(destPort >> 8)); 329 req.push_back((unsigned char)(destPort >> 8));
314 req.push_back((unsigned char)(destPort & 0xff)); 330 req.push_back((unsigned char)(destPort & 0xff));
315 if (!WriteN(s, &req[0], (int)req.size())) 331 if (!WriteN(s, &req[0], (int)req.size()))
332 {
333 error("SOCKS5 CONNECT request write failed for %s:%u\n",
334 destHost.c_str(), (unsigned)destPort);
316 return false; 335 return false;
336 }
317 337
318 unsigned char rep[4] = {0,0,0,0}; 338 unsigned char rep[4] = {0,0,0,0};
319 if (!ReadN(s, rep, sizeof(rep)) || rep[0] != 0x05) 339 if (!ReadN(s, rep, sizeof(rep)))
340 {
341 error("SOCKS5 CONNECT reply read failed for %s:%u\n",
342 destHost.c_str(), (unsigned)destPort);
320 return false; 343 return false;
344 }
345 if (rep[0] != 0x05)
346 {
347 error("SOCKS5 CONNECT reply has invalid version 0x%02x for %s:%u\n",
348 (unsigned)rep[0], destHost.c_str(), (unsigned)destPort);
349 return false;
350 }
321 if (rep[1] != 0x00) 351 if (rep[1] != 0x00)
322 { 352 {
323 error("SOCKS5 CONNECT to %s:%u failed with reply 0x%02x\n", 353 error("SOCKS5 CONNECT to %s:%u failed with reply 0x%02x\n",
@@ -335,10 +365,20 @@ static bool Socks5Connect(SOCKET s, const std::string& destHost,
335 nAddr = len; 365 nAddr = len;
336 } 366 }
337 else 367 else
368 {
369 error("SOCKS5 CONNECT reply has invalid address type 0x%02x for %s:%u\n",
370 (unsigned)rep[3], destHost.c_str(), (unsigned)destPort);
338 return false; 371 return false;
372 }
339 373
340 std::vector<unsigned char> discard(nAddr + 2); 374 std::vector<unsigned char> discard(nAddr + 2);
341 return ReadN(s, &discard[0], (int)discard.size()); 375 if (!ReadN(s, &discard[0], (int)discard.size()))
376 {
377 error("SOCKS5 CONNECT bind address read failed for %s:%u\n",
378 destHost.c_str(), (unsigned)destPort);
379 return false;
380 }
381 return true;
342} 382}
343 383
344SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort, 384SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
@@ -347,9 +387,11 @@ SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
347 std::string proxyHost; 387 std::string proxyHost;
348 unsigned short proxyPort = 0; 388 unsigned short proxyPort = 0;
349 bool fProxy = false; 389 bool fProxy = false;
390 bool fTorProxy = false;
350 CRITICAL_BLOCK(cs_socks5) 391 CRITICAL_BLOCK(cs_socks5)
351 { 392 {
352 fProxy = g_fSocks5Proxy; 393 fProxy = g_fSocks5Proxy;
394 fTorProxy = g_fTorProxy;
353 proxyHost = g_socks5Host; 395 proxyHost = g_socks5Host;
354 proxyPort = g_socks5Port; 396 proxyPort = g_socks5Port;
355 } 397 }
@@ -365,6 +407,9 @@ SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
365 return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs); 407 return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs);
366 } 408 }
367 409
410 if (fTorProxy && nTimeoutSecs > 0 && nTimeoutSecs < TOR_CONNECT_TIMEOUT_SECS)
411 nTimeoutSecs = TOR_CONNECT_TIMEOUT_SECS;
412
368 SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs); 413 SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs);
369 if (s == INVALID_SOCKET) 414 if (s == INVALID_SOCKET)
370 return INVALID_SOCKET; 415 return INVALID_SOCKET;
diff --git a/src/readme.txt b/src/readme.txt
index 794fe20..752be66 100644
--- a/src/readme.txt
+++ b/src/readme.txt
@@ -43,12 +43,17 @@ Command-line options (in addition to the original ones)
43------------------------------------------------------- 43-------------------------------------------------------
44 /port=N listen on P2P port N (default 8433) 44 /port=N listen on P2P port N (default 8433)
45 /socks=HOST:PORT 45 /socks=HOST:PORT
46 route outbound Nostr and .btf rendezvous dials through a 46 route outbound Nostr, .btf rendezvous, and direct onion peer
47 SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor 47 dials through a SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor
48 or /socks=[::1]:9050 for an IPv6 loopback proxy 48 or /socks=[::1]:9050 for an IPv6 loopback proxy
49 /tor[=HOST:PORT] 49 /tor[=HOST:PORT]
50 Tor mode for outbound Nostr and .btf rendezvous dials; 50 Tor mode for outbound Nostr, .btf rendezvous, and direct onion peer dials;
51 defaults to the local Tor SOCKS5 proxy at 127.0.0.1:9050 51 defaults to the local Tor SOCKS5 proxy at 127.0.0.1:9050
52 /managedtor[=PATH]
53 start Tor, create a v3 hidden service for this node, route outbound
54 discovery through it, and advertise the generated onion endpoint
55 Windows release packages that include tor/tor.exe start this mode
56 automatically; use /nomanagedtor to force the legacy relay path
52 /solomine mine without requiring a connected peer (local testing) 57 /solomine mine without requiring a connected peer (local testing)
53 /operator operator mode (run pool server) 58 /operator operator mode (run pool server)
54 /participant=ADDR 59 /participant=ADDR
@@ -72,6 +77,8 @@ Command-line options (in addition to the original ones)
72 override meeting relay list with one relay 77 override meeting relay list with one relay
73 /announcerelay=HOST:PORT 78 /announcerelay=HOST:PORT
74 announce this relay on Nostr for discovery 79 announce this relay on Nostr for discovery
80 /onionservice=HOST.onion:PORT
81 advertise this node's Tor hidden service in its signed .btf descriptor
75 82
76 83
77Roadmap 84Roadmap
diff --git a/src/selftest.cpp b/src/selftest.cpp
index 034e1f7..9f7106f 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -6,10 +6,12 @@
6// and against a temporary data directory. 6// and against a temporary data directory.
7 7
8#include "headers_core.h" 8#include "headers_core.h"
9#include "btfaddr.h"
9#include "bip32.h" 10#include "bip32.h"
10#include "proxy.h" 11#include "proxy.h"
11#include "selftest.h" 12#include "selftest.h"
12#include "sockcount.h" 13#include "sockcount.h"
14#include "tor.h"
13#include "walletcmd.h" 15#include "walletcmd.h"
14 16
15extern int RunPoolStratumSelfTest(); 17extern int RunPoolStratumSelfTest();
@@ -587,6 +589,34 @@ static int RunWalletHDSelfTest()
587 "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; 589 "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1;
588 nFail += Check(audit.nLegacyImmatureTx == 1, 590 nFail += Check(audit.nLegacyImmatureTx == 1,
589 "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1; 591 "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1;
592
593 CKey keyLegacyHD11;
594 nSchemaForLegacyAudit = nHDKeySchema;
595 nHDKeySchema = HD_SCHEMA_LEGACY;
596 if (!DeriveHDKey(11, keyLegacyHD11, strError))
597 throw std::runtime_error("legacy compatibility index 11 derivation failed: " + strError);
598 nHDKeySchema = nSchemaForLegacyAudit;
599 if (!AddKey(keyLegacyHD11))
600 throw std::runtime_error("could not store the legacy compatibility index 11 key");
601
602 std::map<unsigned int, std::vector<unsigned char> > mapLegacyProbe;
603 mapLegacyProbe[0] = keyAuditLegacyHD.GetPubKey();
604 mapLegacyProbe[11] = keyLegacyHD11.GetPubKey();
605
606 CWalletTx wtxLegacyHD11;
607 wtxLegacyHD11.vout.push_back(CTxOut(19 * COIN, CScript() << keyLegacyHD11.GetPubKey() << OP_CHECKSIG));
608 CRITICAL_BLOCK(cs_mapWallet)
609 {
610 mapWallet.clear();
611 mapWallet[wtxLegacyHD11.GetHash()] = wtxLegacyHD11;
612 }
613 nFail += Check(WalletLastUsedPubKeyIndexNext(mapLegacyProbe) == 12,
614 "restore collapses legacy hdnext to the last used compatibility index plus one") ? 0 : 1;
615
616 CRITICAL_BLOCK(cs_mapWallet)
617 mapWallet.clear();
618 nFail += Check(WalletLastUsedPubKeyIndexNext(mapLegacyProbe) == 0,
619 "restore does not persist legacy hdnext at the scan depth when no compatibility key was used") ? 0 : 1;
590 } 620 }
591 catch (const std::exception& e) 621 catch (const std::exception& e)
592 { 622 {
@@ -1505,6 +1535,14 @@ static int RunSocks5ProxySelfTest()
1505 !BtfIsTorOnionHost("relay.example") && 1535 !BtfIsTorOnionHost("relay.example") &&
1506 !BtfIsTorOnionHost(".onion"), 1536 !BtfIsTorOnionHost(".onion"),
1507 "detects .onion hosts without accepting lookalikes") ? 0 : 1; 1537 "detects .onion hosts without accepting lookalikes") ? 0 : 1;
1538 std::string onion;
1539 nFail += Check(btf::NormalizeOnionEndpoint("ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCD.onion.:8433", onion) &&
1540 onion == "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:8433",
1541 "normalizes v3 onion peer endpoint") ? 0 : 1;
1542 nFail += Check(!btf::NormalizeOnionEndpoint("short.onion:8433", onion) &&
1543 !btf::NormalizeOnionEndpoint("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:0", onion) &&
1544 !btf::NormalizeOnionEndpoint("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc1.onion:8433", onion),
1545 "rejects malformed onion peer endpoints") ? 0 : 1;
1508 1546
1509 BtfClearSocks5Proxy(); 1547 BtfClearSocks5Proxy();
1510 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1; 1548 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1;
@@ -1554,6 +1592,41 @@ static int RunSocks5ProxySelfTest()
1554 return nFail == 0 ? 0 : 1; 1592 return nFail == 0 ? 0 : 1;
1555} 1593}
1556 1594
1595static int RunManagedTorSelfTest()
1596{
1597 printf("managed-tor self-test\n");
1598 int nFail = 0;
1599
1600 std::string torrc = BtfBuildManagedTorrcForTest("C:/Bitflash Managed Tor/data",
1601 "C:/Bitflash Managed Tor/onion-service",
1602 19050, 19051, 8433);
1603 nFail += Check(torrc.find("DataDirectory \"C:/Bitflash Managed Tor/data\"") != std::string::npos,
1604 "quotes the Tor data directory") ? 0 : 1;
1605 nFail += Check(torrc.find("SocksPort 127.0.0.1:19050") != std::string::npos,
1606 "binds SOCKS5 to localhost only") ? 0 : 1;
1607 nFail += Check(torrc.find("ControlPort 127.0.0.1:19051") != std::string::npos &&
1608 torrc.find("CookieAuthentication 1") != std::string::npos,
1609 "binds an authenticated Tor control port to localhost only") ? 0 : 1;
1610 nFail += Check(torrc.find("IsolateSOCKSAuth") != std::string::npos &&
1611 torrc.find("IsolateClientAddr") != std::string::npos &&
1612 torrc.find("IsolateDestAddr") != std::string::npos &&
1613 torrc.find("IsolateDestPort") != std::string::npos,
1614 "enables strict Tor stream isolation") ? 0 : 1;
1615 nFail += Check(torrc.find("HiddenServiceDir \"C:/Bitflash Managed Tor/onion-service\"") != std::string::npos,
1616 "writes a hidden service directory") ? 0 : 1;
1617 nFail += Check(torrc.find("HiddenServiceVersion 3") != std::string::npos,
1618 "requests a v3 onion service") ? 0 : 1;
1619 nFail += Check(torrc.find("HiddenServicePort 8433 127.0.0.1:8433") != std::string::npos,
1620 "maps the onion service to the Bitflash P2P listener") ? 0 : 1;
1621 nFail += Check(BtfManagedTorStatus() == "disabled",
1622 "managed Tor starts disabled") ? 0 : 1;
1623
1624 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1625 nFail, nFail == 1 ? "" : "s");
1626 fflush(stdout);
1627 return nFail == 0 ? 0 : 1;
1628}
1629
1557int RunSelfTest(const std::string& name) 1630int RunSelfTest(const std::string& name)
1558{ 1631{
1559 AttachTerminal(); 1632 AttachTerminal();
@@ -1580,8 +1653,10 @@ int RunSelfTest(const std::string& name)
1580 return RunParseMoneySelfTest(); 1653 return RunParseMoneySelfTest();
1581 if (name == "socks5-proxy") 1654 if (name == "socks5-proxy")
1582 return RunSocks5ProxySelfTest(); 1655 return RunSocks5ProxySelfTest();
1656 if (name == "managed-tor")
1657 return RunManagedTorSelfTest();
1583 1658
1584 printf("Unknown self-test '%s'\n", name.c_str()); 1659 printf("Unknown self-test '%s'\n", name.c_str());
1585 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, payout-ledger, parse-money, socks5-proxy\n"); 1660 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, payout-ledger, parse-money, socks5-proxy, managed-tor\n");
1586 return 1; 1661 return 1;
1587} 1662}
diff --git a/src/sockcount.h b/src/sockcount.h
index 5147f55..a63c6be 100644
--- a/src/sockcount.h
+++ b/src/sockcount.h
@@ -63,6 +63,7 @@ enum
63 SOCK_PAIR_APP, // loopback pair: the app end handed to CNode 63 SOCK_PAIR_APP, // loopback pair: the app end handed to CNode
64 SOCK_PAIR_PUMP, // loopback pair: the pump end 64 SOCK_PAIR_PUMP, // loopback pair: the pump end
65 SOCK_NOSTR, // outbound to a Nostr relay 65 SOCK_NOSTR, // outbound to a Nostr relay
66 SOCK_ONION_PEER, // direct outbound to a peer's Tor hidden service
66 SOCK_STRATUM_BRIDGE, // local Stratum bridge listener/miner sockets 67 SOCK_STRATUM_BRIDGE, // local Stratum bridge listener/miner sockets
67 SOCK_SITES 68 SOCK_SITES
68}; 69};
diff --git a/src/test_btfaddr.cpp b/src/test_btfaddr.cpp
index b40dcc1..8de426e 100644
--- a/src/test_btfaddr.cpp
+++ b/src/test_btfaddr.cpp
@@ -86,6 +86,7 @@ int main()
86 } 86 }
87 87
88 const std::string ENC_HEX = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899"; 88 const std::string ENC_HEX = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899";
89 const std::string ONION = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:8433";
89 90
90 // helper: fresh keypair -> (seckey, xonly pubkey) 91 // helper: fresh keypair -> (seckey, xonly pubkey)
91 auto newKey = [&](unsigned char sk[32], unsigned char pk[32]) { 92 auto newKey = [&](unsigned char sk[32], unsigned char pk[32]) {
@@ -108,6 +109,17 @@ int main()
108 CHECK(!btf::VerifyDescriptor(ctx, d, pk2, bad), "different key rejected"); 109 CHECK(!btf::VerifyDescriptor(ctx, d, pk2, bad), "different key rejected");
109 } 110 }
110 111
112 printf("descriptor_signs_optional_onion_endpoint\n");
113 {
114 unsigned char sk[32], pk[32]; newKey(sk, pk);
115 std::string d = btf::SignDescriptor(ctx, sk, ENC_HEX, "meeting-node-abc", 1000, ONION);
116 CHECK(!d.empty(), "sign produced onion descriptor");
117 btf::Descriptor got;
118 CHECK(btf::VerifyDescriptor(ctx, d, pk, got), "onion descriptor verifies");
119 CHECK(got.meeting_node == "meeting-node-abc" && got.enc == ENC_HEX &&
120 got.created == 1000 && got.onion == ONION, "onion fields match");
121 }
122
111 printf("descriptor_rejects_tampering\n"); 123 printf("descriptor_rejects_tampering\n");
112 { 124 {
113 unsigned char sk[32], pk[32]; newKey(sk, pk); 125 unsigned char sk[32], pk[32]; newKey(sk, pk);
@@ -117,6 +129,16 @@ int main()
117 CHECK(!btf::VerifyDescriptor(ctx, j.dump(), pk, o), "tampered meeting_node rejected"); 129 CHECK(!btf::VerifyDescriptor(ctx, j.dump(), pk, o), "tampered meeting_node rejected");
118 json j2 = json::parse(d); j2["enc"] = "00000000000000000000000000000000000000000000000000000000deadbeef"; 130 json j2 = json::parse(d); j2["enc"] = "00000000000000000000000000000000000000000000000000000000deadbeef";
119 CHECK(!btf::VerifyDescriptor(ctx, j2.dump(), pk, o), "tampered enc rejected"); 131 CHECK(!btf::VerifyDescriptor(ctx, j2.dump(), pk, o), "tampered enc rejected");
132
133 std::string d3 = btf::SignDescriptor(ctx, sk, ENC_HEX, "node-1", 5, ONION);
134 json j3 = json::parse(d3);
135 j3["onion"] = "bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcde.onion:8433";
136 CHECK(btf::VerifyDescriptor(ctx, j3.dump(), pk, o) && o.onion.empty(),
137 "tampered onion ignored rather than trusted");
138 json j4 = json::parse(d3);
139 j4["sig3"] = std::string(128, '0');
140 CHECK(btf::VerifyDescriptor(ctx, j4.dump(), pk, o) && o.onion.empty(),
141 "bad onion signature ignored rather than trusted");
120 } 142 }
121 143
122 printf("full_resolve_chain_from_address\n"); 144 printf("full_resolve_chain_from_address\n");
diff --git a/src/tor.cpp b/src/tor.cpp
new file mode 100644
index 0000000..20b4577
--- /dev/null
+++ b/src/tor.cpp
@@ -0,0 +1,881 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3
4#include "headers_core.h"
5#include "tor.h"
6#include "nostr.h"
7#include "proxy.h"
8
9#include <errno.h>
10
11#ifndef _WIN32
12#include <signal.h>
13#include <sys/wait.h>
14#endif
15
16static CCriticalSection cs_managedTor;
17static bool g_fManagedTor = false;
18static bool g_fManagedTorStopping = false;
19static unsigned short g_managedTorSocksPort = 0;
20static unsigned short g_managedTorControlPort = 0;
21static unsigned short g_managedTorP2PPort = 0;
22static std::string g_managedTorPath;
23static std::string g_managedTorDataDir;
24static std::string g_managedTorHiddenServiceDir;
25static std::string g_managedTorOnion;
26static std::string g_managedTorStatus;
27#ifdef _WIN32
28static PROCESS_INFORMATION g_managedTorProcess;
29#else
30static pid_t g_managedTorPid = -1;
31#endif
32
33static std::string PathJoin(const std::string& a, const std::string& b)
34{
35 if (a.empty())
36 return b;
37 char last = a[a.size() - 1];
38 if (last == '/' || last == '\\')
39 return a + b;
40 return a + "/" + b;
41}
42
43static std::string NormalizeTorrcPath(std::string path)
44{
45 for (size_t i = 0; i < path.size(); i++)
46 if (path[i] == '\\')
47 path[i] = '/';
48 return path;
49}
50
51static std::string QuoteTorrcPath(const std::string& path)
52{
53 std::string out = "\"";
54 for (size_t i = 0; i < path.size(); i++)
55 {
56 if (path[i] == '"' || path[i] == '\\')
57 out += '\\';
58 out += path[i];
59 }
60 out += "\"";
61 return out;
62}
63
64static std::string QuoteCommandArg(const std::string& arg)
65{
66 std::string out = "\"";
67 for (size_t i = 0; i < arg.size(); i++)
68 {
69 if (arg[i] == '"')
70 out += '\\';
71 out += arg[i];
72 }
73 out += "\"";
74 return out;
75}
76
77static bool EnsureDir(const std::string& path, std::string& errOut)
78{
79 if (path.empty())
80 {
81 errOut = "directory path is empty";
82 return false;
83 }
84#ifdef _WIN32
85 DWORD attr = GetFileAttributesA(path.c_str());
86 if (attr != INVALID_FILE_ATTRIBUTES)
87 {
88 if (attr & FILE_ATTRIBUTE_DIRECTORY)
89 return true;
90 errOut = strprintf("%s exists and is not a directory", path.c_str());
91 return false;
92 }
93 if (CreateDirectoryA(path.c_str(), NULL))
94 return true;
95 DWORD err = GetLastError();
96 if (err == ERROR_ALREADY_EXISTS)
97 return true;
98 errOut = strprintf("could not create %s (error %lu)", path.c_str(), (unsigned long)err);
99 return false;
100#else
101 struct stat st;
102 if (stat(path.c_str(), &st) == 0)
103 {
104 if (S_ISDIR(st.st_mode))
105 {
106 chmod(path.c_str(), 0700);
107 return true;
108 }
109 errOut = strprintf("%s exists and is not a directory", path.c_str());
110 return false;
111 }
112 if (mkdir(path.c_str(), 0700) == 0)
113 return true;
114 if (errno == EEXIST)
115 {
116 if (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode))
117 {
118 chmod(path.c_str(), 0700);
119 return true;
120 }
121 }
122 errOut = strprintf("could not create %s: %s", path.c_str(), strerror(errno));
123 return false;
124#endif
125}
126
127static bool WriteTextFile(const std::string& path, const std::string& data,
128 std::string& errOut)
129{
130 std::string tmp = path + ".tmp";
131 FILE* f = fopen(tmp.c_str(), "wb");
132 if (!f)
133 {
134 errOut = strprintf("could not write %s", tmp.c_str());
135 return false;
136 }
137 bool ok = fwrite(data.data(), 1, data.size(), f) == data.size();
138 if (fclose(f) != 0)
139 ok = false;
140 if (!ok)
141 {
142 remove(tmp.c_str());
143 errOut = strprintf("could not finish writing %s", tmp.c_str());
144 return false;
145 }
146#ifdef _WIN32
147 if (!MoveFileExA(tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
148 {
149 remove(tmp.c_str());
150 errOut = strprintf("could not install %s atomically (error %lu)",
151 path.c_str(), (unsigned long)GetLastError());
152 return false;
153 }
154#else
155 if (rename(tmp.c_str(), path.c_str()) != 0)
156 {
157 remove(tmp.c_str());
158 errOut = strprintf("could not install %s atomically: %s",
159 path.c_str(), strerror(errno));
160 return false;
161 }
162#endif
163 return true;
164}
165
166static bool ReadBinaryFile(const std::string& path, std::vector<unsigned char>& data,
167 std::string& errOut)
168{
169 data.clear();
170 FILE* f = fopen(path.c_str(), "rb");
171 if (!f)
172 {
173 errOut = strprintf("could not read %s", path.c_str());
174 return false;
175 }
176 unsigned char buf[256];
177 for (;;)
178 {
179 size_t n = fread(buf, 1, sizeof(buf), f);
180 if (n > 0)
181 data.insert(data.end(), buf, buf + n);
182 if (n < sizeof(buf))
183 {
184 if (ferror(f))
185 {
186 fclose(f);
187 errOut = strprintf("could not finish reading %s", path.c_str());
188 return false;
189 }
190 break;
191 }
192 if (data.size() > 1024)
193 {
194 fclose(f);
195 errOut = strprintf("%s is unexpectedly large", path.c_str());
196 return false;
197 }
198 }
199 fclose(f);
200 if (data.empty())
201 {
202 errOut = strprintf("%s is empty", path.c_str());
203 return false;
204 }
205 return true;
206}
207
208static void SetSocketTimeouts(SOCKET s, int millis)
209{
210#ifdef _WIN32
211 DWORD timeout = millis;
212 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
213 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout));
214#else
215 struct timeval tv;
216 tv.tv_sec = millis / 1000;
217 tv.tv_usec = (millis % 1000) * 1000;
218 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
219 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
220#endif
221}
222
223static bool SendAll(SOCKET s, const std::string& data)
224{
225 const char* p = data.data();
226 int left = (int)data.size();
227 while (left > 0)
228 {
229 int n = send(s, p, left, 0);
230 if (n <= 0)
231 return false;
232 p += n;
233 left -= n;
234 }
235 return true;
236}
237
238static bool RecvControlLine(SOCKET s, std::string& line)
239{
240 line.clear();
241 while (line.size() < 4096)
242 {
243 char ch = 0;
244 int n = recv(s, &ch, 1, 0);
245 if (n <= 0)
246 return false;
247 if (ch == '\n')
248 return true;
249 if (ch != '\r')
250 line += ch;
251 }
252 return false;
253}
254
255static SOCKET ConnectLoopback(unsigned short port, std::string& errOut)
256{
257#ifdef _WIN32
258 WSADATA wsadata;
259 int ret = WSAStartup(MAKEWORD(2,2), &wsadata);
260 if (ret != NO_ERROR)
261 {
262 errOut = strprintf("could not start Winsock for Tor control shutdown (WSAStartup returned %d)", ret);
263 return INVALID_SOCKET;
264 }
265#endif
266 SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
267 if (s == INVALID_SOCKET)
268 {
269 errOut = "could not create Tor control socket";
270#ifdef _WIN32
271 WSACleanup();
272#endif
273 return INVALID_SOCKET;
274 }
275 SetSocketTimeouts(s, 3000);
276 struct sockaddr_in addr;
277 memset(&addr, 0, sizeof(addr));
278 addr.sin_family = AF_INET;
279 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
280 addr.sin_port = htons(port);
281 if (connect(s, (struct sockaddr*)&addr, sizeof(addr)) != 0)
282 {
283 closesocket(s);
284 errOut = strprintf("could not connect to Tor control port 127.0.0.1:%u", (unsigned)port);
285#ifdef _WIN32
286 WSACleanup();
287#endif
288 return INVALID_SOCKET;
289 }
290 return s;
291}
292
293static void CloseLoopback(SOCKET s)
294{
295 if (s != INVALID_SOCKET)
296 closesocket(s);
297#ifdef _WIN32
298 WSACleanup();
299#endif
300}
301
302static bool ReadPositiveTorControlReply(SOCKET s, std::string& errOut)
303{
304 std::string line;
305 if (!RecvControlLine(s, line))
306 {
307 errOut = "Tor control port closed before replying";
308 return false;
309 }
310 if (line.size() >= 3 && line.substr(0, 3) == "250")
311 return true;
312 errOut = strprintf("Tor control command failed: %s", line.c_str());
313 return false;
314}
315
316static bool RequestManagedTorShutdown(std::string& errOut)
317{
318 std::string dataDir;
319 unsigned short controlPort = 0;
320 CRITICAL_BLOCK(cs_managedTor)
321 {
322 dataDir = g_managedTorDataDir;
323 controlPort = g_managedTorControlPort;
324 }
325 if (controlPort == 0 || dataDir.empty())
326 {
327 errOut = "Tor control port is not available";
328 return false;
329 }
330
331 std::vector<unsigned char> cookie;
332 if (!ReadBinaryFile(PathJoin(dataDir, "control_auth_cookie"), cookie, errOut))
333 return false;
334
335 SOCKET s = ConnectLoopback(controlPort, errOut);
336 if (s == INVALID_SOCKET)
337 return false;
338
339 bool ok = false;
340 std::string auth = "AUTHENTICATE " + HexStr(cookie.begin(), cookie.end(), false) + "\r\n";
341 if (!SendAll(s, auth) || !ReadPositiveTorControlReply(s, errOut))
342 goto done;
343
344 if (!SendAll(s, "SIGNAL SHUTDOWN\r\n"))
345 {
346 errOut = "could not send Tor shutdown signal";
347 goto done;
348 }
349
350 {
351 std::string reply;
352 if (RecvControlLine(s, reply) && !(reply.size() >= 3 && reply.substr(0, 3) == "250"))
353 {
354 errOut = strprintf("Tor rejected shutdown signal: %s", reply.c_str());
355 goto done;
356 }
357 }
358 ok = true;
359
360done:
361 CloseLoopback(s);
362 return ok;
363}
364
365static bool ReadOneLine(const std::string& path, std::string& out)
366{
367 out.clear();
368 FILE* f = fopen(path.c_str(), "rb");
369 if (!f)
370 return false;
371 char buf[256] = {0};
372 if (!fgets(buf, sizeof(buf), f))
373 {
374 fclose(f);
375 return false;
376 }
377 fclose(f);
378 out = buf;
379 while (!out.empty() && (out[out.size() - 1] == '\n' || out[out.size() - 1] == '\r' ||
380 out[out.size() - 1] == ' ' || out[out.size() - 1] == '\t'))
381 out.erase(out.size() - 1);
382 return !out.empty();
383}
384
385static bool PickLocalPort(unsigned short& portOut, std::string& errOut)
386{
387 portOut = 0;
388 bool ok = false;
389#ifdef _WIN32
390 WSADATA wsadata;
391 int ret = WSAStartup(MAKEWORD(2,2), &wsadata);
392 if (ret != NO_ERROR)
393 {
394 errOut = strprintf("could not start Winsock for local port probe (WSAStartup returned %d)", ret);
395 return false;
396 }
397#endif
398 struct sockaddr_in addr;
399 socklen_t len = sizeof(addr);
400 SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
401 if (s == INVALID_SOCKET)
402 {
403 errOut = "could not create local port probe socket";
404 goto done;
405 }
406 memset(&addr, 0, sizeof(addr));
407 addr.sin_family = AF_INET;
408 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
409 addr.sin_port = 0;
410 if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) != 0)
411 {
412 errOut = "could not bind local port probe socket";
413 goto done;
414 }
415 if (getsockname(s, (struct sockaddr*)&addr, &len) != 0)
416 {
417 errOut = "could not read local port probe socket";
418 goto done;
419 }
420 portOut = ntohs(addr.sin_port);
421 if (portOut == 0)
422 {
423 errOut = "local port probe returned port zero";
424 goto done;
425 }
426 ok = true;
427done:
428 if (s != INVALID_SOCKET)
429 closesocket(s);
430#ifdef _WIN32
431 WSACleanup();
432#endif
433 return ok;
434}
435
436static std::string ExecutableDir()
437{
438#ifdef _WIN32
439 char path[MAX_PATH + 1] = {0};
440 DWORD n = GetModuleFileNameA(NULL, path, MAX_PATH);
441 if (n == 0 || n >= MAX_PATH)
442 return ".";
443 std::string s = path;
444 size_t slash = s.find_last_of("\\/");
445 if (slash == std::string::npos)
446 return ".";
447 return s.substr(0, slash);
448#else
449 char path[4096] = {0};
450 ssize_t n = readlink("/proc/self/exe", path, sizeof(path) - 1);
451 if (n <= 0)
452 return ".";
453 path[n] = 0;
454 std::string s = path;
455 size_t slash = s.find_last_of('/');
456 if (slash == std::string::npos)
457 return ".";
458 return s.substr(0, slash);
459#endif
460}
461
462static bool FileIsExecutableCandidate(const std::string& path)
463{
464#ifdef _WIN32
465 DWORD attr = GetFileAttributesA(path.c_str());
466 return attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY);
467#else
468 return access(path.c_str(), X_OK) == 0;
469#endif
470}
471
472static std::string ResolveTorPath(const std::string& torPathOpt)
473{
474 if (!torPathOpt.empty())
475 return torPathOpt;
476
477 std::vector<std::string> candidates;
478 std::string exeDir = ExecutableDir();
479#ifdef _WIN32
480 candidates.push_back(PathJoin(exeDir, "tor/tor.exe"));
481 candidates.push_back(PathJoin(exeDir, "tor.exe"));
482 candidates.push_back("tor.exe");
483#else
484 candidates.push_back(PathJoin(exeDir, "tor/tor"));
485 candidates.push_back(PathJoin(exeDir, "tor"));
486 candidates.push_back("tor");
487#endif
488 for (size_t i = 0; i < candidates.size(); i++)
489 {
490 if (candidates[i].find('/') == std::string::npos &&
491 candidates[i].find('\\') == std::string::npos)
492 return candidates[i]; // Let the OS search PATH.
493 if (FileIsExecutableCandidate(candidates[i]))
494 return candidates[i];
495 }
496#ifdef _WIN32
497 return "tor.exe";
498#else
499 return "tor";
500#endif
501}
502
503bool BtfBundledTorPath(std::string& torPathOut)
504{
505 torPathOut.clear();
506 std::string exeDir = ExecutableDir();
507#ifdef _WIN32
508 std::string path = PathJoin(exeDir, "tor/tor.exe");
509#else
510 std::string path = PathJoin(exeDir, "tor/tor");
511#endif
512 if (!FileIsExecutableCandidate(path))
513 return false;
514 torPathOut = path;
515 return true;
516}
517
518std::string BtfBuildManagedTorrcForTest(const std::string& dataDir,
519 const std::string& hiddenServiceDir,
520 unsigned short socksPort,
521 unsigned short controlPort,
522 unsigned short p2pPort)
523{
524 std::string torData = NormalizeTorrcPath(dataDir);
525 std::string hsDir = NormalizeTorrcPath(hiddenServiceDir);
526 std::string s;
527 s += "# Generated by Bitflash. Edit only while Bitflash is not managing Tor.\n";
528 s += "DataDirectory " + QuoteTorrcPath(torData) + "\n";
529 s += strprintf("SocksPort 127.0.0.1:%u IsolateSOCKSAuth IsolateClientAddr IsolateDestAddr IsolateDestPort\n",
530 (unsigned)socksPort);
531 s += strprintf("ControlPort 127.0.0.1:%u\n", (unsigned)controlPort);
532 s += "CookieAuthentication 1\n";
533 s += "HiddenServiceDir " + QuoteTorrcPath(hsDir) + "\n";
534 s += "HiddenServiceVersion 3\n";
535 s += strprintf("HiddenServicePort %u 127.0.0.1:%u\n",
536 (unsigned)p2pPort, (unsigned)p2pPort);
537 return s;
538}
539
540static bool StartTorProcess(const std::string& torPath, const std::string& torrcPath,
541 std::string& errOut)
542{
543#ifdef _WIN32
544 memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess));
545 STARTUPINFOA si;
546 memset(&si, 0, sizeof(si));
547 si.cb = sizeof(si);
548 std::string cmd = QuoteCommandArg(torPath) + " -f " + QuoteCommandArg(torrcPath);
549 std::vector<char> mutableCmd(cmd.begin(), cmd.end());
550 mutableCmd.push_back(0);
551 if (!CreateProcessA(NULL, &mutableCmd[0], NULL, NULL, FALSE, CREATE_NO_WINDOW,
552 NULL, NULL, &si, &g_managedTorProcess))
553 {
554 errOut = strprintf("could not start Tor process %s (error %lu)",
555 torPath.c_str(), (unsigned long)GetLastError());
556 return false;
557 }
558 return true;
559#else
560 g_managedTorPid = fork();
561 if (g_managedTorPid < 0)
562 {
563 errOut = strprintf("could not fork Tor process: %s", strerror(errno));
564 return false;
565 }
566 if (g_managedTorPid == 0)
567 {
568 execlp(torPath.c_str(), torPath.c_str(), "-f", torrcPath.c_str(), (char*)NULL);
569 _exit(127);
570 }
571 return true;
572#endif
573}
574
575static bool ManagedTorProcessAlive(bool& fExited, int& nExitCode)
576{
577 fExited = false;
578 nExitCode = 0;
579#ifdef _WIN32
580 HANDLE hProcess = NULL;
581 CRITICAL_BLOCK(cs_managedTor)
582 hProcess = g_managedTorProcess.hProcess;
583 if (!hProcess)
584 return false;
585 DWORD code = 0;
586 if (!GetExitCodeProcess(hProcess, &code))
587 return false;
588 if (code != STILL_ACTIVE)
589 {
590 fExited = true;
591 nExitCode = (int)code;
592 return false;
593 }
594 return true;
595#else
596 pid_t pid = -1;
597 CRITICAL_BLOCK(cs_managedTor)
598 pid = g_managedTorPid;
599 if (pid <= 0)
600 return false;
601 int status = 0;
602 pid_t r = waitpid(pid, &status, WNOHANG);
603 if (r == 0)
604 return true;
605 if (r == pid)
606 {
607 fExited = true;
608 if (WIFEXITED(status))
609 nExitCode = WEXITSTATUS(status);
610 else if (WIFSIGNALED(status))
611 nExitCode = 128 + WTERMSIG(status);
612 CRITICAL_BLOCK(cs_managedTor)
613 if (g_managedTorPid == pid)
614 g_managedTorPid = -1;
615 }
616 return false;
617#endif
618}
619
620static bool ValidateManagedTorStartup(std::string& errOut)
621{
622 Sleep(500);
623 bool fExited = false;
624 int nExitCode = 0;
625 if (ManagedTorProcessAlive(fExited, nExitCode))
626 return true;
627 if (fExited)
628 errOut = strprintf("Tor exited during startup with code %d", nExitCode);
629 else
630 errOut = "Tor process is not running after startup";
631 return false;
632}
633
634static void MarkManagedTorDead(int nExitCode)
635{
636 BtfStopManagedTor();
637 CRITICAL_BLOCK(cs_managedTor)
638 g_managedTorStatus = strprintf("stopped unexpectedly, Tor exit code %d", nExitCode);
639}
640
641static void ThreadManagedTorWatchdog(void*)
642{
643 for (;;)
644 {
645 Sleep(5000);
646 CRITICAL_BLOCK(cs_managedTor)
647 {
648 if (!g_fManagedTor || g_fManagedTorStopping || fShutdown)
649 return;
650 }
651
652 bool fExited = false;
653 int nExitCode = 0;
654 if (!ManagedTorProcessAlive(fExited, nExitCode))
655 {
656 if (fExited)
657 MarkManagedTorDead(nExitCode);
658 else
659 {
660 BtfStopManagedTor();
661 CRITICAL_BLOCK(cs_managedTor)
662 g_managedTorStatus = "stopped unexpectedly, Tor process handle is gone";
663 }
664 return;
665 }
666 }
667}
668
669static void ThreadManagedTorHostname(void*)
670{
671 std::string hostnamePath;
672 unsigned short p2pPort = 0;
673 CRITICAL_BLOCK(cs_managedTor)
674 {
675 hostnamePath = PathJoin(g_managedTorHiddenServiceDir, "hostname");
676 p2pPort = g_managedTorP2PPort;
677 }
678
679 for (int i = 0; i < 90 && !fShutdown; i++)
680 {
681 std::string onion;
682 if (ReadOneLine(hostnamePath, onion))
683 {
684 std::string endpoint = strprintf("%s:%u", onion.c_str(), (unsigned)p2pPort);
685 std::string err;
686 if (BtfSetLocalOnionEndpoint(endpoint, err))
687 {
688 CRITICAL_BLOCK(cs_managedTor)
689 {
690 g_managedTorOnion = BtfLocalOnionEndpoint();
691 g_managedTorStatus = strprintf("running, SOCKS5 127.0.0.1:%u, onion %s",
692 (unsigned)g_managedTorSocksPort,
693 g_managedTorOnion.c_str());
694 }
695 printf("Managed Tor hidden service ready: %s\n", BtfLocalOnionEndpoint().c_str());
696 return;
697 }
698 CRITICAL_BLOCK(cs_managedTor)
699 g_managedTorStatus = strprintf("running, but generated onion endpoint was rejected: %s",
700 err.c_str());
701 return;
702 }
703 Sleep(1000);
704 }
705
706 BtfStopManagedTor();
707 CRITICAL_BLOCK(cs_managedTor)
708 g_managedTorStatus = "failed: Tor did not create a hidden service hostname within 90s";
709}
710
711bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut)
712{
713 CRITICAL_BLOCK(cs_managedTor)
714 {
715 if (g_fManagedTor)
716 {
717 errOut = "managed Tor is already running";
718 return false;
719 }
720 }
721
722 std::string root = PathJoin(GetAppDir(), "managed-tor");
723 std::string dataDir = PathJoin(root, "data");
724 std::string hsDir = PathJoin(root, "onion-service");
725 std::string torrcPath = PathJoin(root, "torrc");
726 if (!EnsureDir(root, errOut) || !EnsureDir(dataDir, errOut) || !EnsureDir(hsDir, errOut))
727 return false;
728
729 unsigned short socksPort = 0;
730 if (!PickLocalPort(socksPort, errOut))
731 return false;
732 unsigned short controlPort = 0;
733 if (!PickLocalPort(controlPort, errOut))
734 return false;
735
736 unsigned short p2pPort = ntohs(nListenPort);
737 std::string torrc = BtfBuildManagedTorrcForTest(dataDir, hsDir, socksPort,
738 controlPort, p2pPort);
739 if (!WriteTextFile(torrcPath, torrc, errOut))
740 return false;
741
742 std::string torPath = ResolveTorPath(torPathOpt);
743 {
744 CRITICAL_BLOCK(cs_managedTor)
745 {
746 g_fManagedTor = true;
747 g_fManagedTorStopping = false;
748 g_managedTorPath = torPath;
749 g_managedTorDataDir = dataDir;
750 g_managedTorHiddenServiceDir = hsDir;
751 g_managedTorSocksPort = socksPort;
752 g_managedTorControlPort = controlPort;
753 g_managedTorP2PPort = p2pPort;
754 g_managedTorOnion.clear();
755 g_managedTorStatus = strprintf("starting %s, SOCKS5 127.0.0.1:%u",
756 torPath.c_str(), (unsigned)socksPort);
757 }
758 }
759
760 if (!StartTorProcess(torPath, torrcPath, errOut))
761 {
762 CRITICAL_BLOCK(cs_managedTor)
763 {
764 g_fManagedTor = false;
765 g_managedTorStatus = strprintf("failed: %s", errOut.c_str());
766 }
767 return false;
768 }
769 if (!ValidateManagedTorStartup(errOut))
770 {
771 BtfStopManagedTor();
772 CRITICAL_BLOCK(cs_managedTor)
773 g_managedTorStatus = strprintf("failed: %s", errOut.c_str());
774 return false;
775 }
776
777 std::string proxyErr;
778 if (!BtfEnableTorProxy(strprintf("127.0.0.1:%u", (unsigned)socksPort), proxyErr))
779 {
780 BtfStopManagedTor();
781 errOut = strprintf("could not enable managed Tor SOCKS5 proxy: %s", proxyErr.c_str());
782 return false;
783 }
784
785 if (_beginthread(ThreadManagedTorHostname, 0, NULL) == (uintptr_t)-1)
786 {
787 BtfStopManagedTor();
788 errOut = "could not start managed Tor hostname watcher";
789 return false;
790 }
791 if (_beginthread(ThreadManagedTorWatchdog, 0, NULL) == (uintptr_t)-1)
792 {
793 BtfStopManagedTor();
794 errOut = "could not start managed Tor watchdog";
795 return false;
796 }
797
798 return true;
799}
800
801void BtfStopManagedTor()
802{
803 bool fWasRunning = false;
804 CRITICAL_BLOCK(cs_managedTor)
805 {
806 fWasRunning = g_fManagedTor;
807 if (!fWasRunning)
808 return;
809 g_fManagedTorStopping = true;
810 g_fManagedTor = false;
811 g_managedTorStatus = "stopping";
812 }
813#ifdef _WIN32
814 if (g_managedTorProcess.hProcess)
815 {
816 std::string shutdownErr;
817 bool fGraceful = RequestManagedTorShutdown(shutdownErr);
818 DWORD waitResult = WaitForSingleObject(g_managedTorProcess.hProcess,
819 fGraceful ? 10000 : 1000);
820 if (waitResult != WAIT_OBJECT_0)
821 {
822 TerminateProcess(g_managedTorProcess.hProcess, 0);
823 WaitForSingleObject(g_managedTorProcess.hProcess, 5000);
824 }
825 CloseHandle(g_managedTorProcess.hProcess);
826 CloseHandle(g_managedTorProcess.hThread);
827 memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess));
828 }
829#else
830 if (g_managedTorPid > 0)
831 {
832 std::string shutdownErr;
833 if (!RequestManagedTorShutdown(shutdownErr))
834 kill(g_managedTorPid, SIGTERM);
835 bool fExited = false;
836 for (int i = 0; i < 100; i++)
837 {
838 int status = 0;
839 pid_t r = waitpid(g_managedTorPid, &status, WNOHANG);
840 if (r == g_managedTorPid)
841 {
842 fExited = true;
843 break;
844 }
845 Sleep(100);
846 }
847 if (!fExited)
848 {
849 kill(g_managedTorPid, SIGKILL);
850 waitpid(g_managedTorPid, NULL, 0);
851 }
852 g_managedTorPid = -1;
853 }
854#endif
855 CRITICAL_BLOCK(cs_managedTor)
856 {
857 g_managedTorStatus = "stopped";
858 g_managedTorOnion.clear();
859 g_managedTorSocksPort = 0;
860 g_managedTorControlPort = 0;
861 g_managedTorP2PPort = 0;
862 }
863}
864
865bool BtfManagedTorEnabled()
866{
867 CRITICAL_BLOCK(cs_managedTor)
868 return g_fManagedTor;
869 return false;
870}
871
872std::string BtfManagedTorStatus()
873{
874 CRITICAL_BLOCK(cs_managedTor)
875 {
876 if (!g_fManagedTor && g_managedTorStatus.empty())
877 return "disabled";
878 return g_managedTorStatus;
879 }
880 return "disabled";
881}
diff --git a/src/tor.h b/src/tor.h
new file mode 100644
index 0000000..cfb87a5
--- /dev/null
+++ b/src/tor.h
@@ -0,0 +1,24 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// Managed Tor process support. This is transport bootstrapping only: it does
5// not touch consensus, wallet keys, mining, or the .btf identity.
6
7#ifndef BITFLASH_TOR_H
8#define BITFLASH_TOR_H
9
10#include <string>
11
12bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut);
13void BtfStopManagedTor();
14bool BtfManagedTorEnabled();
15std::string BtfManagedTorStatus();
16bool BtfBundledTorPath(std::string& torPathOut);
17
18std::string BtfBuildManagedTorrcForTest(const std::string& dataDir,
19 const std::string& hiddenServiceDir,
20 unsigned short socksPort,
21 unsigned short controlPort,
22 unsigned short p2pPort);
23
24#endif
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp
index 0ea2251..e8ac4bf 100644
--- a/src/walletcmd.cpp
+++ b/src/walletcmd.cpp
@@ -54,6 +54,37 @@ bool RestoreScanReachedDepth(int nSchema,
54 return nLegacyNext >= nDepth; 54 return nLegacyNext >= nDepth;
55} 55}
56 56
57unsigned int WalletLastUsedPubKeyIndexNext(
58 const std::map<unsigned int, std::vector<unsigned char> >& mapPubKeysByIndex)
59{
60 std::map<std::vector<unsigned char>, unsigned int> mapIndexByPubKey;
61 for (std::map<unsigned int, std::vector<unsigned char> >::const_iterator it =
62 mapPubKeysByIndex.begin();
63 it != mapPubKeysByIndex.end(); ++it)
64 mapIndexByPubKey[it->second] = it->first;
65
66 unsigned int nNext = 0;
67 CRITICAL_BLOCK(cs_mapWallet)
68 {
69 for (map<uint256, CWalletTx>::iterator it = mapWallet.begin();
70 it != mapWallet.end(); ++it)
71 {
72 CWalletTx& wtx = (*it).second;
73 for (int i = 0; i < (int)wtx.vout.size(); i++)
74 {
75 vector<unsigned char> vchPubKey;
76 if (!ExtractPubKey(wtx.vout[i].scriptPubKey, false, vchPubKey))
77 continue;
78 std::map<std::vector<unsigned char>, unsigned int>::const_iterator mi =
79 mapIndexByPubKey.find(vchPubKey);
80 if (mi != mapIndexByPubKey.end() && mi->second + 1 > nNext)
81 nNext = mi->second + 1;
82 }
83 }
84 }
85 return nNext;
86}
87
57int CmdNewPhrase() 88int CmdNewPhrase()
58{ 89{
59 AttachTerminal(); 90 AttachTerminal();
@@ -132,7 +163,10 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
132 // Derive forward in batches, scanning after each, until a whole batch turns 163 // Derive forward in batches, scanning after each, until a whole batch turns
133 // up nothing. Every derived key is written to the wallet before the scan, 164 // up nothing. Every derived key is written to the wallet before the scan,
134 // because the scan asks the wallet what belongs to it. 165 // because the scan asks the wallet what belongs to it.
135 int nTotalDerived = nHDKeySchema == HD_SCHEMA_BIP44 166 const bool fRestoreBIP44 = nHDKeySchema == HD_SCHEMA_BIP44;
167 unsigned int nLegacyScanNext = nHDNext;
168 std::map<unsigned int, vector<unsigned char> > mapLegacyScanPubKeys;
169 int nTotalDerived = fRestoreBIP44
136 ? (int)(nHDReceiveNext + nHDChangeNext) 170 ? (int)(nHDReceiveNext + nHDChangeNext)
137 : (int)nHDNext; 171 : (int)nHDNext;
138 int nStopDepth = max(nMinDepth, RESTORE_MIN_SCAN); 172 int nStopDepth = max(nMinDepth, RESTORE_MIN_SCAN);
@@ -151,7 +185,7 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
151 nWalletBefore = mapWallet.size(); 185 nWalletBefore = mapWallet.size();
152 186
153 std::string strDeriveError; 187 std::string strDeriveError;
154 if (nHDKeySchema == HD_SCHEMA_BIP44) 188 if (fRestoreBIP44)
155 { 189 {
156 for (int i = 0; i < RESTORE_BATCH; i++) 190 for (int i = 0; i < RESTORE_BATCH; i++)
157 { 191 {
@@ -182,15 +216,15 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
182 for (int i = 0; i < RESTORE_BATCH; i++) 216 for (int i = 0; i < RESTORE_BATCH; i++)
183 { 217 {
184 CKey key; 218 CKey key;
185 if (!DeriveHDKey(nHDNext, key, strDeriveError)) 219 if (!DeriveHDKey(nLegacyScanNext, key, strDeriveError))
186 break; 220 break;
187 if (!AddKey(key)) 221 if (!AddKey(key))
188 break; 222 break;
189 nHDNext++; 223 mapLegacyScanPubKeys[nLegacyScanNext] = key.GetPubKey();
224 nLegacyScanNext++;
190 nTotalDerived++; 225 nTotalDerived++;
191 } 226 }
192 nHDKeySchema = nSavedSchema; 227 nHDKeySchema = nSavedSchema;
193 CWalletDB().WriteHDNext(nHDNext);
194 } 228 }
195 else 229 else
196 { 230 {
@@ -209,6 +243,22 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
209 243
210 ScanForWalletTransactions(pindexGenesisBlock); 244 ScanForWalletTransactions(pindexGenesisBlock);
211 245
246 if (fRestoreBIP44)
247 {
248 // The legacy branch is scanned only for compatibility with phrases
249 // created before BIP44. Persist the last used legacy index + 1, not
250 // the lookahead depth. Otherwise a restore with no legacy hits can
251 // leave hdnext at RESTORE_MAX and make future compatibility scans
252 // look exhausted even though no legacy key was ever used.
253 unsigned int nLegacyUsedNext =
254 WalletLastUsedPubKeyIndexNext(mapLegacyScanPubKeys);
255 if (nLegacyUsedNext != nHDNext)
256 {
257 nHDNext = nLegacyUsedNext;
258 CWalletDB().WriteHDNext(nHDNext);
259 }
260 }
261
212 size_t nWalletAfter = 0; 262 size_t nWalletAfter = 0;
213 CRITICAL_BLOCK(cs_mapWallet) 263 CRITICAL_BLOCK(cs_mapWallet)
214 nWalletAfter = mapWallet.size(); 264 nWalletAfter = mapWallet.size();
@@ -239,7 +289,7 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
239 RestoreScanReachedDepth(nHDKeySchema, 289 RestoreScanReachedDepth(nHDKeySchema,
240 nHDReceiveNext, 290 nHDReceiveNext,
241 nHDChangeNext, 291 nHDChangeNext,
242 nHDNext, 292 fRestoreBIP44 ? nLegacyScanNext : nHDNext,
243 nStopDepth)) 293 nStopDepth))
244 break; 294 break;
245 } 295 }
diff --git a/src/walletcmd.h b/src/walletcmd.h
index 04d3fb7..007b8b5 100644
--- a/src/walletcmd.h
+++ b/src/walletcmd.h
@@ -7,7 +7,9 @@
7#ifndef BITFLASH_WALLETCMD_H 7#ifndef BITFLASH_WALLETCMD_H
8#define BITFLASH_WALLETCMD_H 8#define BITFLASH_WALLETCMD_H
9 9
10#include <map>
10#include <string> 11#include <string>
12#include <vector>
11 13
12// Create a phrase for a wallet that does not have one, install the seed, and 14// Create a phrase for a wallet that does not have one, install the seed, and
13// show the words once. Refuses if a phrase already exists. Returns 0 on success. 15// show the words once. Refuses if a phrase already exists. Returns 0 on success.
@@ -41,6 +43,13 @@ bool RestoreScanReachedDepth(int nSchema,
41 unsigned int nLegacyNext, 43 unsigned int nLegacyNext,
42 int nStopDepth); 44 int nStopDepth);
43 45
46// Given the pubkeys derived while scanning a compatibility branch, return the
47// next index after the highest one that actually appears in wallet
48// transactions. The scan depth can be much larger than this; hdnext must record
49// use, not how far a restore looked ahead.
50unsigned int WalletLastUsedPubKeyIndexNext(
51 const std::map<unsigned int, std::vector<unsigned char> >& mapPubKeysByIndex);
52
44// Take the next address from the key pool and print it. With a recovery phrase 53// Take the next address from the key pool and print it. With a recovery phrase
45// installed the address is derived, so the phrase can bring back whatever is 54// installed the address is derived, so the phrase can bring back whatever is
46// paid to it. 55// paid to it.
diff --git a/tests/scripts/test_bitflash_tools.py b/tests/scripts/test_bitflash_tools.py
index 4b18d1e..3b4c846 100644
--- a/tests/scripts/test_bitflash_tools.py
+++ b/tests/scripts/test_bitflash_tools.py
@@ -159,8 +159,11 @@ def compare_golden(name, actual, update):
159 path.parent.mkdir(parents=True, exist_ok=True) 159 path.parent.mkdir(parents=True, exist_ok=True)
160 path.write_bytes(actual) 160 path.write_bytes(actual)
161 return 161 return
162 expected = path.read_bytes() 162 # The tools promise canonical LF-terminated JSON. On Windows checkouts a
163 if actual != expected: 163 # fixture may still appear as CRLF in the working tree; normalize only line
164 # endings so content, field order and compact serialization still drift-test.
165 expected = path.read_bytes().replace(b"\r\n", b"\n")
166 if actual.replace(b"\r\n", b"\n") != expected:
164 raise AssertionError("golden mismatch: %s" % name) 167 raise AssertionError("golden mismatch: %s" % name)
165 168
166 169