upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-08-26 22:41:53 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-26 22:41:53 +0000
commitbbe04dc8c96b4c4766d2771d3438233bf82687c0 (patch)
tree62e5a1febfb8e9bc38d133ac7981e9ef93918484 /src
parent991853fa55a749d28d6f9e507ce72b289df92785 (diff)
parent9e39a50d1f110020b0841d31e4b931aa877cc448 (diff)
Merge upstream v1.2.19 into the fork
Merges upstream tag v1.2.19 (9e39a50) into merge/upstream-1.2.18 (991853f). Unlike the 1.2.15->1.2.18 replay, bc3de30 is a true ancestor of our branch via a9b4f89, so this is an ordinary three-way merge with merge-base bc3de30. Delta: 36 files, +3265/-163, the Tor onion transport (managed Tor, signed direct onion endpoints, onion-only dial mode) and the nHDNext restore fix. src/main.cpp, main.h, script.cpp/h and db.cpp/h are byte-identical between the two tags, and the poll() socket loop carries no diff hunks, so the merge is consensus-neutral and the soak-tested socket paths are unchanged. relay/ is untouched; the payout ledger (rpc.cpp) is untouched. 7 conflict blocks in 6 files, resolved as follows. Makefile -- VERSION. Ours: stays 1.2.19+sec with the naming comment updated, so artifacts from this tree cannot be mistaken for the upstream release. src/Makefile, src/makefile.mingw -- HEADERS list. Ours (no market.h) plus their tor.h. NODE_OBJS auto-merged correctly: no market.o, tor.o present. src/selftest.cpp -- known-tests list. Union: payout-ledger (ours) and managed-tor (theirs). src/main_gui.cpp -- one block. Our /dumpaddresses kept; their BtfStopManagedTor() on the StartNode failure path taken. The /rvrelay pin, /noannounce and the marketplace removal auto-merged and were verified intact. src/nostr.cpp -- two blocks in ResolveDescriptor. Signature takes both out-params (our fExchangeOk, their rawOut). The body keeps our drain-to-EOSE read -- returning early poisons the pooled socket for the next caller -- and adds their rawOut capture. Our TLS 1.2 floor (SSL_CTX_set_min_proto_version) is untouched by this merge; upstream 1.2.19 still has no floor of its own. Upstream's release commit also added four build logs and a compiled Python cache (build-1219.log, build-17.log, build-appimage.log, build-node.log, scripts/__pycache__/bitflash_chain.cpython-313.pyc). Removed here; they are build output, not source.
Diffstat (limited to 'src')
-rw-r--r--src/Makefile5
-rw-r--r--src/btfaddr.cpp102
-rw-r--r--src/btfaddr.h8
-rw-r--r--src/main_gui.cpp96
-rw-r--r--src/makefile.mingw5
-rw-r--r--src/net.cpp145
-rw-r--r--src/net.h4
-rw-r--r--src/nostr.cpp88
-rw-r--r--src/nostr.h12
-rw-r--r--src/proxy.cpp51
-rw-r--r--src/readme.txt13
-rw-r--r--src/selftest.cpp77
-rw-r--r--src/sockcount.h1
-rw-r--r--src/test_btfaddr.cpp22
-rw-r--r--src/tor.cpp881
-rw-r--r--src/tor.h24
-rw-r--r--src/walletcmd.cpp62
-rw-r--r--src/walletcmd.h9
18 files changed, 1518 insertions, 87 deletions
diff --git a/src/Makefile b/src/Makefile
index 89ba559..b068376 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -53,7 +53,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \
53HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 53HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
54 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 54 key.h bignum.h script.h db.h base58.h net.h nostr.h \
55 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ 55 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \
56 selftest.h walletcmd.h compat.h sockcount.h proxy.h font_roboto.h 56 selftest.h walletcmd.h compat.h sockcount.h proxy.h tor.h font_roboto.h
57 57
58IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ 58IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
59 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o 59 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o
@@ -61,7 +61,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
61NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 61NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
62 obj/sha.o obj/nostr.o obj/randomx_pow.o \ 62 obj/sha.o obj/nostr.o obj/randomx_pow.o \
63 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 63 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
64 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o 64 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o
65 65
66GUI_OBJS = obj/gui.o obj/main_gui.o 66GUI_OBJS = obj/gui.o obj/main_gui.o
67 67
@@ -195,5 +195,6 @@ tests: test_bip32 bitflash-node
195 ./bitflash-node -selftest=payout-ledger 195 ./bitflash-node -selftest=payout-ledger
196 ./bitflash-node -selftest=parse-money 196 ./bitflash-node -selftest=parse-money
197 ./bitflash-node -selftest=socks5-proxy 197 ./bitflash-node -selftest=socks5-proxy
198 ./bitflash-node -selftest=managed-tor
198 199
199.PHONY: tests fuzz-net-message-smoke fuzz-script-smoke 200.PHONY: tests fuzz-net-message-smoke fuzz-script-smoke
diff --git a/src/btfaddr.cpp b/src/btfaddr.cpp
index 6edcc91..ce2ac86 100644
--- a/src/btfaddr.cpp
+++ b/src/btfaddr.cpp
@@ -19,6 +19,8 @@
19#include <cstring> 19#include <cstring>
20#include <algorithm> 20#include <algorithm>
21#include <cctype> 21#include <cctype>
22#include <cerrno>
23#include <cstdlib>
22 24
23using json = nlohmann::json; 25using json = nlohmann::json;
24 26
@@ -166,10 +168,47 @@ static bool HexDecode(const std::string& s, unsigned char* out, size_t n)
166 return true; 168 return true;
167} 169}
168 170
171bool NormalizeOnionEndpoint(const std::string& in, std::string& out)
172{
173 out.clear();
174 std::string s = ToLowerTrim(in);
175 size_t colon = s.rfind(':');
176 if (colon == std::string::npos || colon == 0 || colon + 1 >= s.size())
177 return false;
178
179 std::string host = s.substr(0, colon);
180 std::string port = s.substr(colon + 1);
181 while (!host.empty() && host.back() == '.') host.pop_back();
182 static const char* suffix = ".onion";
183 const size_t suffixLen = strlen(suffix);
184 if (host.size() != 56 + suffixLen)
185 return false;
186 if (host.compare(host.size() - suffixLen, suffixLen, suffix) != 0)
187 return false;
188 for (size_t i = 0; i < 56; i++)
189 {
190 char c = host[i];
191 if (!((c >= 'a' && c <= 'z') || (c >= '2' && c <= '7')))
192 return false;
193 }
194 for (size_t i = 0; i < port.size(); i++)
195 if (!isdigit((unsigned char)port[i]))
196 return false;
197
198 errno = 0;
199 char* end = NULL;
200 long nPort = strtol(port.c_str(), &end, 10);
201 if (errno == ERANGE || end == NULL || *end != '\0' || nPort <= 0 || nPort > 65535)
202 return false;
203
204 out = host + ":" + port;
205 return true;
206}
207
169// Digest signed over the descriptor fields (domain-separated). 208// Digest signed over the descriptor fields (domain-separated).
170static void DescriptorDigest(const unsigned char pubkey[32], const std::string& enc, 209static void DescriptorDigestV2(const unsigned char pubkey[32], const std::string& enc,
171 const std::string& meeting_node, uint64_t created, 210 const std::string& meeting_node, uint64_t created,
172 unsigned char out[32]) 211 unsigned char out[32])
173{ 212{
174 static const char* DS = ".btf-descriptor-v2"; 213 static const char* DS = ".btf-descriptor-v2";
175 SHA256_CTX ctx; 214 SHA256_CTX ctx;
@@ -184,9 +223,27 @@ static void DescriptorDigest(const unsigned char pubkey[32], const std::string&
184 SHA256_Final(out, &ctx); 223 SHA256_Final(out, &ctx);
185} 224}
186 225
226static void DescriptorDigestV3(const unsigned char pubkey[32], const std::string& enc,
227 const std::string& meeting_node, const std::string& onion,
228 uint64_t created, unsigned char out[32])
229{
230 static const char* DS = ".btf-descriptor-v3-onion";
231 SHA256_CTX ctx;
232 SHA256_Init(&ctx);
233 SHA256_Update(&ctx, (const unsigned char*)DS, strlen(DS));
234 SHA256_Update(&ctx, pubkey, 32);
235 SHA256_Update(&ctx, (const unsigned char*)enc.data(), enc.size());
236 SHA256_Update(&ctx, (const unsigned char*)meeting_node.data(), meeting_node.size());
237 SHA256_Update(&ctx, (const unsigned char*)onion.data(), onion.size());
238 unsigned char be[8];
239 for (int i = 0; i < 8; i++) be[i] = (unsigned char)((created >> (8 * (7 - i))) & 0xff);
240 SHA256_Update(&ctx, be, 8);
241 SHA256_Final(out, &ctx);
242}
243
187std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], 244std::string SignDescriptor(void* ctxv, const unsigned char seckey[32],
188 const std::string& enc, const std::string& meeting_node, 245 const std::string& enc, const std::string& meeting_node,
189 uint64_t created) 246 uint64_t created, const std::string& onion)
190{ 247{
191 secp256k1_context* ctx = (secp256k1_context*)ctxv; 248 secp256k1_context* ctx = (secp256k1_context*)ctxv;
192 secp256k1_keypair kp; 249 secp256k1_keypair kp;
@@ -197,17 +254,28 @@ std::string SignDescriptor(void* ctxv, const unsigned char seckey[32],
197 secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xpub); 254 secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xpub);
198 255
199 unsigned char digest[32]; 256 unsigned char digest[32];
200 DescriptorDigest(pubkey, enc, meeting_node, created, digest); 257 DescriptorDigestV2(pubkey, enc, meeting_node, created, digest);
201 258
202 unsigned char sig[64]; 259 unsigned char sig[64];
203 // Deterministic signature (NULL aux_rand), matching Itzal's no-aux-rand signing. 260 // Deterministic signature (NULL aux_rand), matching Itzal's no-aux-rand signing.
204 if (!secp256k1_schnorrsig_sign32(ctx, sig, digest, &kp, NULL)) return std::string(); 261 if (!secp256k1_schnorrsig_sign32(ctx, sig, digest, &kp, NULL)) return std::string();
205 262
206 json v = json::object(); 263 json v = json::object();
207 v["v"] = 2; 264 v["v"] = onion.empty() ? 2 : 3;
208 v["pubkey"] = HexEncode(pubkey, 32); 265 v["pubkey"] = HexEncode(pubkey, 32);
209 v["enc"] = enc; 266 v["enc"] = enc;
210 v["meeting_node"] = meeting_node; 267 v["meeting_node"] = meeting_node;
268 if (!onion.empty())
269 {
270 std::string onionNorm;
271 if (!NormalizeOnionEndpoint(onion, onionNorm)) return std::string();
272 unsigned char digest3[32];
273 DescriptorDigestV3(pubkey, enc, meeting_node, onionNorm, created, digest3);
274 unsigned char sig3[64];
275 if (!secp256k1_schnorrsig_sign32(ctx, sig3, digest3, &kp, NULL)) return std::string();
276 v["onion"] = onionNorm;
277 v["sig3"] = HexEncode(sig3, 64);
278 }
211 v["created"] = created; 279 v["created"] = created;
212 v["sig"] = HexEncode(sig, 64); 280 v["sig"] = HexEncode(sig, 64);
213 return v.dump(); 281 return v.dump();
@@ -242,15 +310,35 @@ bool VerifyDescriptor(void* ctxv, const std::string& jsonStr,
242 if (!HexDecode(v["sig"].get<std::string>(), sig, 64)) return false; 310 if (!HexDecode(v["sig"].get<std::string>(), sig, 64)) return false;
243 311
244 unsigned char digest[32]; 312 unsigned char digest[32];
245 DescriptorDigest(expect_pubkey, enc, meeting_node, created, digest); 313 DescriptorDigestV2(expect_pubkey, enc, meeting_node, created, digest);
246 314
247 secp256k1_xonly_pubkey xpub; 315 secp256k1_xonly_pubkey xpub;
248 if (!secp256k1_xonly_pubkey_parse(ctx, &xpub, expect_pubkey)) return false; 316 if (!secp256k1_xonly_pubkey_parse(ctx, &xpub, expect_pubkey)) return false;
249 if (!secp256k1_schnorrsig_verify(ctx, sig, digest, 32, &xpub)) return false; 317 if (!secp256k1_schnorrsig_verify(ctx, sig, digest, 32, &xpub)) return false;
250 318
319 std::string onion;
320 if (v.contains("onion"))
321 {
322 if (v["onion"].is_string() && v.contains("sig3") && v["sig3"].is_string())
323 {
324 std::string onionNorm;
325 std::string onionRaw = v["onion"].get<std::string>();
326 if (NormalizeOnionEndpoint(onionRaw, onionNorm) && onionNorm == onionRaw)
327 {
328 unsigned char sig3[64];
329 unsigned char digest3[32];
330 DescriptorDigestV3(expect_pubkey, enc, meeting_node, onionNorm, created, digest3);
331 if (HexDecode(v["sig3"].get<std::string>(), sig3, 64) &&
332 secp256k1_schnorrsig_verify(ctx, sig3, digest3, 32, &xpub))
333 onion = onionNorm;
334 }
335 }
336 }
337
251 memcpy(out.pubkey, expect_pubkey, 32); 338 memcpy(out.pubkey, expect_pubkey, 32);
252 out.enc = enc; 339 out.enc = enc;
253 out.meeting_node = meeting_node; 340 out.meeting_node = meeting_node;
341 out.onion = onion;
254 out.created = created; 342 out.created = created;
255 return true; 343 return true;
256 } 344 }
diff --git a/src/btfaddr.h b/src/btfaddr.h
index 1addf8f..55a3ebd 100644
--- a/src/btfaddr.h
+++ b/src/btfaddr.h
@@ -52,15 +52,21 @@ struct Descriptor
52 unsigned char pubkey[32]; 52 unsigned char pubkey[32];
53 std::string enc; // service x25519 public key (hex) for the E2E channel 53 std::string enc; // service x25519 public key (hex) for the E2E channel
54 std::string meeting_node; // rendezvous node the service is registered at 54 std::string meeting_node; // rendezvous node the service is registered at
55 std::string onion; // optional direct Tor hidden-service endpoint (host.onion:port)
55 uint64_t created; 56 uint64_t created;
56}; 57};
57 58
59// Normalize and validate a direct Tor hidden-service endpoint. Only v3 onion
60// names are accepted: 56 base32 chars plus ".onion", followed by ":PORT".
61bool NormalizeOnionEndpoint(const std::string& in, std::string& out);
62
58// Sign a descriptor. `ctx` is a secp256k1_context* (passed as void* to keep this 63// Sign a descriptor. `ctx` is a secp256k1_context* (passed as void* to keep this
59// header free of secp headers); `seckey` is the 32-byte service secret key. 64// header free of secp headers); `seckey` is the 32-byte service secret key.
60// Returns the descriptor JSON as a string, or "" on failure. 65// Returns the descriptor JSON as a string, or "" on failure.
61std::string SignDescriptor(void* ctx, const unsigned char seckey[32], 66std::string SignDescriptor(void* ctx, const unsigned char seckey[32],
62 const std::string& enc, const std::string& meeting_node, 67 const std::string& enc, const std::string& meeting_node,
63 uint64_t created); 68 uint64_t created,
69 const std::string& onion = std::string());
64 70
65// Verify a descriptor is validly signed by `expect_pubkey` (the key the resolved 71// Verify a descriptor is validly signed by `expect_pubkey` (the key the resolved
66// `.btf` address encodes). Returns true and fills `out` on success. 72// `.btf` address encodes). Returns true and fills `out` on success.
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index e941522..e8dacd3 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -1,8 +1,10 @@
1// Bitflash entry point -- starts node threads then runs GUI (or headless). 1// Bitflash entry point -- starts node threads then runs GUI (or headless).
2 2
3#include "headers_core.h" 3#include "headers_core.h"
4#include "nostr.h"
4#include "proxy.h" 5#include "proxy.h"
5#include "selftest.h" 6#include "selftest.h"
7#include "tor.h"
6#include "walletcmd.h" 8#include "walletcmd.h"
7#include <thread> // hardware_concurrency, to sanity-check /genproclimit 9#include <thread> // hardware_concurrency, to sanity-check /genproclimit
8#ifndef _WIN32 10#ifndef _WIN32
@@ -112,7 +114,7 @@ static void PrintUsage()
112 printf(" /gen\n"); 114 printf(" /gen\n");
113 printf(" /nogui or /daemon\n"); 115 printf(" /nogui or /daemon\n");
114 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n"); 116 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n");
115 printf(" net-message, consensus-limits, pool-stratum, parse-money, or socks5-proxy\n"); 117 printf(" net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, or managed-tor\n");
116 printf("\n"); 118 printf("\n");
117 printf("Mining mode:\n"); 119 printf("Mining mode:\n");
118 printf(" /operator\n"); 120 printf(" /operator\n");
@@ -137,11 +139,15 @@ static void PrintUsage()
137 printf(" /connectbtf=PEER_BTF_ADDRESS\n"); 139 printf(" /connectbtf=PEER_BTF_ADDRESS\n");
138 printf(" /rvrelay=HOST:PORT\n"); 140 printf(" /rvrelay=HOST:PORT\n");
139 printf(" /announcerelay=HOST:PORT\n"); 141 printf(" /announcerelay=HOST:PORT\n");
142 printf(" /onionservice=HOST.onion:PORT (advertise this node's Tor hidden service)\n");
140 printf("\n"); 143 printf("\n");
141 printf("Network:\n"); 144 printf("Network:\n");
142 printf(" /port=N (P2P listen port, default 8433)\n"); 145 printf(" /port=N (P2P listen port, default 8433)\n");
143 printf(" /socks=HOST:PORT (SOCKS5 proxy for outbound Nostr and .btf relay dials)\n"); 146 printf(" /socks=HOST:PORT (SOCKS5 proxy for Nostr, .btf relay, and onion dials)\n");
144 printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n"); 147 printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n");
148 printf(" /managedtor[=PATH] (start Tor, create a hidden service, advertise its onion)\n");
149 printf(" /nomanagedtor (disable automatic bundled Tor startup)\n");
150 printf(" /oniononly (do not fall back to rendezvous when a .btf onion dial fails)\n");
145 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); 151 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n");
146 printf("\n"); 152 printf("\n");
147 printf("Wallet:\n"); 153 printf("Wallet:\n");
@@ -295,6 +301,7 @@ static void ParseStartupArguments(int argc, char* argv[])
295 string btfConnect = argval2(argc, argv, "/connectbtf", "-connectbtf"); 301 string btfConnect = argval2(argc, argv, "/connectbtf", "-connectbtf");
296 if (!btfConnect.empty()) 302 if (!btfConnect.empty())
297 strBtfConnect = btfConnect; 303 strBtfConnect = btfConnect;
304 fBtfOnionOnly = arg(argc, argv, "/oniononly") || arg(argc, argv, "-oniononly");
298 305
299 string rvRelay = argval2(argc, argv, "/rvrelay", "-rvrelay"); 306 string rvRelay = argval2(argc, argv, "/rvrelay", "-rvrelay");
300 if (!rvRelay.empty()) 307 if (!rvRelay.empty())
@@ -315,14 +322,67 @@ static void ParseStartupArguments(int argc, char* argv[])
315 } 322 }
316 } 323 }
317 324
325 // net.cpp has described nListenPort as "tunable via /port" since it was
326 // written, but nothing ever read the option, so the port was fixed at 8433
327 // and a second node could not start on a machine already running one. Parse
328 // it before Tor setup so a managed hidden service maps the real listener.
329 string strPort = argval2(argc, argv, "/port", "-port");
330 if (!strPort.empty())
331 {
332 int nPort = atoi(strPort.c_str());
333 if (nPort <= 0 || nPort > 65535)
334 fprintf(stderr, "Ignoring /port=%s: not a port number\n", strPort.c_str());
335 else
336 {
337 nListenPort = htons((unsigned short)nPort);
338 // Or we would announce a port we never bound.
339 addrLocalHost.port = nListenPort;
340 }
341 }
342
318 string announceRelay = argval2(argc, argv, "/announcerelay", "-announcerelay"); 343 string announceRelay = argval2(argc, argv, "/announcerelay", "-announcerelay");
319 if (!announceRelay.empty()) 344 if (!announceRelay.empty())
320 strBtfAnnounceRelay = announceRelay; 345 strBtfAnnounceRelay = announceRelay;
321 346
347 string onionService = argval2(argc, argv, "/onionservice", "-onionservice");
348 if (!onionService.empty())
349 {
350 string err;
351 if (!BtfSetLocalOnionEndpoint(onionService, err))
352 fprintf(stderr, "Ignoring /onionservice=%s: %s\n", onionService.c_str(), err.c_str());
353 else
354 fprintf(stderr, "Direct onion peer endpoint advertised: %s\n",
355 BtfLocalOnionEndpoint().c_str());
356 }
357
358 bool fManagedTor = arg(argc, argv, "/managedtor") || arg(argc, argv, "-managedtor");
359 bool fNoManagedTor = arg(argc, argv, "/nomanagedtor") || arg(argc, argv, "-nomanagedtor");
322 bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor"); 360 bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor");
323 if (fTorMode) 361 string socksProxy = argval2(argc, argv, "/socks", "-socks");
362 string bundledTorPath;
363 bool fAutoManagedTor = !fManagedTor && !fNoManagedTor && !fTorMode &&
364 socksProxy.empty() && BtfBundledTorPath(bundledTorPath);
365 if (fManagedTor || fAutoManagedTor)
366 {
367 if (fManagedTor && (fTorMode || !socksProxy.empty()))
368 fprintf(stderr, "Warning: /managedtor takes precedence over /tor and /socks\n");
369
370 string torPath = fManagedTor ? argval2(argc, argv, "/managedtor", "-managedtor") : bundledTorPath;
371 string err;
372 if (!BtfStartManagedTor(torPath, err))
373 {
374 if (fAutoManagedTor)
375 fprintf(stderr, "Bundled Tor not enabled automatically: %s\n", err.c_str());
376 else
377 fprintf(stderr, "Ignoring /managedtor=%s: %s\n", torPath.c_str(), err.c_str());
378 }
379 else
380 fprintf(stderr, "Managed Tor enabled%s: %s\n",
381 fAutoManagedTor ? " automatically" : "",
382 BtfManagedTorStatus().c_str());
383 }
384 else if (fTorMode)
324 { 385 {
325 string socksProxy = argval2(argc, argv, "/socks", "-socks");
326 if (!socksProxy.empty()) 386 if (!socksProxy.empty())
327 fprintf(stderr, "Warning: /tor takes precedence over /socks=%s\n", 387 fprintf(stderr, "Warning: /tor takes precedence over /socks=%s\n",
328 socksProxy.c_str()); 388 socksProxy.c_str());
@@ -337,7 +397,6 @@ static void ParseStartupArguments(int argc, char* argv[])
337 } 397 }
338 else 398 else
339 { 399 {
340 string socksProxy = argval2(argc, argv, "/socks", "-socks");
341 if (!socksProxy.empty()) 400 if (!socksProxy.empty())
342 { 401 {
343 string err; 402 string err;
@@ -348,6 +407,8 @@ static void ParseStartupArguments(int argc, char* argv[])
348 BtfSocks5ProxyName().c_str()); 407 BtfSocks5ProxyName().c_str());
349 } 408 }
350 } 409 }
410 if (fBtfOnionOnly && !BtfSocks5ProxyEnabled())
411 fprintf(stderr, "Warning: /oniononly without /tor, /managedtor, or /socks cannot dial .onion peers\n");
351 412
352 // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for 413 // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for
353 // testing the seed path and for private networks that ship no compiled list. 414 // testing the seed path and for private networks that ship no compiled list.
@@ -368,22 +429,6 @@ static void ParseStartupArguments(int argc, char* argv[])
368 vBtfExtraSeeds.push_back(make_pair(val.substr(0, colon), val.substr(colon + 1))); 429 vBtfExtraSeeds.push_back(make_pair(val.substr(0, colon), val.substr(colon + 1)));
369 } 430 }
370 431
371 // net.cpp has described nListenPort as "tunable via /port" since it was
372 // written, but nothing ever read the option, so the port was fixed at 8433
373 // and a second node could not start on a machine already running one.
374 string strPort = argval2(argc, argv, "/port", "-port");
375 if (!strPort.empty())
376 {
377 int nPort = atoi(strPort.c_str());
378 if (nPort <= 0 || nPort > 65535)
379 fprintf(stderr, "Ignoring /port=%s: not a port number\n", strPort.c_str());
380 else
381 {
382 nListenPort = htons((unsigned short)nPort);
383 // Or we would announce a port we never bound.
384 addrLocalHost.port = nListenPort;
385 }
386 }
387} 432}
388 433
389// Report a startup failure somewhere the user will actually see it. 434// Report a startup failure somewhere the user will actually see it.
@@ -760,7 +805,12 @@ int main(int argc, char* argv[])
760 return 0; 805 return 0;
761 } 806 }
762 807
763 if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } 808 if (!StartNode(strErrors))
809 {
810 fprintf(stderr,"StartNode: %s\n",strErrors.c_str());
811 BtfStopManagedTor();
812 return 1;
813 }
764 814
765 if (nMineMode == MINE_OPERATOR) { 815 if (nMineMode == MINE_OPERATOR) {
766 gPoolServerRunning = true; 816 gPoolServerRunning = true;
@@ -793,6 +843,7 @@ int main(int argc, char* argv[])
793 printf("Running headless. Ctrl-C to stop.\n"); 843 printf("Running headless. Ctrl-C to stop.\n");
794 while (!fShutdown) Sleep(500); 844 while (!fShutdown) Sleep(500);
795 StopNode(); 845 StopNode();
846 BtfStopManagedTor();
796 // Checkpoints, releases wallet.dat from this directory's Berkeley DB 847 // Checkpoints, releases wallet.dat from this directory's Berkeley DB
797 // environment, and closes it. Declared since 2009 and never once 848 // environment, and closes it. Declared since 2009 and never once
798 // called here, which is why a wallet.dat copied elsewhere would not 849 // called here, which is why a wallet.dat copied elsewhere would not
@@ -812,6 +863,7 @@ int main(int argc, char* argv[])
812 int ret = RunGUI(argc, argv); 863 int ret = RunGUI(argc, argv);
813 fShutdown = true; 864 fShutdown = true;
814 StopNode(); 865 StopNode();
866 BtfStopManagedTor();
815 DBFlush(true); 867 DBFlush(true);
816 return ret; 868 return ret;
817#endif 869#endif
diff --git a/src/makefile.mingw b/src/makefile.mingw
index 8b49cac..0ba1d76 100644
--- a/src/makefile.mingw
+++ b/src/makefile.mingw
@@ -37,7 +37,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
37NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 37NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
38 obj/sha.o obj/nostr.o obj/randomx_pow.o \ 38 obj/sha.o obj/nostr.o obj/randomx_pow.o \
39 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 39 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
40 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o 40 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o
41 41
42GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o 42GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
43 43
@@ -51,7 +51,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
51HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 51HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
52 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 52 key.h bignum.h script.h db.h base58.h net.h nostr.h \
53 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ 53 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \
54 selftest.h walletcmd.h sockcount.h proxy.h compat.h font_roboto.h 54 selftest.h walletcmd.h sockcount.h proxy.h tor.h compat.h font_roboto.h
55 55
56all: bitflash.exe 56all: bitflash.exe
57 57
@@ -113,3 +113,4 @@ tests: test_bip32.exe bitflash.exe
113 ./bitflash.exe -selftest=consensus-limits 113 ./bitflash.exe -selftest=consensus-limits
114 ./bitflash.exe -selftest=pool-stratum 114 ./bitflash.exe -selftest=pool-stratum
115 ./bitflash.exe -selftest=socks5-proxy 115 ./bitflash.exe -selftest=socks5-proxy
116 ./bitflash.exe -selftest=managed-tor
diff --git a/src/net.cpp b/src/net.cpp
index bb39122..35baf21 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -17,9 +17,12 @@
17#include <poll.h> 17#include <poll.h>
18#endif 18#endif
19#include <openssl/rand.h> 19#include <openssl/rand.h>
20#include <cerrno>
21#include <cstdlib>
20#include "btfaddr.h" 22#include "btfaddr.h"
21#include "btftunnel.h" 23#include "btftunnel.h"
22#include "proxy.h" 24#include "proxy.h"
25#include "tor.h"
23 26
24void ThreadReconnectCachedBtfPeers(void* parg); 27void ThreadReconnectCachedBtfPeers(void* parg);
25void ThreadMessageHandler2(void* parg); 28void ThreadMessageHandler2(void* parg);
@@ -62,6 +65,8 @@ deque<pair<int64, CInv> > vRelayExpiration;
62CCriticalSection cs_mapRelay; 65CCriticalSection cs_mapRelay;
63map<CInv, int64> mapAlreadyAskedFor; 66map<CInv, int64> mapAlreadyAskedFor;
64string strBtfConnect; // .btf peer to keep connected to (from /connectbtf) 67string strBtfConnect; // .btf peer to keep connected to (from /connectbtf)
68bool fBtfOnionOnly = false; // require direct onion transport for .btf peers
69static const int BTF_ONION_CONNECT_TIMEOUT_SECS = 120;
65 70
66int nPeersWatched = 0; 71int nPeersWatched = 0;
67int64 nBlocksReceived = 0; 72int64 nBlocksReceived = 0;
@@ -170,7 +175,7 @@ static const char* pszSockSite[SOCK_SITES] = {
170 "external-ip probe", "listen socket", "inbound accept", 175 "external-ip probe", "listen socket", "inbound accept",
171 "rendezvous dial", "rendezvous listen", "rendezvous accept", 176 "rendezvous dial", "rendezvous listen", "rendezvous accept",
172 "loopback pair listener", "loopback pair app end", "loopback pair pump end", 177 "loopback pair listener", "loopback pair app end", "loopback pair pump end",
173 "nostr relay", "stratum bridge" 178 "nostr relay", "direct onion peer", "stratum bridge"
174}; 179};
175 180
176static string SockAccountingText() 181static string SockAccountingText()
@@ -263,15 +268,21 @@ string GetDiagnosticsText()
263 int64 nNow = GetTime(); 268 int64 nNow = GetTime();
264 string str; 269 string str;
265 270
266 int nInbound = 0, nHeld = 0; 271 int nInbound = 0, nHeld = 0, nDirectOnion = 0, nRendezvous = 0;
267 vector<CNode*> vCopy; 272 vector<CNode*> vCopy;
268 CRITICAL_BLOCK(cs_vNodes) 273 CRITICAL_BLOCK(cs_vNodes)
269 { 274 {
270 vCopy = vNodes; 275 vCopy = vNodes;
271 nHeld = (int)vNodes.size(); 276 nHeld = (int)vNodes.size();
272 foreach(CNode* pnode, vNodes) 277 foreach(CNode* pnode, vNodes)
278 {
273 if (pnode->fInbound) 279 if (pnode->fInbound)
274 nInbound++; 280 nInbound++;
281 if (pnode->strBtfMeeting.find(".onion:") != string::npos)
282 nDirectOnion++;
283 else if (!pnode->strBtfMeeting.empty())
284 nRendezvous++;
285 }
275 } 286 }
276 287
277 int nMedian = GetPeerMedianHeight(); 288 int nMedian = GetPeerMedianHeight();
@@ -294,6 +305,23 @@ string GetDiagnosticsText()
294 str += strprintf(" peers watched %d of %d held by poll%s\n", 305 str += strprintf(" peers watched %d of %d held by poll%s\n",
295 nPeersWatched, nHeld, 306 nPeersWatched, nHeld,
296 nHeld > nPeersWatched ? " <-- NOT ALL PEERS ARE BEING READ" : ""); 307 nHeld > nPeersWatched ? " <-- NOT ALL PEERS ARE BEING READ" : "");
308 str += strprintf(" managed Tor %s\n", BtfManagedTorStatus().c_str());
309
310 string strTorMode = "disabled";
311 if (BtfManagedTorEnabled())
312 strTorMode = "managed";
313 else if (BtfTorProxyEnabled())
314 strTorMode = "external";
315 else if (BtfSocks5ProxyEnabled())
316 strTorMode = "socks5";
317 string strOnion = BtfLocalOnionEndpoint();
318 str += strprintf(" Tor mode %s%s\n",
319 strTorMode.c_str(),
320 fBtfOnionOnly ? " (onion-only)" : "");
321 str += strprintf(" onion endpoint %s\n",
322 strOnion.empty() ? "none" : strOnion.c_str());
323 str += strprintf(" .btf peers %d direct onion, %d rendezvous\n",
324 nDirectOnion, nRendezvous);
297 325
298 if (nBlocksReceived > 0) 326 if (nBlocksReceived > 0)
299 str += strprintf(" blocks received %lld (%lld arrived without a parent, %.1f%%)\n", 327 str += strprintf(" blocks received %lld (%lld arrived without a parent, %.1f%%)\n",
@@ -544,6 +572,7 @@ struct CachedBtfPeer
544{ 572{
545 string btfAddr; 573 string btfAddr;
546 string meeting; 574 string meeting;
575 string onion;
547 string encHex; 576 string encHex;
548 int64 lastSeen; 577 int64 lastSeen;
549 // The peer's own signed descriptor, as it announced itself. Kept verbatim 578 // The peer's own signed descriptor, as it announced itself. Kept verbatim
@@ -602,6 +631,8 @@ void LoadCachedBtfPeers(vector<CachedBtfPeer>& out)
602 CachedBtfPeer p; 631 CachedBtfPeer p;
603 p.btfAddr = o["btf"].get<string>(); 632 p.btfAddr = o["btf"].get<string>();
604 p.meeting = o["meeting"].get<string>(); 633 p.meeting = o["meeting"].get<string>();
634 if (o.contains("onion") && o["onion"].is_string())
635 btf::NormalizeOnionEndpoint(o["onion"].get<string>(), p.onion);
605 p.encHex = o["enc"].get<string>(); 636 p.encHex = o["enc"].get<string>();
606 p.lastSeen = o.value("seen", (int64)0); 637 p.lastSeen = o.value("seen", (int64)0);
607 if (o.contains("desc") && o["desc"].is_string()) 638 if (o.contains("desc") && o["desc"].is_string())
@@ -625,7 +656,8 @@ static CCriticalSection cs_btfPeerCache;
625// learned about by exchange must not cost us the ability to pass it on. 656// learned about by exchange must not cost us the ability to pass it on.
626static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting, 657static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
627 const unsigned char enc_pub[32], 658 const unsigned char enc_pub[32],
628 const string& strDesc = string()) 659 const string& strDesc = string(),
660 const string& strOnion = string())
629{ 661{
630 CRITICAL_BLOCK(cs_btfPeerCache) 662 CRITICAL_BLOCK(cs_btfPeerCache)
631 { 663 {
@@ -633,11 +665,15 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
633 LoadCachedBtfPeers(peers); 665 LoadCachedBtfPeers(peers);
634 666
635 string strKeepDesc = strDesc; 667 string strKeepDesc = strDesc;
668 string strKeepOnion;
669 btf::NormalizeOnionEndpoint(strOnion, strKeepOnion);
636 for (size_t i = 0; i < peers.size(); i++) 670 for (size_t i = 0; i < peers.size(); i++)
637 if (peers[i].btfAddr == strBtfAddr) 671 if (peers[i].btfAddr == strBtfAddr)
638 { 672 {
639 if (strKeepDesc.empty()) 673 if (strKeepDesc.empty())
640 strKeepDesc = peers[i].desc; 674 strKeepDesc = peers[i].desc;
675 if (strKeepOnion.empty())
676 strKeepOnion = peers[i].onion;
641 peers.erase(peers.begin() + i); 677 peers.erase(peers.begin() + i);
642 break; 678 break;
643 } 679 }
@@ -645,6 +681,7 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
645 CachedBtfPeer p; 681 CachedBtfPeer p;
646 p.btfAddr = strBtfAddr; 682 p.btfAddr = strBtfAddr;
647 p.meeting = strMeeting; 683 p.meeting = strMeeting;
684 p.onion = strKeepOnion;
648 p.encHex = BytesToHex(enc_pub, 32); 685 p.encHex = BytesToHex(enc_pub, 32);
649 p.lastSeen = GetTime(); 686 p.lastSeen = GetTime();
650 p.desc = strKeepDesc; 687 p.desc = strKeepDesc;
@@ -659,6 +696,8 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
659 nlohmann::json o; 696 nlohmann::json o;
660 o["btf"] = q.btfAddr; 697 o["btf"] = q.btfAddr;
661 o["meeting"] = q.meeting; 698 o["meeting"] = q.meeting;
699 if (!q.onion.empty())
700 o["onion"] = q.onion;
662 o["enc"] = q.encHex; 701 o["enc"] = q.encHex;
663 o["seen"] = q.lastSeen; 702 o["seen"] = q.lastSeen;
664 if (!q.desc.empty()) 703 if (!q.desc.empty())
@@ -747,7 +786,7 @@ static int TryBtfSeeds()
747 foreach(const string& strRelay, relays) 786 foreach(const string& strRelay, relays)
748 { 787 {
749 if (fShutdown) return nConnected; 788 if (fShutdown) return nConnected;
750 if (ConnectNodeBtfResolved(s.first, strRelay, enc)) 789 if (ConnectNodeBtfResolved(s.first, strRelay, "", enc))
751 { 790 {
752 LogPrint("net", "btfseed: reached %s via %s\n", 791 LogPrint("net", "btfseed: reached %s via %s\n",
753 s.first.c_str(), strRelay.c_str()); 792 s.first.c_str(), strRelay.c_str());
@@ -785,7 +824,7 @@ void ThreadReconnectCachedBtfPeers(void* parg)
785 if (fShutdown) return; 824 if (fShutdown) return;
786 unsigned char enc[32]; 825 unsigned char enc[32];
787 if (!HexToBytes(p.encHex, enc, 32)) continue; 826 if (!HexToBytes(p.encHex, enc, 32)) continue;
788 if (ConnectNodeBtfResolved(p.btfAddr, p.meeting, enc)) 827 if (ConnectNodeBtfResolved(p.btfAddr, p.meeting, p.onion, enc))
789 { 828 {
790 nConnected++; 829 nConnected++;
791 if (nConnected >= 8) break; // enough to bootstrap; the rest can wait 830 if (nConnected >= 8) break; // enough to bootstrap; the rest can wait
@@ -876,14 +915,37 @@ int BtfPexAccept(const vector<string>& vDesc)
876 if (!HexToBytes(d.enc, enc, 32)) 915 if (!HexToBytes(d.enc, enc, 32))
877 continue; 916 continue;
878 917
879 RememberBtfPeer(strAddr, d.meeting_node, enc, strDesc); 918 RememberBtfPeer(strAddr, d.meeting_node, enc, strDesc, d.onion);
880 nKept++; 919 nKept++;
881 } 920 }
882 return nKept; 921 return nKept;
883} 922}
884 923
924static bool SplitHostPort(const string& strHostPort, string& strHost, unsigned short& nPort)
925{
926 size_t colon = strHostPort.rfind(':');
927 if (colon == string::npos || colon == 0 || colon + 1 >= strHostPort.size())
928 return false;
929 string port = strHostPort.substr(colon + 1);
930 for (size_t i = 0; i < port.size(); i++)
931 if (!isdigit((unsigned char)port[i]))
932 return false;
933
934 errno = 0;
935 char* end = NULL;
936 long n = strtol(port.c_str(), &end, 10);
937 if (errno == ERANGE || end == NULL || *end != '\0' || n <= 0 || n > 65535)
938 return false;
939
940 strHost = strHostPort.substr(0, colon);
941 nPort = (unsigned short)n;
942 return true;
943}
944
885static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32], 945static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32],
886 const string& strMeeting, const unsigned char enc_pub[32]) 946 const string& strMeeting, const string& strOnion,
947 const unsigned char enc_pub[32],
948 const string& strDesc = string())
887{ 949{
888 CAddress addr = BtfMarkerAddr(pk); 950 CAddress addr = BtfMarkerAddr(pk);
889 CNode* pnode = FindNode(addr.ip); 951 CNode* pnode = FindNode(addr.ip);
@@ -893,16 +955,54 @@ static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char p
893 return pnode; 955 return pnode;
894 } 956 }
895 957
896 BtfChurnNoteDialAttempt(strBtfAddr, strMeeting); 958 string strOnionNorm;
897 size_t colon = strMeeting.rfind(':'); 959 if (BtfSocks5ProxyEnabled() && btf::NormalizeOnionEndpoint(strOnion, strOnionNorm))
898 if (colon == string::npos)
899 { 960 {
900 BtfChurnNoteDialResult(strBtfAddr, strMeeting, false); 961 string strOnionHost;
962 unsigned short nOnionPort = 0;
963 if (SplitHostPort(strOnionNorm, strOnionHost, nOnionPort))
964 {
965 BtfChurnNoteDialAttempt(strBtfAddr, strOnionNorm);
966 SOCKET hOnionSocket = BtfConnectSocket(strOnionHost, nOnionPort, SOCK_ONION_PEER,
967 BTF_ONION_CONNECT_TIMEOUT_SECS);
968 if (hOnionSocket != INVALID_SOCKET)
969 {
970 if (fDebug)
971 LogPrint("net", "connected %s via direct onion %s\n",
972 strBtfAddr.c_str(), strOnionNorm.c_str());
973 BtfChurnNoteDialResult(strBtfAddr, strOnionNorm, true);
974 RememberBtfPeer(strBtfAddr, strMeeting, enc_pub, strDesc, strOnionNorm);
975
976 pnode = new CNode(hOnionSocket, addr, false);
977 pnode->strBtfAddr = strBtfAddr;
978 pnode->strBtfMeeting = string("onion ") + strOnionNorm;
979 pnode->AddRef();
980 CRITICAL_BLOCK(cs_vNodes)
981 vNodes.push_back(pnode);
982 return pnode;
983 }
984 if (fDebug)
985 LogPrint("net", "ConnectNodeBtf: direct onion to %s at %s failed%s\n",
986 strBtfAddr.c_str(), strOnionNorm.c_str(),
987 fBtfOnionOnly ? "" : ", falling back to rendezvous");
988 BtfChurnNoteDialResult(strBtfAddr, strOnionNorm, false);
989 if (fBtfOnionOnly)
990 return NULL;
991 }
992 }
993
994 if (fBtfOnionOnly)
995 {
996 if (fDebug)
997 LogPrint("net", "ConnectNodeBtf: onion-only mode refused rendezvous fallback for %s\n",
998 strBtfAddr.c_str());
901 return NULL; 999 return NULL;
902 } 1000 }
903 string strHost = strMeeting.substr(0, colon); 1001
904 int nPort = atoi(strMeeting.substr(colon + 1).c_str()); 1002 BtfChurnNoteDialAttempt(strBtfAddr, strMeeting);
905 if (nPort <= 0 || nPort > 65535) 1003 string strHost;
1004 unsigned short nPort = 0;
1005 if (!SplitHostPort(strMeeting, strHost, nPort))
906 { 1006 {
907 BtfChurnNoteDialResult(strBtfAddr, strMeeting, false); 1007 BtfChurnNoteDialResult(strBtfAddr, strMeeting, false);
908 return NULL; 1008 return NULL;
@@ -922,7 +1022,7 @@ static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char p
922 BtfChurnNoteDialResult(strBtfAddr, strMeeting, true); 1022 BtfChurnNoteDialResult(strBtfAddr, strMeeting, true);
923 1023
924 // This one answered -- worth trying first next time we start. 1024 // This one answered -- worth trying first next time we start.
925 RememberBtfPeer(strBtfAddr, strMeeting, enc_pub); 1025 RememberBtfPeer(strBtfAddr, strMeeting, enc_pub, strDesc, strOnionNorm);
926 1026
927 // Add node 1027 // Add node
928 pnode = new CNode(hSocket, addr, false); 1028 pnode = new CNode(hSocket, addr, false);
@@ -952,9 +1052,10 @@ CNode* ConnectNodeBtf(const string& strBtfAddr)
952 LogPrint("net", "trying %s\n", strBtfAddr.c_str()); 1052 LogPrint("net", "trying %s\n", strBtfAddr.c_str());
953 1053
954 string strMeeting; 1054 string strMeeting;
1055 string strOnion;
955 unsigned char enc_pub[32]; 1056 unsigned char enc_pub[32];
956 BtfChurnNoteResolveAttempt(); 1057 BtfChurnNoteResolveAttempt();
957 if (!BtfResolve(strBtfAddr, strMeeting, enc_pub)) 1058 if (!BtfResolve(strBtfAddr, strMeeting, enc_pub, &strOnion))
958 { 1059 {
959 BtfChurnNoteResolveResult(false); 1060 BtfChurnNoteResolveResult(false);
960 if (fDebug) 1061 if (fDebug)
@@ -962,7 +1063,7 @@ CNode* ConnectNodeBtf(const string& strBtfAddr)
962 return NULL; 1063 return NULL;
963 } 1064 }
964 BtfChurnNoteResolveResult(true); 1065 BtfChurnNoteResolveResult(true);
965 return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, enc_pub); 1066 return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, strOnion, enc_pub);
966} 1067}
967 1068
968// Same as ConnectNodeBtf, but skips the Nostr resolve step because the caller 1069// Same as ConnectNodeBtf, but skips the Nostr resolve step because the caller
@@ -971,7 +1072,8 @@ CNode* ConnectNodeBtf(const string& strBtfAddr)
971// address). Makes zero relay connections -- only talks to the peer's 1072// address). Makes zero relay connections -- only talks to the peer's
972// rendezvous meeting node. 1073// rendezvous meeting node.
973CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, 1074CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting,
974 const unsigned char enc_pub[32]) 1075 const string& strOnion, const unsigned char enc_pub[32],
1076 const string& strDesc)
975{ 1077{
976 unsigned char pk[32]; 1078 unsigned char pk[32];
977 if (!btf::ParseAddress(strBtfAddr, pk)) 1079 if (!btf::ParseAddress(strBtfAddr, pk))
@@ -981,7 +1083,7 @@ CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting
981 } 1083 }
982 if (BtfLocalAddress() == strBtfAddr) 1084 if (BtfLocalAddress() == strBtfAddr)
983 return NULL; // ourselves 1085 return NULL; // ourselves
984 return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, enc_pub); 1086 return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, strOnion, enc_pub, strDesc);
985} 1087}
986 1088
987// Anonymous inbound listener (this node's `.btf` hidden service). Registers at 1089// Anonymous inbound listener (this node's `.btf` hidden service). Registers at
@@ -1936,6 +2038,11 @@ bool StartNode(string& strError)
1936 // IP address, and port for the socket that is being bound 2038 // IP address, and port for the socket that is being bound
1937 int nRetryLimit = 15; 2039 int nRetryLimit = 15;
1938 struct sockaddr_in sockaddr = addrLocalHost.GetSockAddr(); 2040 struct sockaddr_in sockaddr = addrLocalHost.GetSockAddr();
2041 // Keep addrLocalHost as this node's advertised address, but listen on all
2042 // local interfaces. Managed Tor forwards hidden-service traffic to
2043 // 127.0.0.1:<port>, and binding only to the LAN address made that onion
2044 // endpoint unreachable even though the P2P listener was running.
2045 sockaddr.sin_addr.s_addr = INADDR_ANY;
1939 if (bind(hListenSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR) 2046 if (bind(hListenSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR)
1940 { 2047 {
1941 int nErr = WSAGetLastError(); 2048 int nErr = WSAGetLastError();
diff --git a/src/net.h b/src/net.h
index 653a015..9f3c7e4 100644
--- a/src/net.h
+++ b/src/net.h
@@ -26,10 +26,12 @@ enum
26bool GetMyExternalIP(unsigned int& ipRet); 26bool GetMyExternalIP(unsigned int& ipRet);
27CNode* ConnectNodeBtf(const string& strBtfAddr); 27CNode* ConnectNodeBtf(const string& strBtfAddr);
28CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, 28CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting,
29 const unsigned char enc_pub[32]); 29 const string& strOnion, const unsigned char enc_pub[32],
30 const string& strDesc=string());
30void ThreadBtfAccept(void* parg); 31void ThreadBtfAccept(void* parg);
31void ThreadBtfConnect(void* parg); 32void ThreadBtfConnect(void* parg);
32extern string strBtfConnect; 33extern string strBtfConnect;
34extern bool fBtfOnionOnly;
33// Bootstrap seeds added at runtime with /btfseed=ADDRESS:ENCHEX (repeatable). 35// Bootstrap seeds added at runtime with /btfseed=ADDRESS:ENCHEX (repeatable).
34// The compiled-in list lives in net.cpp; these are appended to it. 36// The compiled-in list lives in net.cpp; these are appended to it.
35extern std::vector<std::pair<std::string, std::string> > vBtfExtraSeeds; 37extern std::vector<std::pair<std::string, std::string> > vBtfExtraSeeds;
diff --git a/src/nostr.cpp b/src/nostr.cpp
index 32cea18..e2d839c 100644
--- a/src/nostr.cpp
+++ b/src/nostr.cpp
@@ -132,6 +132,8 @@ vector<string> vBtfMeetingRelays = {
132bool fBtfRelayPinned = false; 132bool fBtfRelayPinned = false;
133string strBtfActiveRelay; 133string strBtfActiveRelay;
134static CCriticalSection cs_activeRelay; 134static CCriticalSection cs_activeRelay;
135static string strBtfOnionEndpoint;
136static CCriticalSection cs_onionEndpoint;
135 137
136// Only the rendezvous we actually registered at (BtfSetActiveRelay, called from 138// Only the rendezvous we actually registered at (BtfSetActiveRelay, called from
137// ThreadBtfAccept once registration succeeds). Empty until then, on purpose: it 139// ThreadBtfAccept once registration succeeds). Empty until then, on purpose: it
@@ -153,6 +155,26 @@ void BtfSetActiveRelay(const string& relay)
153 strBtfActiveRelay = relay; 155 strBtfActiveRelay = relay;
154} 156}
155 157
158bool BtfSetLocalOnionEndpoint(const string& endpoint, string& errOut)
159{
160 string norm;
161 if (!btf::NormalizeOnionEndpoint(endpoint, norm))
162 {
163 errOut = "expected v3 onion endpoint HOST.onion:PORT";
164 return false;
165 }
166 CRITICAL_BLOCK(cs_onionEndpoint)
167 strBtfOnionEndpoint = norm;
168 return true;
169}
170
171string BtfLocalOnionEndpoint()
172{
173 CRITICAL_BLOCK(cs_onionEndpoint)
174 return strBtfOnionEndpoint;
175 return "";
176}
177
156// A relay this node advertises on Nostr for others to discover (/announcerelay). 178// A relay this node advertises on Nostr for others to discover (/announcerelay).
157string strBtfAnnounceRelay; 179string strBtfAnnounceRelay;
158// Relays learned from other operators' Nostr announcements. 180// Relays learned from other operators' Nostr announcements.
@@ -392,10 +414,16 @@ std::string BtfLocalDescriptor()
392 string meeting; 414 string meeting;
393 CRITICAL_BLOCK(cs_activeRelay) 415 CRITICAL_BLOCK(cs_activeRelay)
394 meeting = strBtfActiveRelay; 416 meeting = strBtfActiveRelay;
417 string onion = BtfLocalOnionEndpoint();
395 if (meeting.empty()) 418 if (meeting.empty())
396 return ""; 419 {
420 if (onion.empty())
421 return "";
422 meeting = "rendezvous-pending";
423 }
397 return btf::SignDescriptor(g_nostrKey.ctx, g_nostrKey.seckey, 424 return btf::SignDescriptor(g_nostrKey.ctx, g_nostrKey.seckey,
398 g_nostrKey.EncPubHex(), meeting, (uint64_t)GetTime()); 425 g_nostrKey.EncPubHex(), meeting, (uint64_t)GetTime(),
426 onion);
399} 427}
400 428
401// The shared secp256k1 context, so net.cpp can verify descriptors that arrive 429// The shared secp256k1 context, so net.cpp can verify descriptors that arrive
@@ -854,17 +882,21 @@ static void PublishDescriptor(CWebSocket& ws, CNostrKey& key)
854 // meeting_node is the rendezvous relay this node's hidden service 882 // meeting_node is the rendezvous relay this node's hidden service
855 // (ThreadBtfAccept in net.cpp) is registered at -- where clients dial us. 883 // (ThreadBtfAccept in net.cpp) is registered at -- where clients dial us.
856 string meeting_node = BtfActiveRelay(); 884 string meeting_node = BtfActiveRelay();
885 string onion = BtfLocalOnionEndpoint();
857 if (meeting_node.empty()) 886 if (meeting_node.empty())
858 { 887 {
859 // Not registered anywhere yet. Publishing now would advertise a meeting 888 if (onion.empty())
860 // node we can't be reached at, and peers would waste dials on it. The 889 {
861 // ThreadNostrSeed loop republishes on every pass, so skipping costs 890 // Not registered anywhere yet, and no direct onion endpoint exists.
862 // nothing but the delay until ThreadBtfAccept registers. 891 // Publishing now would advertise a meeting node we can't be reached at.
863 LogPrint("nostr", "Nostr: skipping descriptor publish, no rendezvous registered yet\n"); 892 LogPrint("nostr", "Nostr: skipping descriptor publish, no rendezvous registered yet\n");
864 return; 893 return;
894 }
895 meeting_node = "rendezvous-pending";
865 } 896 }
866 string desc = btf::SignDescriptor(key.ctx, key.seckey, key.EncPubHex(), 897 string desc = btf::SignDescriptor(key.ctx, key.seckey, key.EncPubHex(),
867 meeting_node, (uint64_t)GetTime()); 898 meeting_node, (uint64_t)GetTime(),
899 onion);
868 if (desc.empty()) 900 if (desc.empty())
869 return; 901 return;
870 json tags = json::array({ json::array({ "d", BTF_DESC_DTAG }) }); 902 json tags = json::array({ json::array({ "d", BTF_DESC_DTAG }) });
@@ -1070,7 +1102,8 @@ static void HandlePoolAnnouncement(const json& ev)
1070// this socket for reuse, and only a completed exchange leaves it in a state 1102// this socket for reuse, and only a completed exchange leaves it in a state
1071// the next caller can trust. 1103// the next caller can trust.
1072static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, 1104static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1073 btf::Descriptor& out, bool& fExchangeOk) 1105 btf::Descriptor& out, bool& fExchangeOk,
1106 string* rawOut = NULL)
1074{ 1107{
1075 fExchangeOk = false; 1108 fExchangeOk = false;
1076 unsigned char pk[32]; 1109 unsigned char pk[32];
@@ -1106,7 +1139,11 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1106 const json& ev = j[2]; 1139 const json& ev = j[2];
1107 if (!fFound && ev.contains("content") && 1140 if (!fFound && ev.contains("content") &&
1108 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) 1141 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out))
1142 {
1109 fFound = true; 1143 fFound = true;
1144 if (rawOut)
1145 *rawOut = ev["content"].get<string>();
1146 }
1110 // Deliberately keep reading to EOSE rather than returning here. 1147 // Deliberately keep reading to EOSE rather than returning here.
1111 // Returning early leaves the rest of the relay's answer queued on a 1148 // Returning early leaves the rest of the relay's answer queued on a
1112 // socket that goes straight back into the pool, and the next caller 1149 // socket that goes straight back into the pool, and the next caller
@@ -1126,8 +1163,10 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1126// public discovery relays for the owner's self-certified descriptor. Used by 1163// public discovery relays for the owner's self-certified descriptor. Used by
1127// ConnectNodeBtf in net.cpp. 1164// ConnectNodeBtf in net.cpp.
1128bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, 1165bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
1129 unsigned char enc_pub[32]) 1166 unsigned char enc_pub[32], std::string* onionHostPort)
1130{ 1167{
1168 if (onionHostPort)
1169 onionHostPort->clear();
1131 if (!EnsureNostrKey()) 1170 if (!EnsureNostrKey())
1132 return false; 1171 return false;
1133 for (int i = 0; i < nNostrRelays && !fShutdown; i++) 1172 for (int i = 0; i < nNostrRelays && !fShutdown; i++)
@@ -1155,10 +1194,15 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
1155 } 1194 }
1156 lease.fOk = fExchangeOk; 1195 lease.fOk = fExchangeOk;
1157 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") 1196 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending")
1158 continue; // descriptor predates the owner's relay config 1197 {
1198 if (!onionHostPort || d.onion.empty())
1199 continue; // caller needs rendezvous, or descriptor has no direct path
1200 }
1159 if (!HexDecode(d.enc, enc_pub, 32)) 1201 if (!HexDecode(d.enc, enc_pub, 32))
1160 continue; 1202 continue;
1161 meetingHostPort = d.meeting_node; 1203 meetingHostPort = d.meeting_node;
1204 if (onionHostPort)
1205 *onionHostPort = d.onion;
1162 return true; 1206 return true;
1163 } 1207 }
1164 CATCH_PRINT_EXCEPTION("BtfResolve") 1208 CATCH_PRINT_EXCEPTION("BtfResolve")
@@ -1247,12 +1291,15 @@ bool BtfResolveMany(const std::vector<std::string>& btfAddrs,
1247 btf::Descriptor d; 1291 btf::Descriptor d;
1248 if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get<string>(), pk, d)) 1292 if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get<string>(), pk, d))
1249 continue; 1293 continue;
1250 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") 1294 if ((d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") &&
1295 d.onion.empty())
1251 continue; 1296 continue;
1252 BtfResolvedPeer rp; 1297 BtfResolvedPeer rp;
1253 if (!HexDecode(d.enc, rp.enc_pub, 32)) 1298 if (!HexDecode(d.enc, rp.enc_pub, 32))
1254 continue; 1299 continue;
1255 rp.meetingHostPort = d.meeting_node; 1300 rp.meetingHostPort = d.meeting_node;
1301 rp.onionHostPort = d.onion;
1302 rp.descriptor = ev["content"].get<string>();
1256 out[ai->second] = rp; 1303 out[ai->second] = rp;
1257 pending.erase(ai->second); 1304 pending.erase(ai->second);
1258 } 1305 }
@@ -1729,11 +1776,13 @@ static void ConnectDiscoveredBtfPeers()
1729 DialCtx* ctx = (DialCtx*)arg; 1776 DialCtx* ctx = (DialCtx*)arg;
1730 string addr = ctx->addr; 1777 string addr = ctx->addr;
1731 string meeting = ctx->rp.meetingHostPort; 1778 string meeting = ctx->rp.meetingHostPort;
1779 string onion = ctx->rp.onionHostPort;
1780 string desc = ctx->rp.descriptor;
1732 unsigned char enc_pub[32]; 1781 unsigned char enc_pub[32];
1733 memcpy(enc_pub, ctx->rp.enc_pub, 32); 1782 memcpy(enc_pub, ctx->rp.enc_pub, 32);
1734 delete ctx; 1783 delete ctx;
1735 1784
1736 CNode* pnode = ConnectNodeBtfResolved(addr, meeting, enc_pub); 1785 CNode* pnode = ConnectNodeBtfResolved(addr, meeting, onion, enc_pub, desc);
1737 CRITICAL_BLOCK(cs_btfPeerState) 1786 CRITICAL_BLOCK(cs_btfPeerState)
1738 { 1787 {
1739 if (pnode) 1788 if (pnode)
@@ -1795,11 +1844,12 @@ void ThreadNostrSeed(void* parg)
1795 LogPrint("nostr", "Nostr: node pubkey = %s\n", key.PubKeyHex().c_str()); 1844 LogPrint("nostr", "Nostr: node pubkey = %s\n", key.PubKeyHex().c_str());
1796 LogPrint("nostr", "Nostr: node .btf address = %s\n", key.BtfAddress().c_str()); 1845 LogPrint("nostr", "Nostr: node .btf address = %s\n", key.BtfAddress().c_str());
1797 1846
1798 // The self-test below publishes a real descriptor, and PublishDescriptor now 1847 // The self-test below publishes a real descriptor. Rendezvous-only nodes
1799 // refuses to publish before ThreadBtfAccept has registered a rendezvous. Give 1848 // need ThreadBtfAccept to register first; Tor nodes can publish as soon as
1800 // registration a bounded head start so the test measures the publish/resolve 1849 // their signed onion endpoint exists, with rendezvous marked pending.
1801 // chain instead of losing the race against it. 1850 for (int i = 0; i < 60 && !fShutdown &&
1802 for (int i = 0; i < 60 && !fShutdown && BtfActiveRelay().empty(); i++) 1851 BtfActiveRelay().empty() &&
1852 BtfLocalOnionEndpoint().empty(); i++)
1803 Sleep(1000); 1853 Sleep(1000);
1804 1854
1805 // One-time self-test of the rendezvous: publish our descriptor to a relay, 1855 // One-time self-test of the rendezvous: publish our descriptor to a relay,
diff --git a/src/nostr.h b/src/nostr.h
index 827b2f3..6dbbe26 100644
--- a/src/nostr.h
+++ b/src/nostr.h
@@ -40,6 +40,13 @@ std::string BtfActiveRelay();
40// Record the relay this node just registered its service at (thread-safe). 40// Record the relay this node just registered its service at (thread-safe).
41void BtfSetActiveRelay(const std::string& relay); 41void BtfSetActiveRelay(const std::string& relay);
42 42
43// Optional Tor hidden-service endpoint for this node's normal P2P listener.
44// Set by /onionservice=HOST.onion:PORT. The endpoint is signed into this node's
45// .btf descriptor, so peers with -tor can dial it directly before falling back
46// to rendezvous relays.
47bool BtfSetLocalOnionEndpoint(const std::string& endpoint, std::string& errOut);
48std::string BtfLocalOnionEndpoint();
49
43// If set (via /announcerelay=host:port), this node announces that relay on 50// If set (via /announcerelay=host:port), this node announces that relay on
44// Nostr so other nodes discover it automatically -- no manual seed-list edit. 51// Nostr so other nodes discover it automatically -- no manual seed-list edit.
45extern std::string strBtfAnnounceRelay; 52extern std::string strBtfAnnounceRelay;
@@ -102,12 +109,15 @@ void* BtfSecpContext();
102// self-certified descriptor and return its meeting node ("host:port") and 109// self-certified descriptor and return its meeting node ("host:port") and
103// x25519 public key for the end-to-end channel. 110// x25519 public key for the end-to-end channel.
104bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, 111bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
105 unsigned char enc_pub[32]); 112 unsigned char enc_pub[32],
113 std::string* onionHostPort = NULL);
106 114
107// Result of resolving one .btf address via BtfResolveMany. 115// Result of resolving one .btf address via BtfResolveMany.
108struct BtfResolvedPeer 116struct BtfResolvedPeer
109{ 117{
110 std::string meetingHostPort; 118 std::string meetingHostPort;
119 std::string onionHostPort;
120 std::string descriptor;
111 unsigned char enc_pub[32]; 121 unsigned char enc_pub[32];
112}; 122};
113 123
diff --git a/src/proxy.cpp b/src/proxy.cpp
index 7d30911..eed054c 100644
--- a/src/proxy.cpp
+++ b/src/proxy.cpp
@@ -13,6 +13,7 @@ static bool g_fTorProxy = false;
13static std::string g_socks5Host; 13static std::string g_socks5Host;
14static unsigned short g_socks5Port = 0; 14static unsigned short g_socks5Port = 0;
15static const char* DEFAULT_TOR_SOCKS5_PROXY = "127.0.0.1:9050"; 15static const char* DEFAULT_TOR_SOCKS5_PROXY = "127.0.0.1:9050";
16static const int TOR_CONNECT_TIMEOUT_SECS = 120;
16 17
17static void SetSocketTimeout(SOCKET s, int nTimeoutSecs) 18static void SetSocketTimeout(SOCKET s, int nTimeoutSecs)
18{ 19{
@@ -297,10 +298,25 @@ static bool Socks5Connect(SOCKET s, const std::string& destHost,
297 298
298 unsigned char hello[3] = { 0x05, 0x01, 0x00 }; // SOCKS5, one method, no auth 299 unsigned char hello[3] = { 0x05, 0x01, 0x00 }; // SOCKS5, one method, no auth
299 if (!WriteN(s, hello, sizeof(hello))) 300 if (!WriteN(s, hello, sizeof(hello)))
301 {
302 error("SOCKS5 hello write failed for %s:%u\n",
303 destHost.c_str(), (unsigned)destPort);
300 return false; 304 return false;
305 }
301 unsigned char choice[2] = { 0, 0 }; 306 unsigned char choice[2] = { 0, 0 };
302 if (!ReadN(s, choice, sizeof(choice)) || choice[0] != 0x05 || choice[1] != 0x00) 307 if (!ReadN(s, choice, sizeof(choice)))
308 {
309 error("SOCKS5 method selection read failed for %s:%u\n",
310 destHost.c_str(), (unsigned)destPort);
311 return false;
312 }
313 if (choice[0] != 0x05 || choice[1] != 0x00)
314 {
315 error("SOCKS5 proxy rejected no-auth method for %s:%u (version=0x%02x method=0x%02x)\n",
316 destHost.c_str(), (unsigned)destPort,
317 (unsigned)choice[0], (unsigned)choice[1]);
303 return false; 318 return false;
319 }
304 320
305 std::vector<unsigned char> req; 321 std::vector<unsigned char> req;
306 req.reserve(7 + destHost.size()); 322 req.reserve(7 + destHost.size());
@@ -313,11 +329,25 @@ static bool Socks5Connect(SOCKET s, const std::string& destHost,
313 req.push_back((unsigned char)(destPort >> 8)); 329 req.push_back((unsigned char)(destPort >> 8));
314 req.push_back((unsigned char)(destPort & 0xff)); 330 req.push_back((unsigned char)(destPort & 0xff));
315 if (!WriteN(s, &req[0], (int)req.size())) 331 if (!WriteN(s, &req[0], (int)req.size()))
332 {
333 error("SOCKS5 CONNECT request write failed for %s:%u\n",
334 destHost.c_str(), (unsigned)destPort);
316 return false; 335 return false;
336 }
317 337
318 unsigned char rep[4] = {0,0,0,0}; 338 unsigned char rep[4] = {0,0,0,0};
319 if (!ReadN(s, rep, sizeof(rep)) || rep[0] != 0x05) 339 if (!ReadN(s, rep, sizeof(rep)))
340 {
341 error("SOCKS5 CONNECT reply read failed for %s:%u\n",
342 destHost.c_str(), (unsigned)destPort);
320 return false; 343 return false;
344 }
345 if (rep[0] != 0x05)
346 {
347 error("SOCKS5 CONNECT reply has invalid version 0x%02x for %s:%u\n",
348 (unsigned)rep[0], destHost.c_str(), (unsigned)destPort);
349 return false;
350 }
321 if (rep[1] != 0x00) 351 if (rep[1] != 0x00)
322 { 352 {
323 error("SOCKS5 CONNECT to %s:%u failed with reply 0x%02x\n", 353 error("SOCKS5 CONNECT to %s:%u failed with reply 0x%02x\n",
@@ -335,10 +365,20 @@ static bool Socks5Connect(SOCKET s, const std::string& destHost,
335 nAddr = len; 365 nAddr = len;
336 } 366 }
337 else 367 else
368 {
369 error("SOCKS5 CONNECT reply has invalid address type 0x%02x for %s:%u\n",
370 (unsigned)rep[3], destHost.c_str(), (unsigned)destPort);
338 return false; 371 return false;
372 }
339 373
340 std::vector<unsigned char> discard(nAddr + 2); 374 std::vector<unsigned char> discard(nAddr + 2);
341 return ReadN(s, &discard[0], (int)discard.size()); 375 if (!ReadN(s, &discard[0], (int)discard.size()))
376 {
377 error("SOCKS5 CONNECT bind address read failed for %s:%u\n",
378 destHost.c_str(), (unsigned)destPort);
379 return false;
380 }
381 return true;
342} 382}
343 383
344SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort, 384SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
@@ -347,9 +387,11 @@ SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
347 std::string proxyHost; 387 std::string proxyHost;
348 unsigned short proxyPort = 0; 388 unsigned short proxyPort = 0;
349 bool fProxy = false; 389 bool fProxy = false;
390 bool fTorProxy = false;
350 CRITICAL_BLOCK(cs_socks5) 391 CRITICAL_BLOCK(cs_socks5)
351 { 392 {
352 fProxy = g_fSocks5Proxy; 393 fProxy = g_fSocks5Proxy;
394 fTorProxy = g_fTorProxy;
353 proxyHost = g_socks5Host; 395 proxyHost = g_socks5Host;
354 proxyPort = g_socks5Port; 396 proxyPort = g_socks5Port;
355 } 397 }
@@ -365,6 +407,9 @@ SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
365 return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs); 407 return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs);
366 } 408 }
367 409
410 if (fTorProxy && nTimeoutSecs > 0 && nTimeoutSecs < TOR_CONNECT_TIMEOUT_SECS)
411 nTimeoutSecs = TOR_CONNECT_TIMEOUT_SECS;
412
368 SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs); 413 SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs);
369 if (s == INVALID_SOCKET) 414 if (s == INVALID_SOCKET)
370 return INVALID_SOCKET; 415 return INVALID_SOCKET;
diff --git a/src/readme.txt b/src/readme.txt
index 794fe20..752be66 100644
--- a/src/readme.txt
+++ b/src/readme.txt
@@ -43,12 +43,17 @@ Command-line options (in addition to the original ones)
43------------------------------------------------------- 43-------------------------------------------------------
44 /port=N listen on P2P port N (default 8433) 44 /port=N listen on P2P port N (default 8433)
45 /socks=HOST:PORT 45 /socks=HOST:PORT
46 route outbound Nostr and .btf rendezvous dials through a 46 route outbound Nostr, .btf rendezvous, and direct onion peer
47 SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor 47 dials through a SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor
48 or /socks=[::1]:9050 for an IPv6 loopback proxy 48 or /socks=[::1]:9050 for an IPv6 loopback proxy
49 /tor[=HOST:PORT] 49 /tor[=HOST:PORT]
50 Tor mode for outbound Nostr and .btf rendezvous dials; 50 Tor mode for outbound Nostr, .btf rendezvous, and direct onion peer dials;
51 defaults to the local Tor SOCKS5 proxy at 127.0.0.1:9050 51 defaults to the local Tor SOCKS5 proxy at 127.0.0.1:9050
52 /managedtor[=PATH]
53 start Tor, create a v3 hidden service for this node, route outbound
54 discovery through it, and advertise the generated onion endpoint
55 Windows release packages that include tor/tor.exe start this mode
56 automatically; use /nomanagedtor to force the legacy relay path
52 /solomine mine without requiring a connected peer (local testing) 57 /solomine mine without requiring a connected peer (local testing)
53 /operator operator mode (run pool server) 58 /operator operator mode (run pool server)
54 /participant=ADDR 59 /participant=ADDR
@@ -72,6 +77,8 @@ Command-line options (in addition to the original ones)
72 override meeting relay list with one relay 77 override meeting relay list with one relay
73 /announcerelay=HOST:PORT 78 /announcerelay=HOST:PORT
74 announce this relay on Nostr for discovery 79 announce this relay on Nostr for discovery
80 /onionservice=HOST.onion:PORT
81 advertise this node's Tor hidden service in its signed .btf descriptor
75 82
76 83
77Roadmap 84Roadmap
diff --git a/src/selftest.cpp b/src/selftest.cpp
index 034e1f7..9f7106f 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -6,10 +6,12 @@
6// and against a temporary data directory. 6// and against a temporary data directory.
7 7
8#include "headers_core.h" 8#include "headers_core.h"
9#include "btfaddr.h"
9#include "bip32.h" 10#include "bip32.h"
10#include "proxy.h" 11#include "proxy.h"
11#include "selftest.h" 12#include "selftest.h"
12#include "sockcount.h" 13#include "sockcount.h"
14#include "tor.h"
13#include "walletcmd.h" 15#include "walletcmd.h"
14 16
15extern int RunPoolStratumSelfTest(); 17extern int RunPoolStratumSelfTest();
@@ -587,6 +589,34 @@ static int RunWalletHDSelfTest()
587 "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; 589 "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1;
588 nFail += Check(audit.nLegacyImmatureTx == 1, 590 nFail += Check(audit.nLegacyImmatureTx == 1,
589 "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1; 591 "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1;
592
593 CKey keyLegacyHD11;
594 nSchemaForLegacyAudit = nHDKeySchema;
595 nHDKeySchema = HD_SCHEMA_LEGACY;
596 if (!DeriveHDKey(11, keyLegacyHD11, strError))
597 throw std::runtime_error("legacy compatibility index 11 derivation failed: " + strError);
598 nHDKeySchema = nSchemaForLegacyAudit;
599 if (!AddKey(keyLegacyHD11))
600 throw std::runtime_error("could not store the legacy compatibility index 11 key");
601
602 std::map<unsigned int, std::vector<unsigned char> > mapLegacyProbe;
603 mapLegacyProbe[0] = keyAuditLegacyHD.GetPubKey();
604 mapLegacyProbe[11] = keyLegacyHD11.GetPubKey();
605
606 CWalletTx wtxLegacyHD11;
607 wtxLegacyHD11.vout.push_back(CTxOut(19 * COIN, CScript() << keyLegacyHD11.GetPubKey() << OP_CHECKSIG));
608 CRITICAL_BLOCK(cs_mapWallet)
609 {
610 mapWallet.clear();
611 mapWallet[wtxLegacyHD11.GetHash()] = wtxLegacyHD11;
612 }
613 nFail += Check(WalletLastUsedPubKeyIndexNext(mapLegacyProbe) == 12,
614 "restore collapses legacy hdnext to the last used compatibility index plus one") ? 0 : 1;
615
616 CRITICAL_BLOCK(cs_mapWallet)
617 mapWallet.clear();
618 nFail += Check(WalletLastUsedPubKeyIndexNext(mapLegacyProbe) == 0,
619 "restore does not persist legacy hdnext at the scan depth when no compatibility key was used") ? 0 : 1;
590 } 620 }
591 catch (const std::exception& e) 621 catch (const std::exception& e)
592 { 622 {
@@ -1505,6 +1535,14 @@ static int RunSocks5ProxySelfTest()
1505 !BtfIsTorOnionHost("relay.example") && 1535 !BtfIsTorOnionHost("relay.example") &&
1506 !BtfIsTorOnionHost(".onion"), 1536 !BtfIsTorOnionHost(".onion"),
1507 "detects .onion hosts without accepting lookalikes") ? 0 : 1; 1537 "detects .onion hosts without accepting lookalikes") ? 0 : 1;
1538 std::string onion;
1539 nFail += Check(btf::NormalizeOnionEndpoint("ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCD.onion.:8433", onion) &&
1540 onion == "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:8433",
1541 "normalizes v3 onion peer endpoint") ? 0 : 1;
1542 nFail += Check(!btf::NormalizeOnionEndpoint("short.onion:8433", onion) &&
1543 !btf::NormalizeOnionEndpoint("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:0", onion) &&
1544 !btf::NormalizeOnionEndpoint("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc1.onion:8433", onion),
1545 "rejects malformed onion peer endpoints") ? 0 : 1;
1508 1546
1509 BtfClearSocks5Proxy(); 1547 BtfClearSocks5Proxy();
1510 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1; 1548 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1;
@@ -1554,6 +1592,41 @@ static int RunSocks5ProxySelfTest()
1554 return nFail == 0 ? 0 : 1; 1592 return nFail == 0 ? 0 : 1;
1555} 1593}
1556 1594
1595static int RunManagedTorSelfTest()
1596{
1597 printf("managed-tor self-test\n");
1598 int nFail = 0;
1599
1600 std::string torrc = BtfBuildManagedTorrcForTest("C:/Bitflash Managed Tor/data",
1601 "C:/Bitflash Managed Tor/onion-service",
1602 19050, 19051, 8433);
1603 nFail += Check(torrc.find("DataDirectory \"C:/Bitflash Managed Tor/data\"") != std::string::npos,
1604 "quotes the Tor data directory") ? 0 : 1;
1605 nFail += Check(torrc.find("SocksPort 127.0.0.1:19050") != std::string::npos,
1606 "binds SOCKS5 to localhost only") ? 0 : 1;
1607 nFail += Check(torrc.find("ControlPort 127.0.0.1:19051") != std::string::npos &&
1608 torrc.find("CookieAuthentication 1") != std::string::npos,
1609 "binds an authenticated Tor control port to localhost only") ? 0 : 1;
1610 nFail += Check(torrc.find("IsolateSOCKSAuth") != std::string::npos &&
1611 torrc.find("IsolateClientAddr") != std::string::npos &&
1612 torrc.find("IsolateDestAddr") != std::string::npos &&
1613 torrc.find("IsolateDestPort") != std::string::npos,
1614 "enables strict Tor stream isolation") ? 0 : 1;
1615 nFail += Check(torrc.find("HiddenServiceDir \"C:/Bitflash Managed Tor/onion-service\"") != std::string::npos,
1616 "writes a hidden service directory") ? 0 : 1;
1617 nFail += Check(torrc.find("HiddenServiceVersion 3") != std::string::npos,
1618 "requests a v3 onion service") ? 0 : 1;
1619 nFail += Check(torrc.find("HiddenServicePort 8433 127.0.0.1:8433") != std::string::npos,
1620 "maps the onion service to the Bitflash P2P listener") ? 0 : 1;
1621 nFail += Check(BtfManagedTorStatus() == "disabled",
1622 "managed Tor starts disabled") ? 0 : 1;
1623
1624 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1625 nFail, nFail == 1 ? "" : "s");
1626 fflush(stdout);
1627 return nFail == 0 ? 0 : 1;
1628}
1629
1557int RunSelfTest(const std::string& name) 1630int RunSelfTest(const std::string& name)
1558{ 1631{
1559 AttachTerminal(); 1632 AttachTerminal();
@@ -1580,8 +1653,10 @@ int RunSelfTest(const std::string& name)
1580 return RunParseMoneySelfTest(); 1653 return RunParseMoneySelfTest();
1581 if (name == "socks5-proxy") 1654 if (name == "socks5-proxy")
1582 return RunSocks5ProxySelfTest(); 1655 return RunSocks5ProxySelfTest();
1656 if (name == "managed-tor")
1657 return RunManagedTorSelfTest();
1583 1658
1584 printf("Unknown self-test '%s'\n", name.c_str()); 1659 printf("Unknown self-test '%s'\n", name.c_str());
1585 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, payout-ledger, parse-money, socks5-proxy\n"); 1660 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, payout-ledger, parse-money, socks5-proxy, managed-tor\n");
1586 return 1; 1661 return 1;
1587} 1662}
diff --git a/src/sockcount.h b/src/sockcount.h
index 5147f55..a63c6be 100644
--- a/src/sockcount.h
+++ b/src/sockcount.h
@@ -63,6 +63,7 @@ enum
63 SOCK_PAIR_APP, // loopback pair: the app end handed to CNode 63 SOCK_PAIR_APP, // loopback pair: the app end handed to CNode
64 SOCK_PAIR_PUMP, // loopback pair: the pump end 64 SOCK_PAIR_PUMP, // loopback pair: the pump end
65 SOCK_NOSTR, // outbound to a Nostr relay 65 SOCK_NOSTR, // outbound to a Nostr relay
66 SOCK_ONION_PEER, // direct outbound to a peer's Tor hidden service
66 SOCK_STRATUM_BRIDGE, // local Stratum bridge listener/miner sockets 67 SOCK_STRATUM_BRIDGE, // local Stratum bridge listener/miner sockets
67 SOCK_SITES 68 SOCK_SITES
68}; 69};
diff --git a/src/test_btfaddr.cpp b/src/test_btfaddr.cpp
index b40dcc1..8de426e 100644
--- a/src/test_btfaddr.cpp
+++ b/src/test_btfaddr.cpp
@@ -86,6 +86,7 @@ int main()
86 } 86 }
87 87
88 const std::string ENC_HEX = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899"; 88 const std::string ENC_HEX = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899";
89 const std::string ONION = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:8433";
89 90
90 // helper: fresh keypair -> (seckey, xonly pubkey) 91 // helper: fresh keypair -> (seckey, xonly pubkey)
91 auto newKey = [&](unsigned char sk[32], unsigned char pk[32]) { 92 auto newKey = [&](unsigned char sk[32], unsigned char pk[32]) {
@@ -108,6 +109,17 @@ int main()
108 CHECK(!btf::VerifyDescriptor(ctx, d, pk2, bad), "different key rejected"); 109 CHECK(!btf::VerifyDescriptor(ctx, d, pk2, bad), "different key rejected");
109 } 110 }
110 111
112 printf("descriptor_signs_optional_onion_endpoint\n");
113 {
114 unsigned char sk[32], pk[32]; newKey(sk, pk);
115 std::string d = btf::SignDescriptor(ctx, sk, ENC_HEX, "meeting-node-abc", 1000, ONION);
116 CHECK(!d.empty(), "sign produced onion descriptor");
117 btf::Descriptor got;
118 CHECK(btf::VerifyDescriptor(ctx, d, pk, got), "onion descriptor verifies");
119 CHECK(got.meeting_node == "meeting-node-abc" && got.enc == ENC_HEX &&
120 got.created == 1000 && got.onion == ONION, "onion fields match");
121 }
122
111 printf("descriptor_rejects_tampering\n"); 123 printf("descriptor_rejects_tampering\n");
112 { 124 {
113 unsigned char sk[32], pk[32]; newKey(sk, pk); 125 unsigned char sk[32], pk[32]; newKey(sk, pk);
@@ -117,6 +129,16 @@ int main()
117 CHECK(!btf::VerifyDescriptor(ctx, j.dump(), pk, o), "tampered meeting_node rejected"); 129 CHECK(!btf::VerifyDescriptor(ctx, j.dump(), pk, o), "tampered meeting_node rejected");
118 json j2 = json::parse(d); j2["enc"] = "00000000000000000000000000000000000000000000000000000000deadbeef"; 130 json j2 = json::parse(d); j2["enc"] = "00000000000000000000000000000000000000000000000000000000deadbeef";
119 CHECK(!btf::VerifyDescriptor(ctx, j2.dump(), pk, o), "tampered enc rejected"); 131 CHECK(!btf::VerifyDescriptor(ctx, j2.dump(), pk, o), "tampered enc rejected");
132
133 std::string d3 = btf::SignDescriptor(ctx, sk, ENC_HEX, "node-1", 5, ONION);
134 json j3 = json::parse(d3);
135 j3["onion"] = "bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcde.onion:8433";
136 CHECK(btf::VerifyDescriptor(ctx, j3.dump(), pk, o) && o.onion.empty(),
137 "tampered onion ignored rather than trusted");
138 json j4 = json::parse(d3);
139 j4["sig3"] = std::string(128, '0');
140 CHECK(btf::VerifyDescriptor(ctx, j4.dump(), pk, o) && o.onion.empty(),
141 "bad onion signature ignored rather than trusted");
120 } 142 }
121 143
122 printf("full_resolve_chain_from_address\n"); 144 printf("full_resolve_chain_from_address\n");
diff --git a/src/tor.cpp b/src/tor.cpp
new file mode 100644
index 0000000..20b4577
--- /dev/null
+++ b/src/tor.cpp
@@ -0,0 +1,881 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3
4#include "headers_core.h"
5#include "tor.h"
6#include "nostr.h"
7#include "proxy.h"
8
9#include <errno.h>
10
11#ifndef _WIN32
12#include <signal.h>
13#include <sys/wait.h>
14#endif
15
16static CCriticalSection cs_managedTor;
17static bool g_fManagedTor = false;
18static bool g_fManagedTorStopping = false;
19static unsigned short g_managedTorSocksPort = 0;
20static unsigned short g_managedTorControlPort = 0;
21static unsigned short g_managedTorP2PPort = 0;
22static std::string g_managedTorPath;
23static std::string g_managedTorDataDir;
24static std::string g_managedTorHiddenServiceDir;
25static std::string g_managedTorOnion;
26static std::string g_managedTorStatus;
27#ifdef _WIN32
28static PROCESS_INFORMATION g_managedTorProcess;
29#else
30static pid_t g_managedTorPid = -1;
31#endif
32
33static std::string PathJoin(const std::string& a, const std::string& b)
34{
35 if (a.empty())
36 return b;
37 char last = a[a.size() - 1];
38 if (last == '/' || last == '\\')
39 return a + b;
40 return a + "/" + b;
41}
42
43static std::string NormalizeTorrcPath(std::string path)
44{
45 for (size_t i = 0; i < path.size(); i++)
46 if (path[i] == '\\')
47 path[i] = '/';
48 return path;
49}
50
51static std::string QuoteTorrcPath(const std::string& path)
52{
53 std::string out = "\"";
54 for (size_t i = 0; i < path.size(); i++)
55 {
56 if (path[i] == '"' || path[i] == '\\')
57 out += '\\';
58 out += path[i];
59 }
60 out += "\"";
61 return out;
62}
63
64static std::string QuoteCommandArg(const std::string& arg)
65{
66 std::string out = "\"";
67 for (size_t i = 0; i < arg.size(); i++)
68 {
69 if (arg[i] == '"')
70 out += '\\';
71 out += arg[i];
72 }
73 out += "\"";
74 return out;
75}
76
77static bool EnsureDir(const std::string& path, std::string& errOut)
78{
79 if (path.empty())
80 {
81 errOut = "directory path is empty";
82 return false;
83 }
84#ifdef _WIN32
85 DWORD attr = GetFileAttributesA(path.c_str());
86 if (attr != INVALID_FILE_ATTRIBUTES)
87 {
88 if (attr & FILE_ATTRIBUTE_DIRECTORY)
89 return true;
90 errOut = strprintf("%s exists and is not a directory", path.c_str());
91 return false;
92 }
93 if (CreateDirectoryA(path.c_str(), NULL))
94 return true;
95 DWORD err = GetLastError();
96 if (err == ERROR_ALREADY_EXISTS)
97 return true;
98 errOut = strprintf("could not create %s (error %lu)", path.c_str(), (unsigned long)err);
99 return false;
100#else
101 struct stat st;
102 if (stat(path.c_str(), &st) == 0)
103 {
104 if (S_ISDIR(st.st_mode))
105 {
106 chmod(path.c_str(), 0700);
107 return true;
108 }
109 errOut = strprintf("%s exists and is not a directory", path.c_str());
110 return false;
111 }
112 if (mkdir(path.c_str(), 0700) == 0)
113 return true;
114 if (errno == EEXIST)
115 {
116 if (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode))
117 {
118 chmod(path.c_str(), 0700);
119 return true;
120 }
121 }
122 errOut = strprintf("could not create %s: %s", path.c_str(), strerror(errno));
123 return false;
124#endif
125}
126
127static bool WriteTextFile(const std::string& path, const std::string& data,
128 std::string& errOut)
129{
130 std::string tmp = path + ".tmp";
131 FILE* f = fopen(tmp.c_str(), "wb");
132 if (!f)
133 {
134 errOut = strprintf("could not write %s", tmp.c_str());
135 return false;
136 }
137 bool ok = fwrite(data.data(), 1, data.size(), f) == data.size();
138 if (fclose(f) != 0)
139 ok = false;
140 if (!ok)
141 {
142 remove(tmp.c_str());
143 errOut = strprintf("could not finish writing %s", tmp.c_str());
144 return false;
145 }
146#ifdef _WIN32
147 if (!MoveFileExA(tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
148 {
149 remove(tmp.c_str());
150 errOut = strprintf("could not install %s atomically (error %lu)",
151 path.c_str(), (unsigned long)GetLastError());
152 return false;
153 }
154#else
155 if (rename(tmp.c_str(), path.c_str()) != 0)
156 {
157 remove(tmp.c_str());
158 errOut = strprintf("could not install %s atomically: %s",
159 path.c_str(), strerror(errno));
160 return false;
161 }
162#endif
163 return true;
164}
165
166static bool ReadBinaryFile(const std::string& path, std::vector<unsigned char>& data,
167 std::string& errOut)
168{
169 data.clear();
170 FILE* f = fopen(path.c_str(), "rb");
171 if (!f)
172 {
173 errOut = strprintf("could not read %s", path.c_str());
174 return false;
175 }
176 unsigned char buf[256];
177 for (;;)
178 {
179 size_t n = fread(buf, 1, sizeof(buf), f);
180 if (n > 0)
181 data.insert(data.end(), buf, buf + n);
182 if (n < sizeof(buf))
183 {
184 if (ferror(f))
185 {
186 fclose(f);
187 errOut = strprintf("could not finish reading %s", path.c_str());
188 return false;
189 }
190 break;
191 }
192 if (data.size() > 1024)
193 {
194 fclose(f);
195 errOut = strprintf("%s is unexpectedly large", path.c_str());
196 return false;
197 }
198 }
199 fclose(f);
200 if (data.empty())
201 {
202 errOut = strprintf("%s is empty", path.c_str());
203 return false;
204 }
205 return true;
206}
207
208static void SetSocketTimeouts(SOCKET s, int millis)
209{
210#ifdef _WIN32
211 DWORD timeout = millis;
212 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
213 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout));
214#else
215 struct timeval tv;
216 tv.tv_sec = millis / 1000;
217 tv.tv_usec = (millis % 1000) * 1000;
218 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
219 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
220#endif
221}
222
223static bool SendAll(SOCKET s, const std::string& data)
224{
225 const char* p = data.data();
226 int left = (int)data.size();
227 while (left > 0)
228 {
229 int n = send(s, p, left, 0);
230 if (n <= 0)
231 return false;
232 p += n;
233 left -= n;
234 }
235 return true;
236}
237
238static bool RecvControlLine(SOCKET s, std::string& line)
239{
240 line.clear();
241 while (line.size() < 4096)
242 {
243 char ch = 0;
244 int n = recv(s, &ch, 1, 0);
245 if (n <= 0)
246 return false;
247 if (ch == '\n')
248 return true;
249 if (ch != '\r')
250 line += ch;
251 }
252 return false;
253}
254
255static SOCKET ConnectLoopback(unsigned short port, std::string& errOut)
256{
257#ifdef _WIN32
258 WSADATA wsadata;
259 int ret = WSAStartup(MAKEWORD(2,2), &wsadata);
260 if (ret != NO_ERROR)
261 {
262 errOut = strprintf("could not start Winsock for Tor control shutdown (WSAStartup returned %d)", ret);
263 return INVALID_SOCKET;
264 }
265#endif
266 SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
267 if (s == INVALID_SOCKET)
268 {
269 errOut = "could not create Tor control socket";
270#ifdef _WIN32
271 WSACleanup();
272#endif
273 return INVALID_SOCKET;
274 }
275 SetSocketTimeouts(s, 3000);
276 struct sockaddr_in addr;
277 memset(&addr, 0, sizeof(addr));
278 addr.sin_family = AF_INET;
279 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
280 addr.sin_port = htons(port);
281 if (connect(s, (struct sockaddr*)&addr, sizeof(addr)) != 0)
282 {
283 closesocket(s);
284 errOut = strprintf("could not connect to Tor control port 127.0.0.1:%u", (unsigned)port);
285#ifdef _WIN32
286 WSACleanup();
287#endif
288 return INVALID_SOCKET;
289 }
290 return s;
291}
292
293static void CloseLoopback(SOCKET s)
294{
295 if (s != INVALID_SOCKET)
296 closesocket(s);
297#ifdef _WIN32
298 WSACleanup();
299#endif
300}
301
302static bool ReadPositiveTorControlReply(SOCKET s, std::string& errOut)
303{
304 std::string line;
305 if (!RecvControlLine(s, line))
306 {
307 errOut = "Tor control port closed before replying";
308 return false;
309 }
310 if (line.size() >= 3 && line.substr(0, 3) == "250")
311 return true;
312 errOut = strprintf("Tor control command failed: %s", line.c_str());
313 return false;
314}
315
316static bool RequestManagedTorShutdown(std::string& errOut)
317{
318 std::string dataDir;
319 unsigned short controlPort = 0;
320 CRITICAL_BLOCK(cs_managedTor)
321 {
322 dataDir = g_managedTorDataDir;
323 controlPort = g_managedTorControlPort;
324 }
325 if (controlPort == 0 || dataDir.empty())
326 {
327 errOut = "Tor control port is not available";
328 return false;
329 }
330
331 std::vector<unsigned char> cookie;
332 if (!ReadBinaryFile(PathJoin(dataDir, "control_auth_cookie"), cookie, errOut))
333 return false;
334
335 SOCKET s = ConnectLoopback(controlPort, errOut);
336 if (s == INVALID_SOCKET)
337 return false;
338
339 bool ok = false;
340 std::string auth = "AUTHENTICATE " + HexStr(cookie.begin(), cookie.end(), false) + "\r\n";
341 if (!SendAll(s, auth) || !ReadPositiveTorControlReply(s, errOut))
342 goto done;
343
344 if (!SendAll(s, "SIGNAL SHUTDOWN\r\n"))
345 {
346 errOut = "could not send Tor shutdown signal";
347 goto done;
348 }
349
350 {
351 std::string reply;
352 if (RecvControlLine(s, reply) && !(reply.size() >= 3 && reply.substr(0, 3) == "250"))
353 {
354 errOut = strprintf("Tor rejected shutdown signal: %s", reply.c_str());
355 goto done;
356 }
357 }
358 ok = true;
359
360done:
361 CloseLoopback(s);
362 return ok;
363}
364
365static bool ReadOneLine(const std::string& path, std::string& out)
366{
367 out.clear();
368 FILE* f = fopen(path.c_str(), "rb");
369 if (!f)
370 return false;
371 char buf[256] = {0};
372 if (!fgets(buf, sizeof(buf), f))
373 {
374 fclose(f);
375 return false;
376 }
377 fclose(f);
378 out = buf;
379 while (!out.empty() && (out[out.size() - 1] == '\n' || out[out.size() - 1] == '\r' ||
380 out[out.size() - 1] == ' ' || out[out.size() - 1] == '\t'))
381 out.erase(out.size() - 1);
382 return !out.empty();
383}
384
385static bool PickLocalPort(unsigned short& portOut, std::string& errOut)
386{
387 portOut = 0;
388 bool ok = false;
389#ifdef _WIN32
390 WSADATA wsadata;
391 int ret = WSAStartup(MAKEWORD(2,2), &wsadata);
392 if (ret != NO_ERROR)
393 {
394 errOut = strprintf("could not start Winsock for local port probe (WSAStartup returned %d)", ret);
395 return false;
396 }
397#endif
398 struct sockaddr_in addr;
399 socklen_t len = sizeof(addr);
400 SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
401 if (s == INVALID_SOCKET)
402 {
403 errOut = "could not create local port probe socket";
404 goto done;
405 }
406 memset(&addr, 0, sizeof(addr));
407 addr.sin_family = AF_INET;
408 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
409 addr.sin_port = 0;
410 if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) != 0)
411 {
412 errOut = "could not bind local port probe socket";
413 goto done;
414 }
415 if (getsockname(s, (struct sockaddr*)&addr, &len) != 0)
416 {
417 errOut = "could not read local port probe socket";
418 goto done;
419 }
420 portOut = ntohs(addr.sin_port);
421 if (portOut == 0)
422 {
423 errOut = "local port probe returned port zero";
424 goto done;
425 }
426 ok = true;
427done:
428 if (s != INVALID_SOCKET)
429 closesocket(s);
430#ifdef _WIN32
431 WSACleanup();
432#endif
433 return ok;
434}
435
436static std::string ExecutableDir()
437{
438#ifdef _WIN32
439 char path[MAX_PATH + 1] = {0};
440 DWORD n = GetModuleFileNameA(NULL, path, MAX_PATH);
441 if (n == 0 || n >= MAX_PATH)
442 return ".";
443 std::string s = path;
444 size_t slash = s.find_last_of("\\/");
445 if (slash == std::string::npos)
446 return ".";
447 return s.substr(0, slash);
448#else
449 char path[4096] = {0};
450 ssize_t n = readlink("/proc/self/exe", path, sizeof(path) - 1);
451 if (n <= 0)
452 return ".";
453 path[n] = 0;
454 std::string s = path;
455 size_t slash = s.find_last_of('/');
456 if (slash == std::string::npos)
457 return ".";
458 return s.substr(0, slash);
459#endif
460}
461
462static bool FileIsExecutableCandidate(const std::string& path)
463{
464#ifdef _WIN32
465 DWORD attr = GetFileAttributesA(path.c_str());
466 return attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY);
467#else
468 return access(path.c_str(), X_OK) == 0;
469#endif
470}
471
472static std::string ResolveTorPath(const std::string& torPathOpt)
473{
474 if (!torPathOpt.empty())
475 return torPathOpt;
476
477 std::vector<std::string> candidates;
478 std::string exeDir = ExecutableDir();
479#ifdef _WIN32
480 candidates.push_back(PathJoin(exeDir, "tor/tor.exe"));
481 candidates.push_back(PathJoin(exeDir, "tor.exe"));
482 candidates.push_back("tor.exe");
483#else
484 candidates.push_back(PathJoin(exeDir, "tor/tor"));
485 candidates.push_back(PathJoin(exeDir, "tor"));
486 candidates.push_back("tor");
487#endif
488 for (size_t i = 0; i < candidates.size(); i++)
489 {
490 if (candidates[i].find('/') == std::string::npos &&
491 candidates[i].find('\\') == std::string::npos)
492 return candidates[i]; // Let the OS search PATH.
493 if (FileIsExecutableCandidate(candidates[i]))
494 return candidates[i];
495 }
496#ifdef _WIN32
497 return "tor.exe";
498#else
499 return "tor";
500#endif
501}
502
503bool BtfBundledTorPath(std::string& torPathOut)
504{
505 torPathOut.clear();
506 std::string exeDir = ExecutableDir();
507#ifdef _WIN32
508 std::string path = PathJoin(exeDir, "tor/tor.exe");
509#else
510 std::string path = PathJoin(exeDir, "tor/tor");
511#endif
512 if (!FileIsExecutableCandidate(path))
513 return false;
514 torPathOut = path;
515 return true;
516}
517
518std::string BtfBuildManagedTorrcForTest(const std::string& dataDir,
519 const std::string& hiddenServiceDir,
520 unsigned short socksPort,
521 unsigned short controlPort,
522 unsigned short p2pPort)
523{
524 std::string torData = NormalizeTorrcPath(dataDir);
525 std::string hsDir = NormalizeTorrcPath(hiddenServiceDir);
526 std::string s;
527 s += "# Generated by Bitflash. Edit only while Bitflash is not managing Tor.\n";
528 s += "DataDirectory " + QuoteTorrcPath(torData) + "\n";
529 s += strprintf("SocksPort 127.0.0.1:%u IsolateSOCKSAuth IsolateClientAddr IsolateDestAddr IsolateDestPort\n",
530 (unsigned)socksPort);
531 s += strprintf("ControlPort 127.0.0.1:%u\n", (unsigned)controlPort);
532 s += "CookieAuthentication 1\n";
533 s += "HiddenServiceDir " + QuoteTorrcPath(hsDir) + "\n";
534 s += "HiddenServiceVersion 3\n";
535 s += strprintf("HiddenServicePort %u 127.0.0.1:%u\n",
536 (unsigned)p2pPort, (unsigned)p2pPort);
537 return s;
538}
539
540static bool StartTorProcess(const std::string& torPath, const std::string& torrcPath,
541 std::string& errOut)
542{
543#ifdef _WIN32
544 memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess));
545 STARTUPINFOA si;
546 memset(&si, 0, sizeof(si));
547 si.cb = sizeof(si);
548 std::string cmd = QuoteCommandArg(torPath) + " -f " + QuoteCommandArg(torrcPath);
549 std::vector<char> mutableCmd(cmd.begin(), cmd.end());
550 mutableCmd.push_back(0);
551 if (!CreateProcessA(NULL, &mutableCmd[0], NULL, NULL, FALSE, CREATE_NO_WINDOW,
552 NULL, NULL, &si, &g_managedTorProcess))
553 {
554 errOut = strprintf("could not start Tor process %s (error %lu)",
555 torPath.c_str(), (unsigned long)GetLastError());
556 return false;
557 }
558 return true;
559#else
560 g_managedTorPid = fork();
561 if (g_managedTorPid < 0)
562 {
563 errOut = strprintf("could not fork Tor process: %s", strerror(errno));
564 return false;
565 }
566 if (g_managedTorPid == 0)
567 {
568 execlp(torPath.c_str(), torPath.c_str(), "-f", torrcPath.c_str(), (char*)NULL);
569 _exit(127);
570 }
571 return true;
572#endif
573}
574
575static bool ManagedTorProcessAlive(bool& fExited, int& nExitCode)
576{
577 fExited = false;
578 nExitCode = 0;
579#ifdef _WIN32
580 HANDLE hProcess = NULL;
581 CRITICAL_BLOCK(cs_managedTor)
582 hProcess = g_managedTorProcess.hProcess;
583 if (!hProcess)
584 return false;
585 DWORD code = 0;
586 if (!GetExitCodeProcess(hProcess, &code))
587 return false;
588 if (code != STILL_ACTIVE)
589 {
590 fExited = true;
591 nExitCode = (int)code;
592 return false;
593 }
594 return true;
595#else
596 pid_t pid = -1;
597 CRITICAL_BLOCK(cs_managedTor)
598 pid = g_managedTorPid;
599 if (pid <= 0)
600 return false;
601 int status = 0;
602 pid_t r = waitpid(pid, &status, WNOHANG);
603 if (r == 0)
604 return true;
605 if (r == pid)
606 {
607 fExited = true;
608 if (WIFEXITED(status))
609 nExitCode = WEXITSTATUS(status);
610 else if (WIFSIGNALED(status))
611 nExitCode = 128 + WTERMSIG(status);
612 CRITICAL_BLOCK(cs_managedTor)
613 if (g_managedTorPid == pid)
614 g_managedTorPid = -1;
615 }
616 return false;
617#endif
618}
619
620static bool ValidateManagedTorStartup(std::string& errOut)
621{
622 Sleep(500);
623 bool fExited = false;
624 int nExitCode = 0;
625 if (ManagedTorProcessAlive(fExited, nExitCode))
626 return true;
627 if (fExited)
628 errOut = strprintf("Tor exited during startup with code %d", nExitCode);
629 else
630 errOut = "Tor process is not running after startup";
631 return false;
632}
633
634static void MarkManagedTorDead(int nExitCode)
635{
636 BtfStopManagedTor();
637 CRITICAL_BLOCK(cs_managedTor)
638 g_managedTorStatus = strprintf("stopped unexpectedly, Tor exit code %d", nExitCode);
639}
640
641static void ThreadManagedTorWatchdog(void*)
642{
643 for (;;)
644 {
645 Sleep(5000);
646 CRITICAL_BLOCK(cs_managedTor)
647 {
648 if (!g_fManagedTor || g_fManagedTorStopping || fShutdown)
649 return;
650 }
651
652 bool fExited = false;
653 int nExitCode = 0;
654 if (!ManagedTorProcessAlive(fExited, nExitCode))
655 {
656 if (fExited)
657 MarkManagedTorDead(nExitCode);
658 else
659 {
660 BtfStopManagedTor();
661 CRITICAL_BLOCK(cs_managedTor)
662 g_managedTorStatus = "stopped unexpectedly, Tor process handle is gone";
663 }
664 return;
665 }
666 }
667}
668
669static void ThreadManagedTorHostname(void*)
670{
671 std::string hostnamePath;
672 unsigned short p2pPort = 0;
673 CRITICAL_BLOCK(cs_managedTor)
674 {
675 hostnamePath = PathJoin(g_managedTorHiddenServiceDir, "hostname");
676 p2pPort = g_managedTorP2PPort;
677 }
678
679 for (int i = 0; i < 90 && !fShutdown; i++)
680 {
681 std::string onion;
682 if (ReadOneLine(hostnamePath, onion))
683 {
684 std::string endpoint = strprintf("%s:%u", onion.c_str(), (unsigned)p2pPort);
685 std::string err;
686 if (BtfSetLocalOnionEndpoint(endpoint, err))
687 {
688 CRITICAL_BLOCK(cs_managedTor)
689 {
690 g_managedTorOnion = BtfLocalOnionEndpoint();
691 g_managedTorStatus = strprintf("running, SOCKS5 127.0.0.1:%u, onion %s",
692 (unsigned)g_managedTorSocksPort,
693 g_managedTorOnion.c_str());
694 }
695 printf("Managed Tor hidden service ready: %s\n", BtfLocalOnionEndpoint().c_str());
696 return;
697 }
698 CRITICAL_BLOCK(cs_managedTor)
699 g_managedTorStatus = strprintf("running, but generated onion endpoint was rejected: %s",
700 err.c_str());
701 return;
702 }
703 Sleep(1000);
704 }
705
706 BtfStopManagedTor();
707 CRITICAL_BLOCK(cs_managedTor)
708 g_managedTorStatus = "failed: Tor did not create a hidden service hostname within 90s";
709}
710
711bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut)
712{
713 CRITICAL_BLOCK(cs_managedTor)
714 {
715 if (g_fManagedTor)
716 {
717 errOut = "managed Tor is already running";
718 return false;
719 }
720 }
721
722 std::string root = PathJoin(GetAppDir(), "managed-tor");
723 std::string dataDir = PathJoin(root, "data");
724 std::string hsDir = PathJoin(root, "onion-service");
725 std::string torrcPath = PathJoin(root, "torrc");
726 if (!EnsureDir(root, errOut) || !EnsureDir(dataDir, errOut) || !EnsureDir(hsDir, errOut))
727 return false;
728
729 unsigned short socksPort = 0;
730 if (!PickLocalPort(socksPort, errOut))
731 return false;
732 unsigned short controlPort = 0;
733 if (!PickLocalPort(controlPort, errOut))
734 return false;
735
736 unsigned short p2pPort = ntohs(nListenPort);
737 std::string torrc = BtfBuildManagedTorrcForTest(dataDir, hsDir, socksPort,
738 controlPort, p2pPort);
739 if (!WriteTextFile(torrcPath, torrc, errOut))
740 return false;
741
742 std::string torPath = ResolveTorPath(torPathOpt);
743 {
744 CRITICAL_BLOCK(cs_managedTor)
745 {
746 g_fManagedTor = true;
747 g_fManagedTorStopping = false;
748 g_managedTorPath = torPath;
749 g_managedTorDataDir = dataDir;
750 g_managedTorHiddenServiceDir = hsDir;
751 g_managedTorSocksPort = socksPort;
752 g_managedTorControlPort = controlPort;
753 g_managedTorP2PPort = p2pPort;
754 g_managedTorOnion.clear();
755 g_managedTorStatus = strprintf("starting %s, SOCKS5 127.0.0.1:%u",
756 torPath.c_str(), (unsigned)socksPort);
757 }
758 }
759
760 if (!StartTorProcess(torPath, torrcPath, errOut))
761 {
762 CRITICAL_BLOCK(cs_managedTor)
763 {
764 g_fManagedTor = false;
765 g_managedTorStatus = strprintf("failed: %s", errOut.c_str());
766 }
767 return false;
768 }
769 if (!ValidateManagedTorStartup(errOut))
770 {
771 BtfStopManagedTor();
772 CRITICAL_BLOCK(cs_managedTor)
773 g_managedTorStatus = strprintf("failed: %s", errOut.c_str());
774 return false;
775 }
776
777 std::string proxyErr;
778 if (!BtfEnableTorProxy(strprintf("127.0.0.1:%u", (unsigned)socksPort), proxyErr))
779 {
780 BtfStopManagedTor();
781 errOut = strprintf("could not enable managed Tor SOCKS5 proxy: %s", proxyErr.c_str());
782 return false;
783 }
784
785 if (_beginthread(ThreadManagedTorHostname, 0, NULL) == (uintptr_t)-1)
786 {
787 BtfStopManagedTor();
788 errOut = "could not start managed Tor hostname watcher";
789 return false;
790 }
791 if (_beginthread(ThreadManagedTorWatchdog, 0, NULL) == (uintptr_t)-1)
792 {
793 BtfStopManagedTor();
794 errOut = "could not start managed Tor watchdog";
795 return false;
796 }
797
798 return true;
799}
800
801void BtfStopManagedTor()
802{
803 bool fWasRunning = false;
804 CRITICAL_BLOCK(cs_managedTor)
805 {
806 fWasRunning = g_fManagedTor;
807 if (!fWasRunning)
808 return;
809 g_fManagedTorStopping = true;
810 g_fManagedTor = false;
811 g_managedTorStatus = "stopping";
812 }
813#ifdef _WIN32
814 if (g_managedTorProcess.hProcess)
815 {
816 std::string shutdownErr;
817 bool fGraceful = RequestManagedTorShutdown(shutdownErr);
818 DWORD waitResult = WaitForSingleObject(g_managedTorProcess.hProcess,
819 fGraceful ? 10000 : 1000);
820 if (waitResult != WAIT_OBJECT_0)
821 {
822 TerminateProcess(g_managedTorProcess.hProcess, 0);
823 WaitForSingleObject(g_managedTorProcess.hProcess, 5000);
824 }
825 CloseHandle(g_managedTorProcess.hProcess);
826 CloseHandle(g_managedTorProcess.hThread);
827 memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess));
828 }
829#else
830 if (g_managedTorPid > 0)
831 {
832 std::string shutdownErr;
833 if (!RequestManagedTorShutdown(shutdownErr))
834 kill(g_managedTorPid, SIGTERM);
835 bool fExited = false;
836 for (int i = 0; i < 100; i++)
837 {
838 int status = 0;
839 pid_t r = waitpid(g_managedTorPid, &status, WNOHANG);
840 if (r == g_managedTorPid)
841 {
842 fExited = true;
843 break;
844 }
845 Sleep(100);
846 }
847 if (!fExited)
848 {
849 kill(g_managedTorPid, SIGKILL);
850 waitpid(g_managedTorPid, NULL, 0);
851 }
852 g_managedTorPid = -1;
853 }
854#endif
855 CRITICAL_BLOCK(cs_managedTor)
856 {
857 g_managedTorStatus = "stopped";
858 g_managedTorOnion.clear();
859 g_managedTorSocksPort = 0;
860 g_managedTorControlPort = 0;
861 g_managedTorP2PPort = 0;
862 }
863}
864
865bool BtfManagedTorEnabled()
866{
867 CRITICAL_BLOCK(cs_managedTor)
868 return g_fManagedTor;
869 return false;
870}
871
872std::string BtfManagedTorStatus()
873{
874 CRITICAL_BLOCK(cs_managedTor)
875 {
876 if (!g_fManagedTor && g_managedTorStatus.empty())
877 return "disabled";
878 return g_managedTorStatus;
879 }
880 return "disabled";
881}
diff --git a/src/tor.h b/src/tor.h
new file mode 100644
index 0000000..cfb87a5
--- /dev/null
+++ b/src/tor.h
@@ -0,0 +1,24 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// Managed Tor process support. This is transport bootstrapping only: it does
5// not touch consensus, wallet keys, mining, or the .btf identity.
6
7#ifndef BITFLASH_TOR_H
8#define BITFLASH_TOR_H
9
10#include <string>
11
12bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut);
13void BtfStopManagedTor();
14bool BtfManagedTorEnabled();
15std::string BtfManagedTorStatus();
16bool BtfBundledTorPath(std::string& torPathOut);
17
18std::string BtfBuildManagedTorrcForTest(const std::string& dataDir,
19 const std::string& hiddenServiceDir,
20 unsigned short socksPort,
21 unsigned short controlPort,
22 unsigned short p2pPort);
23
24#endif
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp
index 0ea2251..e8ac4bf 100644
--- a/src/walletcmd.cpp
+++ b/src/walletcmd.cpp
@@ -54,6 +54,37 @@ bool RestoreScanReachedDepth(int nSchema,
54 return nLegacyNext >= nDepth; 54 return nLegacyNext >= nDepth;
55} 55}
56 56
57unsigned int WalletLastUsedPubKeyIndexNext(
58 const std::map<unsigned int, std::vector<unsigned char> >& mapPubKeysByIndex)
59{
60 std::map<std::vector<unsigned char>, unsigned int> mapIndexByPubKey;
61 for (std::map<unsigned int, std::vector<unsigned char> >::const_iterator it =
62 mapPubKeysByIndex.begin();
63 it != mapPubKeysByIndex.end(); ++it)
64 mapIndexByPubKey[it->second] = it->first;
65
66 unsigned int nNext = 0;
67 CRITICAL_BLOCK(cs_mapWallet)
68 {
69 for (map<uint256, CWalletTx>::iterator it = mapWallet.begin();
70 it != mapWallet.end(); ++it)
71 {
72 CWalletTx& wtx = (*it).second;
73 for (int i = 0; i < (int)wtx.vout.size(); i++)
74 {
75 vector<unsigned char> vchPubKey;
76 if (!ExtractPubKey(wtx.vout[i].scriptPubKey, false, vchPubKey))
77 continue;
78 std::map<std::vector<unsigned char>, unsigned int>::const_iterator mi =
79 mapIndexByPubKey.find(vchPubKey);
80 if (mi != mapIndexByPubKey.end() && mi->second + 1 > nNext)
81 nNext = mi->second + 1;
82 }
83 }
84 }
85 return nNext;
86}
87
57int CmdNewPhrase() 88int CmdNewPhrase()
58{ 89{
59 AttachTerminal(); 90 AttachTerminal();
@@ -132,7 +163,10 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
132 // Derive forward in batches, scanning after each, until a whole batch turns 163 // Derive forward in batches, scanning after each, until a whole batch turns
133 // up nothing. Every derived key is written to the wallet before the scan, 164 // up nothing. Every derived key is written to the wallet before the scan,
134 // because the scan asks the wallet what belongs to it. 165 // because the scan asks the wallet what belongs to it.
135 int nTotalDerived = nHDKeySchema == HD_SCHEMA_BIP44 166 const bool fRestoreBIP44 = nHDKeySchema == HD_SCHEMA_BIP44;
167 unsigned int nLegacyScanNext = nHDNext;
168 std::map<unsigned int, vector<unsigned char> > mapLegacyScanPubKeys;
169 int nTotalDerived = fRestoreBIP44
136 ? (int)(nHDReceiveNext + nHDChangeNext) 170 ? (int)(nHDReceiveNext + nHDChangeNext)
137 : (int)nHDNext; 171 : (int)nHDNext;
138 int nStopDepth = max(nMinDepth, RESTORE_MIN_SCAN); 172 int nStopDepth = max(nMinDepth, RESTORE_MIN_SCAN);
@@ -151,7 +185,7 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
151 nWalletBefore = mapWallet.size(); 185 nWalletBefore = mapWallet.size();
152 186
153 std::string strDeriveError; 187 std::string strDeriveError;
154 if (nHDKeySchema == HD_SCHEMA_BIP44) 188 if (fRestoreBIP44)
155 { 189 {
156 for (int i = 0; i < RESTORE_BATCH; i++) 190 for (int i = 0; i < RESTORE_BATCH; i++)
157 { 191 {
@@ -182,15 +216,15 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
182 for (int i = 0; i < RESTORE_BATCH; i++) 216 for (int i = 0; i < RESTORE_BATCH; i++)
183 { 217 {
184 CKey key; 218 CKey key;
185 if (!DeriveHDKey(nHDNext, key, strDeriveError)) 219 if (!DeriveHDKey(nLegacyScanNext, key, strDeriveError))
186 break; 220 break;
187 if (!AddKey(key)) 221 if (!AddKey(key))
188 break; 222 break;
189 nHDNext++; 223 mapLegacyScanPubKeys[nLegacyScanNext] = key.GetPubKey();
224 nLegacyScanNext++;
190 nTotalDerived++; 225 nTotalDerived++;
191 } 226 }
192 nHDKeySchema = nSavedSchema; 227 nHDKeySchema = nSavedSchema;
193 CWalletDB().WriteHDNext(nHDNext);
194 } 228 }
195 else 229 else
196 { 230 {
@@ -209,6 +243,22 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
209 243
210 ScanForWalletTransactions(pindexGenesisBlock); 244 ScanForWalletTransactions(pindexGenesisBlock);
211 245
246 if (fRestoreBIP44)
247 {
248 // The legacy branch is scanned only for compatibility with phrases
249 // created before BIP44. Persist the last used legacy index + 1, not
250 // the lookahead depth. Otherwise a restore with no legacy hits can
251 // leave hdnext at RESTORE_MAX and make future compatibility scans
252 // look exhausted even though no legacy key was ever used.
253 unsigned int nLegacyUsedNext =
254 WalletLastUsedPubKeyIndexNext(mapLegacyScanPubKeys);
255 if (nLegacyUsedNext != nHDNext)
256 {
257 nHDNext = nLegacyUsedNext;
258 CWalletDB().WriteHDNext(nHDNext);
259 }
260 }
261
212 size_t nWalletAfter = 0; 262 size_t nWalletAfter = 0;
213 CRITICAL_BLOCK(cs_mapWallet) 263 CRITICAL_BLOCK(cs_mapWallet)
214 nWalletAfter = mapWallet.size(); 264 nWalletAfter = mapWallet.size();
@@ -239,7 +289,7 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
239 RestoreScanReachedDepth(nHDKeySchema, 289 RestoreScanReachedDepth(nHDKeySchema,
240 nHDReceiveNext, 290 nHDReceiveNext,
241 nHDChangeNext, 291 nHDChangeNext,
242 nHDNext, 292 fRestoreBIP44 ? nLegacyScanNext : nHDNext,
243 nStopDepth)) 293 nStopDepth))
244 break; 294 break;
245 } 295 }
diff --git a/src/walletcmd.h b/src/walletcmd.h
index 04d3fb7..007b8b5 100644
--- a/src/walletcmd.h
+++ b/src/walletcmd.h
@@ -7,7 +7,9 @@
7#ifndef BITFLASH_WALLETCMD_H 7#ifndef BITFLASH_WALLETCMD_H
8#define BITFLASH_WALLETCMD_H 8#define BITFLASH_WALLETCMD_H
9 9
10#include <map>
10#include <string> 11#include <string>
12#include <vector>
11 13
12// Create a phrase for a wallet that does not have one, install the seed, and 14// Create a phrase for a wallet that does not have one, install the seed, and
13// show the words once. Refuses if a phrase already exists. Returns 0 on success. 15// show the words once. Refuses if a phrase already exists. Returns 0 on success.
@@ -41,6 +43,13 @@ bool RestoreScanReachedDepth(int nSchema,
41 unsigned int nLegacyNext, 43 unsigned int nLegacyNext,
42 int nStopDepth); 44 int nStopDepth);
43 45
46// Given the pubkeys derived while scanning a compatibility branch, return the
47// next index after the highest one that actually appears in wallet
48// transactions. The scan depth can be much larger than this; hdnext must record
49// use, not how far a restore looked ahead.
50unsigned int WalletLastUsedPubKeyIndexNext(
51 const std::map<unsigned int, std::vector<unsigned char> >& mapPubKeysByIndex);
52
44// Take the next address from the key pool and print it. With a recovery phrase 53// Take the next address from the key pool and print it. With a recovery phrase
45// installed the address is derived, so the phrase can bring back whatever is 54// installed the address is derived, so the phrase can bring back whatever is
46// paid to it. 55// paid to it.