diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-26 22:41:53 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-26 22:41:53 +0000 |
| commit | bbe04dc8c96b4c4766d2771d3438233bf82687c0 (patch) | |
| tree | 62e5a1febfb8e9bc38d133ac7981e9ef93918484 | |
| parent | 991853fa55a749d28d6f9e507ce72b289df92785 (diff) | |
| parent | 9e39a50d1f110020b0841d31e4b931aa877cc448 (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-- | Makefile | 12 | ||||
| -rw-r--r-- | README.md | 63 | ||||
| -rw-r--r-- | docs/mining-calculator.md | 22 | ||||
| -rw-r--r-- | docs/pool-directory.md | 18 | ||||
| -rw-r--r-- | docs/release-verification.md | 18 | ||||
| -rw-r--r-- | docs/rendezvous.md | 72 | ||||
| -rw-r--r-- | docs/site.md | 115 | ||||
| -rw-r--r-- | docs/tor.md | 159 | ||||
| -rwxr-xr-x | scripts/build-explorer.py | 128 | ||||
| -rwxr-xr-x | scripts/make-release-checksums.sh | 2 | ||||
| -rwxr-xr-x | scripts/package-windows-tor.sh | 187 | ||||
| -rw-r--r-- | scripts/smoke-managed-tor-onion.ps1 | 174 | ||||
| -rw-r--r-- | src/Makefile | 5 | ||||
| -rw-r--r-- | src/btfaddr.cpp | 102 | ||||
| -rw-r--r-- | src/btfaddr.h | 8 | ||||
| -rw-r--r-- | src/main_gui.cpp | 96 | ||||
| -rw-r--r-- | src/makefile.mingw | 5 | ||||
| -rw-r--r-- | src/net.cpp | 145 | ||||
| -rw-r--r-- | src/net.h | 4 | ||||
| -rw-r--r-- | src/nostr.cpp | 88 | ||||
| -rw-r--r-- | src/nostr.h | 12 | ||||
| -rw-r--r-- | src/proxy.cpp | 51 | ||||
| -rw-r--r-- | src/readme.txt | 13 | ||||
| -rw-r--r-- | src/selftest.cpp | 77 | ||||
| -rw-r--r-- | src/sockcount.h | 1 | ||||
| -rw-r--r-- | src/test_btfaddr.cpp | 22 | ||||
| -rw-r--r-- | src/tor.cpp | 881 | ||||
| -rw-r--r-- | src/tor.h | 24 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 62 | ||||
| -rw-r--r-- | src/walletcmd.h | 9 | ||||
| -rw-r--r-- | tests/scripts/test_bitflash_tools.py | 7 |
31 files changed, 2417 insertions, 165 deletions
| @@ -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 @@ | |||
| 10 | ROOT := $(shell pwd) | 11 | ROOT := $(shell pwd) |
| 11 | NPROC := $(shell nproc 2>/dev/null || echo 2) | 12 | NPROC := $(shell nproc 2>/dev/null || echo 2) |
| 12 | SUDO := $(shell [ "$$(id -u)" = "0" ] && echo "" || echo "sudo") | 13 | SUDO := $(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. |
| 16 | VERSION := 1.2.18+sec | 17 | VERSION := 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 | ||
| 243 | windows-tor: windows | ||
| 244 | ./scripts/package-windows-tor.sh | ||
| 245 | |||
| 242 | # ---- Clean ---------------------------------------------------------------- | 246 | # ---- Clean ---------------------------------------------------------------- |
| 243 | 247 | ||
| 244 | clean: | 248 | clean: |
| @@ -265,6 +269,6 @@ sign-checksums: | |||
| 265 | verify-release: | 269 | verify-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 |
| @@ -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 | ||
| 27 | No premine. No ICO. 50 BTF per block, halving on schedule, 21M cap, ~2 minute blocks. | 27 | No 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 | ||
| 31 | Worth being precise, because the difference matters if you are relying on it. | 31 | Worth 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 | ||
| 39 | This is unlinkability between peers, not anonymity against a network observer. | 39 | This is unlinkability between peers, not anonymity against a network observer. |
| 40 | For Tor routing, start the node with `-tor`; see [Tor mode](docs/tor.md). | 40 | For Tor routing and direct onion peer endpoints, start the node with `-tor`; |
| 41 | see [Tor mode](docs/tor.md). | ||
| 42 | For 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`. |
| 55 | If you want managed Tor without a separate Tor install, use the | ||
| 56 | `-windows-with-tor.zip` package and start Bitflash with `-managedtor`. | ||
| 52 | 57 | ||
| 53 | Every release ships a `SHA256SUMS` covering the assets. Verifying takes a second | 58 | Every release ships a `SHA256SUMS` covering the assets. Verifying takes a second |
| 54 | and is worth doing: | 59 | and is worth doing: |
| @@ -74,6 +79,14 @@ scripts/verify-release.sh latest | |||
| 74 | See [release verification](docs/release-verification.md) for the full release | 79 | See [release verification](docs/release-verification.md) for the full release |
| 75 | audit flow and the maintainer signing step. | 80 | audit flow and the maintainer signing step. |
| 76 | 81 | ||
| 82 | Public mirrors should make the latest checksum files available both under the | ||
| 83 | versioned release directory and at the release root: | ||
| 84 | |||
| 85 | ```text | ||
| 86 | https://releases.bitflash.network/v1.2.19/SHA256SUMS | ||
| 87 | https://releases.bitflash.network/SHA256SUMS # alias to latest | ||
| 88 | ``` | ||
| 89 | |||
| 77 | Bitflash also ships a deterministic UTXO-set commitment tool: | 90 | Bitflash 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 | |||
| 113 | python3 scripts/build-explorer.py ~/.bitflash/blk0001.dat ~/.bitflash/blk0002.dat ./explorer-out | 126 | python3 scripts/build-explorer.py ~/.bitflash/blk0001.dat ~/.bitflash/blk0002.dat ./explorer-out |
| 114 | ``` | 127 | ``` |
| 115 | 128 | ||
| 129 | The generated explorer is static and self-contained: `index.html`, | ||
| 130 | `style.css`, `explorer.js`, `blocks.json`, `block/*.json`, and `logo.png`. It is | ||
| 131 | safe to host behind a strict policy such as `default-src 'none'; img-src 'self'; | ||
| 132 | style-src 'self'; script-src 'self'; connect-src 'self'; base-uri 'none'; | ||
| 133 | form-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 |
| 117 | releases — 1.2.1 fixed a bug that let anyone spend anyone's coins, and 1.2.2 | 136 | releases — 1.2.1 fixed a bug that let anyone spend anyone's coins, and 1.2.2 |
| 118 | added a per-block signature-operation cap. A node on an older build will accept | 137 | added 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. |
| 266 | The files are informational only: nodes still discover pools through Nostr and | 285 | The 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. |
| 287 | See [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: | |||
| 329 | needed — the data directory takes an exclusive lock, so a second node pointed at | 351 | needed — the data directory takes an exclusive lock, so a second node pointed at |
| 330 | the same one will refuse to start. | 352 | the 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 |
| 333 | dials through a local SOCKS5 proxy such as Tor. When it is set, the node skips | 355 | dials, and direct `.onion` peer dials through a local SOCKS5 proxy such as Tor. |
| 356 | When it is set, the node skips | ||
| 334 | plain HTTP external-IP probes instead of leaking a direct request outside the | 357 | plain HTTP external-IP probes instead of leaking a direct request outside the |
| 335 | proxy. IPv6 proxy endpoints use brackets, for example `-socks=[::1]:9050`. | 358 | proxy. 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, |
| 339 | dials through Tor, refuses direct `.onion` resolution, and keeps external-IP | 362 | and signed direct `.onion` peer dials through Tor, and keeps external-IP probes |
| 340 | probes disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. | 363 | disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. |
| 364 | |||
| 365 | `-managedtor[=PATH]` starts a Tor process for this node, writes a local `torrc`, | ||
| 366 | creates a v3 hidden service for port `8433`, routes outbound discovery through | ||
| 367 | that Tor instance, and signs the generated `.onion:8433` endpoint into the | ||
| 368 | node'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 | ||
| 370 | is 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 | ``` |
| 425 | Installs deps via pacman, produces `Bitflash-*-windows.zip`. | 455 | Installs deps via pacman, produces `Bitflash-*-windows.zip`. |
| 426 | 456 | ||
| 457 | To build the Windows package that also carries the Tor Expert Bundle: | ||
| 458 | |||
| 459 | ```bash | ||
| 460 | make windows-tor | ||
| 461 | ``` | ||
| 462 | |||
| 463 | That produces `Bitflash-*-windows-with-tor.zip`. The release script downloads | ||
| 464 | the Tor Expert Bundle from the official Tor archive, verifies its pinned SHA256, | ||
| 465 | optionally verifies the Tor Project GPG signature when `gpg` is available, and | ||
| 466 | places `tor/tor.exe` beside `Bitflash.exe` so `-managedtor` works without a | ||
| 467 | separate Tor install. For release builds, use `TOR_VERIFY_GPG=required make | ||
| 468 | windows-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 | |||
| 84 | website widget or status dashboard. Until a public price exists, a website | 84 | website widget or status dashboard. Until a public price exists, a website |
| 85 | calculator should keep price as a manual field and label fiat profit as | 85 | calculator should keep price as a manual field and label fiat profit as |
| 86 | speculative. | 86 | speculative. |
| 87 | |||
| 88 | ## Website Fields | ||
| 89 | |||
| 90 | A 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 | |||
| 99 | The 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 | |||
| 107 | Never present the fiat number as a market quote unless it comes from a real | ||
| 108 | public 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 | ||
| 29 | The website is a convenience layer, not a consensus or discovery dependency. | ||
| 30 | Nodes discover pools through Nostr descriptors and `.btf`; a missing website | ||
| 31 | must not stop mining or pool operation. | ||
| 32 | |||
| 29 | When a source is an HTTP(S) `pool_status.json`, the directory links to a | 33 | When 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, |
| 31 | block hash, coinbase txid, coinbase value, pool fee, total shares, per-address | 35 | block hash, coinbase txid, coinbase value, pool fee, total shares, per-address |
| @@ -41,3 +45,17 @@ Ranking order is intentionally simple: | |||
| 41 | 45 | ||
| 42 | An entry is considered online when the source says the pool is running and its | 46 | An entry is considered online when the source says the pool is running and its |
| 43 | status file is no more than 120 seconds old. | 47 | status file is no more than 120 seconds old. |
| 48 | |||
| 49 | ## Recommended Hostnames | ||
| 50 | |||
| 51 | Use: | ||
| 52 | |||
| 53 | ```text | ||
| 54 | pool.bitflash.network human-readable pool directory | ||
| 55 | api.bitflash.network/pools.json | ||
| 56 | status.bitflash.network relay/node health, not pool ranking | ||
| 57 | docs.bitflash.network operator setup guide | ||
| 58 | ``` | ||
| 59 | |||
| 60 | The GUI may use the API as a shortcut for humans, but it should keep the | ||
| 61 | existing 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`: | |||
| 34 | scripts/verify-release.sh latest | 34 | scripts/verify-release.sh latest |
| 35 | ``` | 35 | ``` |
| 36 | 36 | ||
| 37 | The public release host should expose latest-file aliases for convenience: | ||
| 38 | |||
| 39 | ```text | ||
| 40 | https://releases.bitflash.network/SHA256SUMS | ||
| 41 | https://releases.bitflash.network/SHA256SUMS.asc | ||
| 42 | ``` | ||
| 43 | |||
| 44 | Those aliases must point at the current versioned directory. They do not replace | ||
| 45 | signature verification; they only make the common manual path shorter. | ||
| 46 | |||
| 37 | For a specific release: | 47 | For a specific release: |
| 38 | 48 | ||
| 39 | ```bash | 49 | ```bash |
| @@ -71,6 +81,14 @@ Publish, alongside the assets, `SHA256SUMS` and `SHA256SUMS.asc`, and update | |||
| 71 | is ever rotated, announce the new fingerprint clearly and update the embedded | 81 | is ever rotated, announce the new fingerprint clearly and update the embedded |
| 72 | key in `verify-release.sh`. | 82 | key in `verify-release.sh`. |
| 73 | 83 | ||
| 84 | For Windows releases with managed Tor built in, run `make windows-tor`. It | ||
| 85 | creates `Bitflash-*-windows-with-tor.zip` by downloading the Tor Expert Bundle, | ||
| 86 | checking the pinned SHA256 in `scripts/package-windows-tor.sh`, optionally | ||
| 87 | verifying the Tor Project `.asc` signature, and placing `tor/tor.exe` next to | ||
| 88 | `Bitflash.exe`. For release builds, run `TOR_VERIFY_GPG=required make | ||
| 89 | windows-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: | |||
| 18 | 1. **Discovery (Nostr).** The service publishes a signed descriptor as a Nostr | 18 | 1. **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. | ||
| 22 | 2. **Rendezvous (TCP).** Caller and service both connect out to the same relay | 23 | 2. **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. |
| 25 | 3. **Encrypted channel (X25519 + XChaCha20-Poly1305).** Over the paired pipe the | 26 | 3. **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. | ||
| 30 | 4. **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`. | |||
| 107 | The descriptor is a JSON object, signed with the identity key over a domain | 112 | The descriptor is a JSON object, signed with the identity key over a domain |
| 108 | separated digest of its own fields. | 113 | separated digest of its own fields. |
| 109 | 114 | ||
| 115 | Legacy 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 | ||
| 121 | The signed digest is: | 128 | Onion-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 | |||
| 143 | The legacy signed digest is: | ||
| 122 | 144 | ||
| 123 | ``` | 145 | ``` |
| 124 | digest = SHA256( ".btf-descriptor-v2" | 146 | digest = 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 | ||
| 131 | The 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 |
| 132 | randomness) over `digest` by the identity key. | 154 | the rendezvous relay. The onion-capable digest is: |
| 155 | |||
| 156 | ``` | ||
| 157 | digest3 = 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 | ||
| 166 | randomness) 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 |
| 135 | fields with the right types, confirm `pubkey` equals the address being resolved, | 169 | fields with the right types, confirm `pubkey` equals the address being resolved, |
| 136 | recompute the digest, and verify the Schnorr signature. Any failure rejects the | 170 | recompute the digest, and verify the Schnorr signature. Any failure rejects the |
| 137 | descriptor. | 171 | descriptor. |
| 138 | 172 | ||
| 173 | If `onion` is present, readers accept it only when it is a normalized v3 onion | ||
| 174 | endpoint and `sig3` verifies. A bad or tampered onion field is ignored; the v2 | ||
| 175 | descriptor 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 |
| 140 | seen. A node only publishes a descriptor once it has actually registered at a | 178 | seen. 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. | 179 | has actually registered at a `meeting_node`, so it never advertises a relay it |
| 180 | cannot be reached at. A node with a direct onion endpoint may publish before | ||
| 181 | rendezvous registration using `meeting_node = "rendezvous-pending"`; Tor peers | ||
| 182 | can still dial the signed onion endpoint, and rendezvous becomes fallback once | ||
| 183 | registration 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 | |||
| 3 | The public Bitflash sites are generated from this repository. If a deployed | ||
| 4 | page and the source disagree, the deployed page is stale. | ||
| 5 | |||
| 6 | ## Canonical Navigation | ||
| 7 | |||
| 8 | Every 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 | |||
| 19 | The `code` link intentionally points at the repository, not the Forgejo | ||
| 20 | instance root. A visitor choosing "code" wants the source tree. | ||
| 21 | |||
| 22 | ## Documentation Pages To Publish | ||
| 23 | |||
| 24 | The 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 | |||
| 42 | Each release remains under its versioned directory: | ||
| 43 | |||
| 44 | ```text | ||
| 45 | https://releases.bitflash.network/v1.2.19/SHA256SUMS | ||
| 46 | https://releases.bitflash.network/v1.2.19/SHA256SUMS.asc | ||
| 47 | ``` | ||
| 48 | |||
| 49 | The release root should also alias the latest files: | ||
| 50 | |||
| 51 | ```text | ||
| 52 | https://releases.bitflash.network/SHA256SUMS | ||
| 53 | https://releases.bitflash.network/SHA256SUMS.asc | ||
| 54 | https://releases.bitflash.network/Bitflash-latest-windows.zip | ||
| 55 | https://releases.bitflash.network/Bitflash-latest-x86_64.AppImage | ||
| 56 | https://releases.bitflash.network/bitflash-node-latest-x86_64 | ||
| 57 | ``` | ||
| 58 | |||
| 59 | The aliases are convenience only. Verification must still require the pinned | ||
| 60 | release 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 | ||
| 67 | index.html | ||
| 68 | style.css | ||
| 69 | explorer.js | ||
| 70 | logo.png | ||
| 71 | blocks.json | ||
| 72 | block/<height>.json | ||
| 73 | ``` | ||
| 74 | |||
| 75 | Serve it with a strict content policy: | ||
| 76 | |||
| 77 | ```text | ||
| 78 | default-src 'none'; | ||
| 79 | img-src 'self'; | ||
| 80 | style-src 'self'; | ||
| 81 | script-src 'self'; | ||
| 82 | connect-src 'self'; | ||
| 83 | base-uri 'none'; | ||
| 84 | form-action 'none'; | ||
| 85 | frame-ancestors 'none'; | ||
| 86 | ``` | ||
| 87 | |||
| 88 | The viewer must never use `innerHTML` for chain data. A malicious transaction | ||
| 89 | must be rendered as text, not interpreted as markup. | ||
| 90 | |||
| 91 | ## Cache Policy | ||
| 92 | |||
| 93 | Recommended 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 | |||
| 105 | Recommended for all static pages: | ||
| 106 | |||
| 107 | ```text | ||
| 108 | Strict-Transport-Security: max-age=31536000; includeSubDomains | ||
| 109 | X-Content-Type-Options: nosniff | ||
| 110 | Referrer-Policy: no-referrer | ||
| 111 | Permissions-Policy: interest-cohort=(), geolocation=(), camera=(), microphone=() | ||
| 112 | ``` | ||
| 113 | |||
| 114 | Forgejo has its own application headers and should not blindly inherit the | ||
| 115 | static-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 | |||
| 25 | bitflash -tor=[::1]:9050 | 25 | bitflash -tor=[::1]:9050 |
| 26 | ``` | 26 | ``` |
| 27 | 27 | ||
| 28 | For a SOCKS5 proxy that is not Tor, use `-socks` directly: | ||
| 29 | |||
| 30 | ```bash | ||
| 31 | bitflash -socks=127.0.0.1:9050 | ||
| 32 | bitflash -socks=[::1]:9050 | ||
| 33 | ``` | ||
| 34 | |||
| 35 | If both `-tor` and `-socks` are present, Tor mode wins and the node prints a | ||
| 36 | warning. That keeps a command line with `-tor` from silently becoming a custom | ||
| 37 | proxy route. | ||
| 38 | |||
| 39 | ## Managed Tor mode | ||
| 40 | |||
| 41 | `-managedtor` starts a Tor process for this node, writes a local `torrc`, creates | ||
| 42 | a v3 hidden service for the normal Bitflash P2P listener, routes outbound | ||
| 43 | discovery through that Tor instance, and signs the generated `.onion:8433` | ||
| 44 | endpoint into the node's `.btf` descriptor. | ||
| 45 | |||
| 46 | ```bash | ||
| 47 | bitflash -managedtor | ||
| 48 | ``` | ||
| 49 | |||
| 50 | Without a value, Bitflash looks for Tor in this order: | ||
| 51 | |||
| 52 | 1. `tor/tor.exe` or `tor/tor` beside the Bitflash binary; | ||
| 53 | 2. `tor.exe` or `tor` beside the Bitflash binary; | ||
| 54 | 3. `tor.exe` or `tor` on `PATH`. | ||
| 55 | |||
| 56 | Windows releases can also be built as `Bitflash-*-windows-with-tor.zip`. That | ||
| 57 | package includes the official Tor Expert Bundle under `tor/`, so the first path | ||
| 58 | above exists immediately after extraction and `bitflash -managedtor` needs no | ||
| 59 | separate Tor install. | ||
| 60 | |||
| 61 | The bundled-Tor package is created by: | ||
| 62 | |||
| 63 | ```bash | ||
| 64 | make windows-tor | ||
| 65 | ``` | ||
| 66 | |||
| 67 | The packaging script verifies a pinned SHA256 for the downloaded Tor Expert | ||
| 68 | Bundle before copying it into the release directory. When `gpg` is available it | ||
| 69 | also verifies the matching `.asc` signature against the pinned Tor Browser | ||
| 70 | Developers signing-key fingerprint. Release builds should use | ||
| 71 | `TOR_VERIFY_GPG=required make windows-tor` to make that second check mandatory. | ||
| 72 | The Tor binary is bundled; the runtime data directory, hidden-service key, and | ||
| 73 | control cookie are still created under the user's Bitflash data directory. | ||
| 74 | |||
| 75 | To point at a specific Tor executable: | ||
| 76 | |||
| 77 | ```bash | ||
| 78 | bitflash -managedtor=/opt/tor/bin/tor | ||
| 79 | bitflash -managedtor=C:\Tor Browser\Browser\TorBrowser\Tor\tor.exe | ||
| 80 | ``` | ||
| 81 | |||
| 82 | Managed Tor writes its state under the Bitflash data directory: | ||
| 83 | |||
| 84 | ```text | ||
| 85 | managed-tor/ | ||
| 86 | torrc | ||
| 87 | data/ | ||
| 88 | onion-service/ | ||
| 89 | ``` | ||
| 90 | |||
| 91 | The private key for the onion address lives in `managed-tor/onion-service/`. | ||
| 92 | Back up that directory if you want the same onion address after moving the node. | ||
| 93 | Delete it only if you intentionally want a fresh onion endpoint. | ||
| 94 | |||
| 95 | `-managedtor` takes precedence over `-tor` and `-socks`, because it must choose | ||
| 96 | the local SOCKS port it starts. Diagnostics report the current managed Tor | ||
| 97 | state, including the SOCKS port and onion endpoint once Tor has generated it. | ||
| 98 | |||
| 28 | When Tor mode is enabled: | 99 | When 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 | ||
| 110 | Tor 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 | ||
| 112 | relay still sees the TCP client that connected to it; with Tor mode that client | ||
| 113 | is the Tor exit or onion circuit endpoint instead of the user's direct IP. | ||
| 114 | |||
| 115 | ## What to test | ||
| 116 | |||
| 117 | Use the public status page only as a rough health check. To prove the local node | ||
| 118 | is using Tor/SOCKS, test from the node machine: | ||
| 119 | |||
| 120 | ```bash | ||
| 121 | bitflash -tor -debug | ||
| 122 | ``` | ||
| 123 | |||
| 124 | Then confirm in `debug.log` that Nostr and rendezvous dials report the proxy | ||
| 125 | path. A DNS leak test should show no local resolver lookup for relay hostnames: | ||
| 126 | the 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 | ||
| 39 | An operator can expose a Bitflash rendezvous relay as a Tor hidden service. The | 130 | An 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 | ||
| 74 | The legacy P2P listener still binds to port `8433`. You can expose it through | 165 | Managed Tor is the preferred path for ordinary nodes. Operators can still expose |
| 75 | Tor manually: | 166 | the P2P listener through a separately managed Tor service: |
| 76 | 167 | ||
| 77 | ```text | 168 | ```text |
| 78 | HiddenServiceDir /var/lib/tor/bitflash-node/ | 169 | HiddenServiceDir /var/lib/tor/bitflash-node/ |
| 79 | HiddenServicePort 8433 127.0.0.1:8433 | 170 | HiddenServicePort 8433 127.0.0.1:8433 |
| 80 | ``` | 171 | ``` |
| 81 | 172 | ||
| 82 | Current automatic peer discovery is `.btf`/rendezvous based, not onion-address | 173 | After restarting Tor, read the generated onion name: |
| 83 | gossip. Treat direct full-node onion access as an operator/admin tool unless a | 174 | |
| 84 | future release adds first-class onion peer advertisements. | 175 | ```bash |
| 176 | sudo cat /var/lib/tor/bitflash-node/hostname | ||
| 177 | ``` | ||
| 178 | |||
| 179 | Then start Bitflash with Tor outbound enabled and advertise that hidden service: | ||
| 180 | |||
| 181 | ```bash | ||
| 182 | bitflash -tor \ | ||
| 183 | -onionservice=exampleexampleexampleexampleexampleexampleexampleexample.onion:8433 | ||
| 184 | ``` | ||
| 185 | |||
| 186 | That onion endpoint is signed into this node's self-certifying `.btf` | ||
| 187 | descriptor. New peers that resolve the descriptor and also run with `-tor` try | ||
| 188 | the `.onion:8433` P2P connection first. If it fails, they fall back to the | ||
| 189 | ordinary encrypted rendezvous path. Older nodes ignore the onion field and still | ||
| 190 | use the rendezvous relay because the descriptor keeps the legacy signature too. | ||
| 191 | |||
| 192 | If the node has `-onionservice` but has not registered at any rendezvous relay | ||
| 193 | yet, it can still publish an onion-capable descriptor with | ||
| 194 | `meeting_node = rendezvous-pending`. Tor-capable peers can dial the hidden | ||
| 195 | service directly from that descriptor. Rendezvous becomes a fallback, not a | ||
| 196 | precondition for being discoverable. | ||
| 197 | |||
| 198 | This means rendezvous relays stop being the only way to reach a node that has | ||
| 199 | published a hidden service. They remain useful bootstrap/fallback infrastructure, | ||
| 200 | but an already-discovered onion-capable peer can be reached directly over Tor. | ||
| 201 | |||
| 202 | Tor owns the private key under `HiddenServiceDir`; back it up if you want the | ||
| 203 | onion name to stay stable. | ||
| 204 | |||
| 205 | ## Release smoke test | ||
| 206 | |||
| 207 | After building a Windows binary with managed Tor support, run the two-node smoke | ||
| 208 | test before cutting a release: | ||
| 209 | |||
| 210 | ```powershell | ||
| 211 | powershell -ExecutionPolicy Bypass -File scripts\smoke-managed-tor-onion.ps1 ` | ||
| 212 | -Bitflash .\src\bitflash.exe ` | ||
| 213 | -Tor C:\path\to\tor.exe | ||
| 214 | ``` | ||
| 215 | |||
| 216 | The script creates two temporary datadirs, starts node A with `-managedtor`, | ||
| 217 | waits for it to publish its `.btf` identity and generated `.onion` endpoint, | ||
| 218 | then starts node B with its own `-managedtor`, `-oniononly`, and | ||
| 219 | `-connectbtf=<node-a>`. It passes only when node B logs that it connected to | ||
| 220 | node A `via direct onion`; rendezvous fallback cannot satisfy the test. | ||
| 221 | |||
| 222 | By default the script waits 120 seconds after node A's descriptor self-resolves | ||
| 223 | before it starts node B. Tor can create the local `hostname` before the hidden | ||
| 224 | service descriptor is visible enough for client circuits, and early dials often | ||
| 225 | fail with SOCKS5 reply `0x01`. Override this with `-OnionWarmupSec N` when | ||
| 226 | testing a slow or already-warmed Tor environment. | ||
| 227 | |||
| 228 | This test still uses Nostr for descriptor discovery, because that is how nodes | ||
| 229 | learn each other's signed `.btf` descriptors. It does not require a Bitflash | ||
| 230 | rendezvous relay to carry the P2P connection: once the descriptor includes the | ||
| 231 | signed 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 @@ | |||
| 4 | import argparse | 4 | import argparse |
| 5 | import hashlib | 5 | import hashlib |
| 6 | import json | 6 | import json |
| 7 | import shutil | ||
| 7 | import sys | 8 | import sys |
| 8 | from pathlib import Path | 9 | from 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 | |||
| 212 | STYLE_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;} | ||
| 223 | html,body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.55 var(--mono);-webkit-text-size-adjust:100%;} | ||
| 224 | a{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;} | ||
| 237 | h1{font-size:1rem;margin:0 0 .3rem;letter-spacing:.02em;} | ||
| 238 | h2{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;} | ||
| 241 | input{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;} | ||
| 242 | input:focus{outline:none;border-color:var(--accent);} | ||
| 243 | .scroll{overflow-x:auto;} | ||
| 244 | table{width:100%;border-collapse:collapse;margin-top:.8rem;font-size:.86rem;} | ||
| 245 | th,td{text-align:left;padding:.5rem .6rem;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums;} | ||
| 246 | th{font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;color:var(--dim);font-weight:normal;} | ||
| 247 | tr.blk{cursor:pointer;} | ||
| 248 | tr.blk:hover td{background:var(--bg-2);} | ||
| 249 | code{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;} | ||
| 257 | button.link{background:none;border:none;color:var(--accent);cursor:pointer;font:inherit;padding:0;text-decoration:underline;} | ||
| 258 | """ | ||
| 259 | |||
| 260 | |||
| 261 | EXPLORER_JS = r""" | ||
| 219 | var SUM = [], SHOWN = 200; | 262 | var SUM = [], SHOWN = 200; |
| 220 | function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; } | 263 | function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; } |
| 221 | function ts(t){ return new Date(t*1000).toISOString().replace('T',' ').replace('.000Z',' UTC'); } | 264 | function 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 | } |
| 319 | document.getElementById('home').onclick=function(){ | 362 | document.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 | }; |
| 324 | document.getElementById('q').oninput=function(){ renderList(this.value); }; | 367 | document.getElementById('q').oninput=function(){ renderList(this.value); }; |
| 325 | fetch('blocks.json').then(function(r){return r.json();}).then(function(d){ | 368 | fetch('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 | ||
| 11 | Creates SHA256SUMS for release assets in the current directory: | 11 | Creates 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 | |||
| 44 | assets=() | 45 | assets=() |
| 45 | for pattern in \ | 46 | for 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" |
| 49 | do | 51 | do |
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 | ||
| 2 | set -euo pipefail | ||
| 3 | |||
| 4 | ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||
| 5 | VERSION="${VERSION:-$(sed -n 's/^VERSION[[:space:]]*:= *//p' "$ROOT/Makefile" | head -1)}" | ||
| 6 | |||
| 7 | TOR_VERSION="${TOR_VERSION:-15.0.19}" | ||
| 8 | TOR_ARCHIVE_NAME="tor-expert-bundle-windows-x86_64-${TOR_VERSION}.tar.gz" | ||
| 9 | TOR_URL="${TOR_URL:-https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/${TOR_ARCHIVE_NAME}}" | ||
| 10 | TOR_SHA256="${TOR_SHA256:-6AC067402C7B4A3DC37887ED3754B3914B67FDC220C966190683E9CCF91ABF0F}" | ||
| 11 | TOR_ASC_URL="${TOR_ASC_URL:-${TOR_URL}.asc}" | ||
| 12 | TOR_SIGNING_KEY_FINGERPRINT="${TOR_SIGNING_KEY_FINGERPRINT:-EF6E286DDA85EA2A4BA7DE684E2C6E8793298290}" | ||
| 13 | TOR_VERIFY_GPG="${TOR_VERIFY_GPG:-auto}" | ||
| 14 | |||
| 15 | BUNDLE_DIR="${BUNDLE_DIR:-$ROOT/Bitflash-${VERSION}-windows}" | ||
| 16 | OUT_ZIP="${OUT_ZIP:-$ROOT/Bitflash-${VERSION}-windows-with-tor.zip}" | ||
| 17 | CACHE_DIR="${CACHE_DIR:-$ROOT/.cache}" | ||
| 18 | ARCHIVE="${TOR_ARCHIVE:-$CACHE_DIR/$TOR_ARCHIVE_NAME}" | ||
| 19 | ASC_FILE="${TOR_ASC:-$ARCHIVE.asc}" | ||
| 20 | |||
| 21 | usage() { | ||
| 22 | cat <<EOF | ||
| 23 | Usage: scripts/package-windows-tor.sh | ||
| 24 | |||
| 25 | Creates Bitflash-${VERSION}-windows-with-tor.zip from an existing | ||
| 26 | Bitflash-${VERSION}-windows/ release directory. | ||
| 27 | |||
| 28 | Environment 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 | ||
| 39 | EOF | ||
| 40 | } | ||
| 41 | |||
| 42 | if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then | ||
| 43 | usage | ||
| 44 | exit 0 | ||
| 45 | fi | ||
| 46 | |||
| 47 | need() { | ||
| 48 | command -v "$1" >/dev/null 2>&1 || { | ||
| 49 | echo "missing required command: $1" >&2 | ||
| 50 | exit 1 | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | need sha256sum | ||
| 55 | need tar | ||
| 56 | need zip | ||
| 57 | |||
| 58 | download() { | ||
| 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 | |||
| 71 | if [ ! -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 | ||
| 75 | fi | ||
| 76 | if [ ! -f "$BUNDLE_DIR/Bitflash.exe" ]; then | ||
| 77 | echo "missing Bitflash.exe in $BUNDLE_DIR" >&2 | ||
| 78 | exit 1 | ||
| 79 | fi | ||
| 80 | BUNDLE_PARENT="$(cd "$(dirname "$BUNDLE_DIR")" && pwd)" | ||
| 81 | BUNDLE_NAME="$(basename "$BUNDLE_DIR")" | ||
| 82 | OUT_PARENT="$(mkdir -p "$(dirname "$OUT_ZIP")" && cd "$(dirname "$OUT_ZIP")" && pwd)" | ||
| 83 | OUT_NAME="$(basename "$OUT_ZIP")" | ||
| 84 | OUT_ZIP_ABS="$OUT_PARENT/$OUT_NAME" | ||
| 85 | |||
| 86 | mkdir -p "$CACHE_DIR" | ||
| 87 | if [ ! -f "$ARCHIVE" ]; then | ||
| 88 | download "$TOR_URL" "$ARCHIVE" | ||
| 89 | fi | ||
| 90 | |||
| 91 | got="$(sha256sum "$ARCHIVE" | awk '{print tolower($1)}')" | ||
| 92 | want="$(printf '%s' "$TOR_SHA256" | tr 'A-F' 'a-f')" | ||
| 93 | if [ "$got" != "$want" ]; then | ||
| 94 | echo "Tor archive SHA256 mismatch" >&2 | ||
| 95 | echo " expected: $want" >&2 | ||
| 96 | echo " got: $got" >&2 | ||
| 97 | exit 1 | ||
| 98 | fi | ||
| 99 | |||
| 100 | tmp="$(mktemp -d "${TMPDIR:-/tmp}/bitflash-tor-package.XXXXXX")" | ||
| 101 | trap 'rm -rf "$tmp"' EXIT | ||
| 102 | |||
| 103 | case "$TOR_VERIFY_GPG" in | ||
| 104 | auto|required|off) ;; | ||
| 105 | *) echo "TOR_VERIFY_GPG must be auto, required, or off" >&2; exit 2 ;; | ||
| 106 | esac | ||
| 107 | |||
| 108 | if [ "$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 | ||
| 143 | fi | ||
| 144 | |||
| 145 | mkdir -p "$tmp/extract" | ||
| 146 | tar -xzf "$ARCHIVE" -C "$tmp/extract" | ||
| 147 | |||
| 148 | if [ ! -f "$tmp/extract/tor/tor.exe" ]; then | ||
| 149 | echo "Tor archive did not contain tor/tor.exe" >&2 | ||
| 150 | exit 1 | ||
| 151 | fi | ||
| 152 | |||
| 153 | rm -rf "$BUNDLE_DIR/tor" | ||
| 154 | mkdir -p "$BUNDLE_DIR/tor" | ||
| 155 | cp -R "$tmp/extract/tor/." "$BUNDLE_DIR/tor/" | ||
| 156 | if [ -d "$tmp/extract/docs" ]; then | ||
| 157 | mkdir -p "$BUNDLE_DIR/tor/docs" | ||
| 158 | cp -R "$tmp/extract/docs/." "$BUNDLE_DIR/tor/docs/" | ||
| 159 | fi | ||
| 160 | if [ -d "$tmp/extract/data" ]; then | ||
| 161 | mkdir -p "$BUNDLE_DIR/tor/data-defaults" | ||
| 162 | cp -R "$tmp/extract/data/." "$BUNDLE_DIR/tor/data-defaults/" | ||
| 163 | fi | ||
| 164 | |||
| 165 | cat > "$BUNDLE_DIR/TOR-EXPERT-BUNDLE.txt" <<EOF | ||
| 166 | Bitflash managed Tor bundle | ||
| 167 | |||
| 168 | This Windows package includes the Tor Expert Bundle so -managedtor can start | ||
| 169 | Tor without requiring a separate Tor install. | ||
| 170 | |||
| 171 | Tor Expert Bundle: ${TOR_VERSION} | ||
| 172 | Archive URL: ${TOR_URL} | ||
| 173 | Archive SHA256: ${TOR_SHA256} | ||
| 174 | |||
| 175 | Bitflash uses tor/tor.exe from this package. Runtime state, hidden-service keys, | ||
| 176 | and control authentication cookies are written under the user's Bitflash data | ||
| 177 | directory, not inside this release directory. | ||
| 178 | EOF | ||
| 179 | |||
| 180 | rm -f "$OUT_ZIP_ABS" | ||
| 181 | ( | ||
| 182 | cd "$BUNDLE_PARENT" | ||
| 183 | zip -qr "$OUT_ZIP_ABS" "$BUNDLE_NAME" | ||
| 184 | ) | ||
| 185 | |||
| 186 | echo "Bundled Tor Expert Bundle ${TOR_VERSION}" | ||
| 187 | echo "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 @@ | |||
| 1 | param( | ||
| 2 | [string]$Bitflash, | ||
| 3 | [string]$Tor, | ||
| 4 | [string]$WorkDir, | ||
| 5 | [int]$TimeoutSec = 600, | ||
| 6 | [int]$OnionWarmupSec = 120 | ||
| 7 | ) | ||
| 8 | |||
| 9 | $ErrorActionPreference = "Stop" | ||
| 10 | |||
| 11 | function Resolve-RepoRoot { | ||
| 12 | $scriptDir = Split-Path -Parent $PSCommandPath | ||
| 13 | return (Resolve-Path (Join-Path $scriptDir "..")).Path | ||
| 14 | } | ||
| 15 | |||
| 16 | function 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 | |||
| 27 | function 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 | |||
| 50 | function 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 | ||
| 80 | if ([string]::IsNullOrWhiteSpace($Bitflash)) { | ||
| 81 | $Bitflash = Join-Path $repo "src\bitflash.exe" | ||
| 82 | } | ||
| 83 | if ([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 | } | ||
| 95 | if ([string]::IsNullOrWhiteSpace($WorkDir)) { | ||
| 96 | $WorkDir = Join-Path $env:TEMP ("bitflash-managed-tor-two-node-" + (Get-Date -Format "yyyyMMddHHmmss")) | ||
| 97 | } | ||
| 98 | |||
| 99 | if (!(Test-Path $Bitflash)) { | ||
| 100 | throw "Bitflash binary not found: $Bitflash" | ||
| 101 | } | ||
| 102 | if (!(Test-Path $Tor)) { | ||
| 103 | throw "Tor binary not found. Pass -Tor or build the Windows-with-Tor package." | ||
| 104 | } | ||
| 105 | |||
| 106 | New-Item -ItemType Directory -Force -Path $WorkDir | Out-Null | ||
| 107 | $dataA = Join-Path $WorkDir "node-a" | ||
| 108 | $dataB = Join-Path $WorkDir "node-b" | ||
| 109 | New-Item -ItemType Directory -Force -Path $dataA | Out-Null | ||
| 110 | New-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 | |||
| 122 | try { | ||
| 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 | } | ||
| 168 | finally { | ||
| 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 \ | |||
| 53 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 53 | HEADERS = 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 | ||
| 58 | IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ | 58 | IMGUI_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 \ | |||
| 61 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 61 | NODE_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 | ||
| 66 | GUI_OBJS = obj/gui.o obj/main_gui.o | 66 | GUI_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 | ||
| 23 | using json = nlohmann::json; | 25 | using 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 | ||
| 171 | bool 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). |
| 170 | static void DescriptorDigest(const unsigned char pubkey[32], const std::string& enc, | 209 | static 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 | ||
| 226 | static 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 | |||
| 187 | std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], | 244 | std::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". | ||
| 61 | bool 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. |
| 61 | std::string SignDescriptor(void* ctx, const unsigned char seckey[32], | 66 | std::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 \ | |||
| 37 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 37 | NODE_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 | ||
| 42 | GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o | 42 | GUI_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 | |||
| 51 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 51 | HEADERS = 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 | ||
| 56 | all: bitflash.exe | 56 | all: 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 | ||
| 24 | void ThreadReconnectCachedBtfPeers(void* parg); | 27 | void ThreadReconnectCachedBtfPeers(void* parg); |
| 25 | void ThreadMessageHandler2(void* parg); | 28 | void ThreadMessageHandler2(void* parg); |
| @@ -62,6 +65,8 @@ deque<pair<int64, CInv> > vRelayExpiration; | |||
| 62 | CCriticalSection cs_mapRelay; | 65 | CCriticalSection cs_mapRelay; |
| 63 | map<CInv, int64> mapAlreadyAskedFor; | 66 | map<CInv, int64> mapAlreadyAskedFor; |
| 64 | string strBtfConnect; // .btf peer to keep connected to (from /connectbtf) | 67 | string strBtfConnect; // .btf peer to keep connected to (from /connectbtf) |
| 68 | bool fBtfOnionOnly = false; // require direct onion transport for .btf peers | ||
| 69 | static const int BTF_ONION_CONNECT_TIMEOUT_SECS = 120; | ||
| 65 | 70 | ||
| 66 | int nPeersWatched = 0; | 71 | int nPeersWatched = 0; |
| 67 | int64 nBlocksReceived = 0; | 72 | int64 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 | ||
| 176 | static string SockAccountingText() | 181 | static 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. |
| 626 | static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting, | 657 | static 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 | ||
| 924 | static 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 | |||
| 885 | static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32], | 945 | static 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. |
| 973 | CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, | 1074 | CNode* 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(); |
| @@ -26,10 +26,12 @@ enum | |||
| 26 | bool GetMyExternalIP(unsigned int& ipRet); | 26 | bool GetMyExternalIP(unsigned int& ipRet); |
| 27 | CNode* ConnectNodeBtf(const string& strBtfAddr); | 27 | CNode* ConnectNodeBtf(const string& strBtfAddr); |
| 28 | CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, | 28 | CNode* 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()); | ||
| 30 | void ThreadBtfAccept(void* parg); | 31 | void ThreadBtfAccept(void* parg); |
| 31 | void ThreadBtfConnect(void* parg); | 32 | void ThreadBtfConnect(void* parg); |
| 32 | extern string strBtfConnect; | 33 | extern string strBtfConnect; |
| 34 | extern 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. |
| 35 | extern std::vector<std::pair<std::string, std::string> > vBtfExtraSeeds; | 37 | extern 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 = { | |||
| 132 | bool fBtfRelayPinned = false; | 132 | bool fBtfRelayPinned = false; |
| 133 | string strBtfActiveRelay; | 133 | string strBtfActiveRelay; |
| 134 | static CCriticalSection cs_activeRelay; | 134 | static CCriticalSection cs_activeRelay; |
| 135 | static string strBtfOnionEndpoint; | ||
| 136 | static 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 | ||
| 158 | bool 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 | |||
| 171 | string 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). |
| 157 | string strBtfAnnounceRelay; | 179 | string 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. |
| 1072 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | 1104 | static 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. |
| 1128 | bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, | 1165 | bool 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). |
| 41 | void BtfSetActiveRelay(const std::string& relay); | 41 | void 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. | ||
| 47 | bool BtfSetLocalOnionEndpoint(const std::string& endpoint, std::string& errOut); | ||
| 48 | std::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. |
| 45 | extern std::string strBtfAnnounceRelay; | 52 | extern 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. |
| 104 | bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, | 111 | bool 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. |
| 108 | struct BtfResolvedPeer | 116 | struct 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; | |||
| 13 | static std::string g_socks5Host; | 13 | static std::string g_socks5Host; |
| 14 | static unsigned short g_socks5Port = 0; | 14 | static unsigned short g_socks5Port = 0; |
| 15 | static const char* DEFAULT_TOR_SOCKS5_PROXY = "127.0.0.1:9050"; | 15 | static const char* DEFAULT_TOR_SOCKS5_PROXY = "127.0.0.1:9050"; |
| 16 | static const int TOR_CONNECT_TIMEOUT_SECS = 120; | ||
| 16 | 17 | ||
| 17 | static void SetSocketTimeout(SOCKET s, int nTimeoutSecs) | 18 | static 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 | ||
| 344 | SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort, | 384 | SOCKET 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 | ||
| 77 | Roadmap | 84 | Roadmap |
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 | ||
| 15 | extern int RunPoolStratumSelfTest(); | 17 | extern 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 | ||
| 1595 | static 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 | |||
| 1557 | int RunSelfTest(const std::string& name) | 1630 | int 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 | |||
| 16 | static CCriticalSection cs_managedTor; | ||
| 17 | static bool g_fManagedTor = false; | ||
| 18 | static bool g_fManagedTorStopping = false; | ||
| 19 | static unsigned short g_managedTorSocksPort = 0; | ||
| 20 | static unsigned short g_managedTorControlPort = 0; | ||
| 21 | static unsigned short g_managedTorP2PPort = 0; | ||
| 22 | static std::string g_managedTorPath; | ||
| 23 | static std::string g_managedTorDataDir; | ||
| 24 | static std::string g_managedTorHiddenServiceDir; | ||
| 25 | static std::string g_managedTorOnion; | ||
| 26 | static std::string g_managedTorStatus; | ||
| 27 | #ifdef _WIN32 | ||
| 28 | static PROCESS_INFORMATION g_managedTorProcess; | ||
| 29 | #else | ||
| 30 | static pid_t g_managedTorPid = -1; | ||
| 31 | #endif | ||
| 32 | |||
| 33 | static 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 | |||
| 43 | static 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 | |||
| 51 | static 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 | |||
| 64 | static 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 | |||
| 77 | static 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 | |||
| 127 | static 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 | |||
| 166 | static 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 | |||
| 208 | static 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 | |||
| 223 | static 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 | |||
| 238 | static 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 | |||
| 255 | static 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 | |||
| 293 | static void CloseLoopback(SOCKET s) | ||
| 294 | { | ||
| 295 | if (s != INVALID_SOCKET) | ||
| 296 | closesocket(s); | ||
| 297 | #ifdef _WIN32 | ||
| 298 | WSACleanup(); | ||
| 299 | #endif | ||
| 300 | } | ||
| 301 | |||
| 302 | static 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 | |||
| 316 | static 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 | |||
| 360 | done: | ||
| 361 | CloseLoopback(s); | ||
| 362 | return ok; | ||
| 363 | } | ||
| 364 | |||
| 365 | static 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 | |||
| 385 | static 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; | ||
| 427 | done: | ||
| 428 | if (s != INVALID_SOCKET) | ||
| 429 | closesocket(s); | ||
| 430 | #ifdef _WIN32 | ||
| 431 | WSACleanup(); | ||
| 432 | #endif | ||
| 433 | return ok; | ||
| 434 | } | ||
| 435 | |||
| 436 | static 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 | |||
| 462 | static 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 | |||
| 472 | static 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 | |||
| 503 | bool 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 | |||
| 518 | std::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 | |||
| 540 | static 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 | |||
| 575 | static 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 | |||
| 620 | static 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 | |||
| 634 | static 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 | |||
| 641 | static 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 | |||
| 669 | static 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 | |||
| 711 | bool 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 | |||
| 801 | void 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 | |||
| 865 | bool BtfManagedTorEnabled() | ||
| 866 | { | ||
| 867 | CRITICAL_BLOCK(cs_managedTor) | ||
| 868 | return g_fManagedTor; | ||
| 869 | return false; | ||
| 870 | } | ||
| 871 | |||
| 872 | std::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 | |||
| 12 | bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut); | ||
| 13 | void BtfStopManagedTor(); | ||
| 14 | bool BtfManagedTorEnabled(); | ||
| 15 | std::string BtfManagedTorStatus(); | ||
| 16 | bool BtfBundledTorPath(std::string& torPathOut); | ||
| 17 | |||
| 18 | std::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 | ||
| 57 | unsigned 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 | |||
| 57 | int CmdNewPhrase() | 88 | int 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. | ||
| 50 | unsigned 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 | ||