upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/selftest.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 05:21:31 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 05:21:31 +0000
commita9b4f8940907c2bf51e431172b628de89734a908 (patch)
treeff0a0b19d7d7754b809e35c6eae43dae7a32e1e4 /src/selftest.cpp
parent6d4c3f153cf44cefead39b21191dfb33491bfa5a (diff)
parentbc3de301d9ed0e5fb5e2034be2e066d77cf23804 (diff)
Merge upstream v1.2.18 into the fork
Merges upstream tag v1.2.18 (bc3de30) into our 24 commits. The merge base is a259215, upstream's own commit, whose src/ tree is byte-identical to 68de575 -- our commits were replayed onto a259215 first so that git had a real base to work from. That produced 28 conflict hunks in 9 files instead of the 21 conflict events / 51 hunks a direct cherry-pick onto the tag would have forced, most of which were artifacts of lifting our commits away from their own predecessors. No consensus change. main.cpp's GetBlockValue, GetNextWorkRequired, ConnectInputs, ConnectBlock, CheckBlock, AcceptBlock and AcceptTransaction are untouched by this merge; script.cpp, serialize.h and randomx_pow.h are identical on both sides. The frozen-chain oracle reproduces the pinned UTXO root 116a2b6b... byte for byte, and the merged binary loads the same 12,586-block chain to the same tip aac622f0a3b2f8... with all blocks re-verified and none rejected. Which side won, per file and per hunk. src/rpc.cpp -- 15 hunks. 1. RebuildJob transaction selection. THEIRS for the multi-pass loop and the nFees fix, OURS for the cap. Their vfAlreadyAdded/fFoundSomething loop lands dependent transactions that our single pass dropped deterministically, and their nFees is accumulated across the scan instead of being redeclared inside it -- ours passed a hardcoded 0 to GetBlockValue, so every pool block built here discarded its fees. But their bound is MAX_SIZE/2 = 16 MB, and MAX_BLOCK_SIZE is 1 MB, so their template can exceed what their own CheckBlock accepts. Cap kept at MAX_BLOCK_SIZE/2, with our inner break so a single pass cannot overshoot by more than one transaction. Our RebuildJob sync gating and deaf-node detection are kept; upstream has none. 2. struct PendingPayout. OURS for the key, THEIRS for paidTxids. Their id is the string "block-<height>", which is height keying -- the exact bug 44849e6 fixed after a 280-block reorg left ~14.4k BTF owed against blocks that no longer existed. Height keying also lets the crash-recovery probe match the previous round's transaction after a reorg and silently skip a miner, so their id is dropped entirely and blockHash stays the key. paidTxids is taken because it is what makes an entry survivable. Their nextAttempt and our retryAfter are the same concept under two names; ours is kept and theirs dropped. matureAtHeight gained an explicit = 0, which neither side had in our style of initialiser. 3./4. Ledger writer and loader. Mechanical follow-ons of 2. The loader keeps ours and takes their tolerance for an entry with no usable height, guarded so it cannot fire on a well-formed entry. One redundancy was removed rather than merged: their unguarded `if (obj.contains("blockHeight")) get<int>()` sat two lines above our is_number-guarded version, and an unguarded get<int>() on a malformed value throws out of the whole loop, losing every entry in the file. Verified against the live production pending_payouts.json (2 entries, keys amounts/blockHash/blockHeight/matureAtHeight): loads both. A file in the new shape, with retryAfter and paidTxids, also loads. 5. Insertion point after the loader. BOTH. Our PayingRecord/gPaying in-flight ledger and their PoolRoundProof/PoolRoundToJson are disjoint features that landed at the same offset. Both kept, in full. 6. Writer tail. THEIRS, copied into our writer. Checking fclose() matters here because fclose is where the buffered write actually reaches the OS: renaming a short .tmp over a good file is precisely the truncation the atomic write exists to prevent. The Windows remove()-before-rename() is required because rename() there will not replace an existing file. 7. Loader bodies. BOTH -- LoadPaying and LoadPoolRounds are different functions that happened to share a common tail. 8. Function block, 111 vs 36 lines. BOTH, with one deliberate deletion. PayoutBlockStillOnChain, PayoutComment, RecordPoolRound, MarkPoolRoundPaid all kept. WalletAlreadyPaid is DELETED and ReconcilePayouts rewritten, because our reconciliation and their FindRecordedPoolPayout are two answers to the same crash-recovery question and running both would give the same debt two authorities. Their wallet probe wins: it matches structured metadata rather than a comment string, so it can tell two rounds paying the same address the same amount apart. ReconcilePayouts keeps only the job that is still real -- dropping in-flight records the ledger already shows paid, and arming each restored record for its one wallet probe. It no longer touches gPendingPayouts at all; under the new model dropping ledger entries there would reintroduce the loss this merge adopts their model to prevent. Their gPayoutExecMutex/gPayoutThreadRunning re-declarations were dropped as duplicates of ours, and gPayoutExecQueue was not taken: gPaying is the exec queue in this tree, at finer granularity. 9. QueuePayouts signature. THEIRS for the round-proof arguments, OURS for the type of blockHash. It stays a uint256 and is converted to hex only where the round proof needs text -- a ledger key that has been through ToString() is a key that can be compared wrong. 10. Maturity policy. OURS. matureAtHeight = blockHeight + COINBASE_SPENDABLE_DEPTH (120), not + COINBASE_MATURITY (100). At depth 100 the block's own coinbase is still 20 blocks short of spendable, so the payment must come from unrelated mature coin and the pool runs a permanent float. Our own history contradicts itself here -- d6972a0 argued for COINBASE_MATURITY citing "96% of 934 payments", 44849e6 reversed it on the grounds that the measurement was taken while the node was alone on a private fork paying itself. HEAD's position is kept. This is a considered disagreement with upstream, not an oversight. The live production ledger confirms it: both entries have matureAtHeight - blockHeight = 120. 11. Comment above FlushMaturePayouts. Rewritten to describe the merged design. 12. FlushMaturePayouts, the central difference. THEIRS for the model. The ledger entry now survives on disk until every recipient has a txid, instead of being erased as soon as its payments were handed off -- the old ordering left a window where the debt was described by nothing at all, and a container restart on 2026-07-30 silently dropped ~1,156 BTF through it. Our gPaying is kept on top as the in-flight working set, because it carries the per-recipient retry and deferral state a per-round nextAttempt cannot express. It is explicitly a cache derived from the ledger, not a second record of debt: if it were lost the next flush would rebuild it. 13. PayoutThreadFn, 79 vs 70 lines. OURS for the loop and the reorg re-check, THEIRS for the per-recipient dedupe. The re-check on every attempt is load-bearing and upstream has no equivalent -- it decides once, at maturity, and a payment can sit through hours of retries during which the reorg that kills its funding block can land. Their paidTxids check is taken, against the ledger rather than against the batch. Our deferral policy is kept: waiting for coin is not a failure and must not consume the retry budget. Two fixes upstream does not have are folded in here rather than deferred to a follow-up commit: - FindRecordedPoolPayout runs once per record, on its first attempt, not on every attempt. It is a linear scan of mapWallet under cs_mapWallet, per recipient; upstream repeats it for every retry, which with 2000 rotating payout addresses and a budget of 1440 retries is a full wallet scan per address per minute for a day. The question it answers can only change across a restart, and every restart re-arms the flag. - The ledger file write no longer happens under gPayoutLedgerMutex in the per-recipient loop. SavePendingPayoutsLocked was split into a serialise step that needs the lock and a write step that does not, so the mutex is held only long enough to read the structure. The orphan scan in FlushMaturePayouts was likewise restructured so the ledger mutex is not held across PayoutBlockStillOnChain's cs_main acquisition. One case the merged model created and neither side handled: under survives-until-fully-paid, our GIVING UP path dropped the in-flight record but left the recipient owed in the ledger forever, so every later flush would explode it again -- a forfeit that silently becomes an infinite retry. ForfeitPayoutRecipient writes the amount off so the entry can retire. 14. QueuePayouts call site. Follow-on of 9. 15. ThreadRPCServer startup. BOTH. LoadPoolRounds added; LoadPaying and ReconcilePayouts stay, and stay before the payout thread starts. Crash safety, stated as the property that must hold: after kill -9 at any point, restart pays each recipient exactly once. Before SendMoney, the ledger has no txid and the payment is retried. Between SendMoney and the ledger write, the wallet holds the transaction and the first-attempt probe finds it. After the ledger write, paidTxids suppresses it. If the in-flight file is lost entirely, the ledger rebuilds it and the probe still covers the middle window. src/nostr.cpp -- 3 hunks. THEIRS as the base, with two things restored from ours. Theirs loads the Windows ROOT certificate store, which ours lacks and genuinely needs, since OpenSSL's default verify paths are usually empty there and our version would fail closed. But the plan's claim that theirs is a superset of our 06bceb4 is FALSE: upstream's nostr.cpp contains no min_proto_version, no TLS1_2, no SSL_OP_NO_*, and no set_options anywhere, so taking it wholesale would have removed the TLS-version floor outright. Restored. Second, their SSL_get_verify_result check sits after a successful handshake, where with SSL_VERIFY_PEER set it is nearly dead code -- a bad certificate aborts inside SSL_connect. The reporting belongs in the failure branch, which is where the merge put it; their post-handshake duplicate was dropped. Our relay-list and socket-pooling hunks auto-merged and were verified intact. src/net.cpp -- 1 hunk. THEIRS. Their guard, hSocket == INVALID_SOCKET || pnode->hSocket != hSocket, subsumes ours and additionally catches descriptor recycling, which ours did not; our version also referenced a pnode that only existed in the select() loop. Our other db34011 hunk, nulling pnode->hSocket in the poll-failure branch, auto-merged and was NOT re-applied -- plan section 7 Batch D says to re-apply it, and that instruction is stale. Two pieces of residue the merge left behind were cleaned up: a comment still describing select() semantics, and a pnode->hSocket != INVALID_SOCKET guard made redundant by a continue two lines above it. src/main.cpp -- 2 hunks. h1 BOTH, pure add/add adjacency between our ParticipantHashThreadCount and their SecureClear forward declarations. h2 OURS, and their handler discarded: 44004e3 no-ops checkorder/submitorder but keeps market.cpp and market.h, whereas 11766be deletes the marketplace outright including CRequestTracker, subscribe/publish and mapProducts. Ours is strictly more. src/main_gui.cpp -- 2 hunks. BOTH, additive on both sides. Our /payoutaddr, /payoutfile and /hashthreads alongside their /stratumbridge and /stratumbridgeport. src/btfrv.cpp -- 1 hunk, not predicted by the plan. False adjacency: our INVALID_SOCKET check and their #endif landed at the same offset. Ordering is load-bearing -- our check goes inside the #else arm, before the #endif, or the non-proxy build silently loses it. The BITFLASH_NODE_BUILD arm already returns early, so both paths now have the check. Makefile -- 2 hunks plus a hazard with no conflict marker. THEIRS for both dependency-fetch recipes: checkout --detach FETCH_HEAD followed by test "$(git rev-parse HEAD)" = "$(..._COMMIT)" asserts the pin actually landed, where ours only checked it out. Everything of ours that they lack is kept: the appimagetool 1.9.1 release pin with a sha256 check, where upstream fetches the mutable "continuous" tag and executes it undigested, and the move of both the tool and the AppDir out of /tmp, where any local user could pre-place an executable and have it run as whoever runs make. The hazard: both sides added a pin block in a different place, so git merged both, outside any conflict region. SECP256K1_COMMIT and RANDOMX_COMMIT ended up assigned twice, near L16 and near L44, and GNU Make takes the last := -- a naive resolution would have silently built against our old secp256k1 pin while appearing to adopt upstream's. The duplicate block was deleted by hand. Each of the four variables is now assigned exactly once, and make -n resolves SECP256K1_COMMIT to 7fecac74 with 6e2c8bc4 appearing nowhere. Also applied here, from plan phase 4: SECP256K1_COMMIT moved to upstream's 7fecac74aed8e1fd9078380d67dd04663705c989 so signature validation matches the binaries the rest of the network runs. secp256k1 was rebuilt from scratch against the new pin on the build host and the checked-out dependency commit verified. RANDOMX_COMMIT already matched at 1e9d4b2d and is unchanged. src/Makefile, src/makefile.mingw -- 1 hunk each, both the HEADERS list. Ours drops market.h, theirs adds proxy.h; the resolution does both. Taking theirs wholesale would have left the Makefiles referencing a header git had already deleted in our favour, silently, without a conflict. Files the plan predicted would conflict and did not: net.h, main.h and db.cpp auto-merged. Verified rather than assumed. net.h carries both our BTF_MSG_WAIT_MAX_SECS work, including the constructor fix from 2d84982 that starts nMsgWaitSince and nMsgWaitSize at 0, and their comment rewrite with extern int nPeersWatched. The dead members that fix removed are gone from every file. Deviations from MERGE-1.2.18-PLAN.md. - Section 6's mechanism was not used. Replayed onto the real merge base and merged, rather than cherry-picking onto the tag. Same result, 28 hunks instead of 51. - Section 2 calls 09ff575 a superset of our 06bceb4. It is not; upstream has no TLS-version floor at all. Ours restored on top of theirs. - Section 7 Batch D says to re-apply our db34011 line into their poll-failure branch. It auto-merged; re-applying would have duplicated it. - Section 2 says do not take the stratum bridge. It is taken, and left off. fStratumBridge defaults false in main.cpp, the thread is only spawned when /stratumbridge is passed, and the listener binds 127.0.0.1 only. Excising it would cost real divergence and future conflict for no gain while it is disabled. Its defects -- select()/FD_SET on a node that can hold more than 1024 descriptors, and an uncapped thread per connection -- are real only when it is enabled and are being reported upstream separately. - Section 7's staged batches were not run as separate gated commits. The conflict surface is interleaved within single functions, chiefly the payout ledger, and splitting it would have meant committing states that do not build. The merge is one commit, as briefed. - Batch F's two fixes are folded into hunk 13 rather than deferred, because they change the same lines the resolution rewrites. - The plan did not anticipate the btfrv.cpp conflict, the duplicate pin block, the market.h trap in the two Makefiles, or the give-up/infinite-retry interaction created by adopting upstream's ledger model.
Diffstat (limited to 'src/selftest.cpp')
-rw-r--r--src/selftest.cpp332
1 files changed, 325 insertions, 7 deletions
diff --git a/src/selftest.cpp b/src/selftest.cpp
index d12c96b..eed6779 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -7,9 +7,13 @@
7 7
8#include "headers_core.h" 8#include "headers_core.h"
9#include "bip32.h" 9#include "bip32.h"
10#include "proxy.h"
10#include "selftest.h" 11#include "selftest.h"
12#include "sockcount.h"
11#include "walletcmd.h" 13#include "walletcmd.h"
12 14
15extern int RunPoolStratumSelfTest();
16
13// Test results go to the terminal, not to debug.log. 17// Test results go to the terminal, not to debug.log.
14// 18//
15// printf in this tree is OutputDebugStringF, which writes into the data 19// printf in this tree is OutputDebugStringF, which writes into the data
@@ -348,17 +352,17 @@ static int RunWalletHDSelfTest()
348 nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; 352 nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1;
349 nFail += Check(nHDKeySchema == HD_SCHEMA_BIP44, 353 nFail += Check(nHDKeySchema == HD_SCHEMA_BIP44,
350 "a new recovery phrase records the BIP44 HD schema") ? 0 : 1; 354 "a new recovery phrase records the BIP44 HD schema") ? 0 : 1;
351 nFail += Check(nHDCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, 355 nFail += Check(nHDCoinType == HD_BIP44_COIN_TYPE_BITFLASH,
352 "a new recovery phrase records the provisional BIP44 coin type") ? 0 : 1; 356 "a new recovery phrase records the SLIP-0044 BIP44 coin type") ? 0 : 1;
353 nFail += Check(nHDReceiveNext == 1 && nHDChangeNext == 0, 357 nFail += Check(nHDReceiveNext == 1 && nHDChangeNext == 0,
354 "BIP44 receive/change counters reserve the default receive key") ? 0 : 1; 358 "BIP44 receive/change counters reserve the default receive key") ? 0 : 1;
355 std::vector<unsigned int> vBIP44Path = HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, 359 std::vector<unsigned int> vBIP44Path = HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH,
356 HD_BIP44_ACCOUNT, 360 HD_BIP44_ACCOUNT,
357 HD_BIP44_CHAIN_RECEIVE, 361 HD_BIP44_CHAIN_RECEIVE,
358 0); 362 0);
359 nFail += Check(vBIP44Path.size() == 5 && 363 nFail += Check(vBIP44Path.size() == 5 &&
360 vBIP44Path[0] == (HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED) && 364 vBIP44Path[0] == (HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED) &&
361 vBIP44Path[1] == (HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL | bitflash::BIP32_HARDENED) && 365 vBIP44Path[1] == (HD_BIP44_COIN_TYPE_BITFLASH | bitflash::BIP32_HARDENED) &&
362 vBIP44Path[2] == (HD_BIP44_ACCOUNT | bitflash::BIP32_HARDENED) && 366 vBIP44Path[2] == (HD_BIP44_ACCOUNT | bitflash::BIP32_HARDENED) &&
363 vBIP44Path[3] == HD_BIP44_CHAIN_RECEIVE && 367 vBIP44Path[3] == HD_BIP44_CHAIN_RECEIVE &&
364 vBIP44Path[4] == 0, 368 vBIP44Path[4] == 0,
@@ -380,7 +384,7 @@ static int RunWalletHDSelfTest()
380 hdParent.chainCode = vchHDChainCode; 384 hdParent.chainCode = vchHDChainCode;
381 bitflash::BIP32PrivateNode hdBIP44ReceiveChild; 385 bitflash::BIP32PrivateNode hdBIP44ReceiveChild;
382 if (!bitflash::BIP32DerivePath(hdParent, 386 if (!bitflash::BIP32DerivePath(hdParent,
383 HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, 387 HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH,
384 HD_BIP44_ACCOUNT, 388 HD_BIP44_ACCOUNT,
385 HD_BIP44_CHAIN_RECEIVE, 389 HD_BIP44_CHAIN_RECEIVE,
386 0), 390 0),
@@ -568,7 +572,7 @@ static int RunWalletHDSelfTest()
568 nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; 572 nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1;
569 nFail += Check(audit.nSchema == HD_SCHEMA_BIP44, 573 nFail += Check(audit.nSchema == HD_SCHEMA_BIP44,
570 "the recovery audit reports the derivation schema") ? 0 : 1; 574 "the recovery audit reports the derivation schema") ? 0 : 1;
571 nFail += Check(audit.nCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, 575 nFail += Check(audit.nCoinType == HD_BIP44_COIN_TYPE_BITFLASH,
572 "the recovery audit reports the BIP44 coin type") ? 0 : 1; 576 "the recovery audit reports the BIP44 coin type") ? 0 : 1;
573 nFail += Check(audit.nReceiveNext == nHDReceiveNext && audit.nChangeNext == 1, 577 nFail += Check(audit.nReceiveNext == nHDReceiveNext && audit.nChangeNext == 1,
574 "the recovery audit reports receive/change counters") ? 0 : 1; 578 "the recovery audit reports receive/change counters") ? 0 : 1;
@@ -1241,6 +1245,314 @@ static int RunConsensusLimitsSelfTest()
1241 return nFail == 0 ? 0 : 1; 1245 return nFail == 0 ? 0 : 1;
1242} 1246}
1243 1247
1248static int RunParseMoneySelfTest()
1249{
1250 printf("parse-money self-test\n");
1251 int nFail = 0;
1252 struct Case { const char* in; bool ok; int64 val; };
1253 Case cases[] = {
1254 {"0.5", true, 50000000LL},
1255 {"1", true, 100000000LL},
1256 {"1.23456789", true, 123456789LL},
1257 {"0.00000001", true, 1LL},
1258 {"10.00000000", true, 1000000000LL},
1259 {"1,000", true, 100000000000LL},
1260 {"1.5 ", true, 150000000LL},
1261 {"0.123456789", false, 0LL},
1262 {"1.2.3", false, 0LL},
1263 {"abc", false, 0LL},
1264 {"-1", false, 0LL},
1265 {"1e5", false, 0LL},
1266 };
1267 for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++)
1268 {
1269 int64 v = -1;
1270 bool r = ParseMoney(cases[i].in, v);
1271 bool pass = (r == cases[i].ok) && (!r || v == cases[i].val);
1272 char desc[160];
1273 snprintf(desc, sizeof(desc), "ParseMoney(\"%s\") %s and value matches",
1274 cases[i].in, cases[i].ok ? "accepts" : "rejects");
1275 nFail += Check(pass, desc) ? 0 : 1;
1276 }
1277 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1278 nFail, nFail == 1 ? "" : "s");
1279 fflush(stdout);
1280 return nFail == 0 ? 0 : 1;
1281}
1282
1283static bool SelfTestReadN(SOCKET s, void* buf, int n)
1284{
1285 char* p = (char*)buf;
1286 int off = 0;
1287 while (off < n)
1288 {
1289 int r = recv(s, p + off, n - off, 0);
1290 if (r <= 0)
1291 return false;
1292 off += r;
1293 }
1294 return true;
1295}
1296
1297static bool SelfTestWriteN(SOCKET s, const void* buf, int n)
1298{
1299 const char* p = (const char*)buf;
1300 int off = 0;
1301 while (off < n)
1302 {
1303 int r = send(s, p + off, n - off, 0);
1304 if (r <= 0)
1305 return false;
1306 off += r;
1307 }
1308 return true;
1309}
1310
1311static void SelfTestSetSocketTimeout(SOCKET s, int nTimeoutSecs)
1312{
1313#ifdef _WIN32
1314 DWORD tv = (DWORD)nTimeoutSecs * 1000;
1315 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
1316 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
1317#else
1318 struct timeval tv;
1319 tv.tv_sec = nTimeoutSecs;
1320 tv.tv_usec = 0;
1321 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
1322 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
1323#endif
1324}
1325
1326#ifdef _WIN32
1327struct SelfTestWinsock
1328{
1329 bool fStarted;
1330 SelfTestWinsock() : fStarted(false)
1331 {
1332 WSADATA wsadata;
1333 fStarted = (WSAStartup(MAKEWORD(2,2), &wsadata) == 0);
1334 }
1335 ~SelfTestWinsock()
1336 {
1337 if (fStarted)
1338 WSACleanup();
1339 }
1340};
1341#endif
1342
1343static bool RunSocks5HandshakeProbe(std::string& errOut)
1344{
1345 errOut.clear();
1346#ifdef _WIN32
1347 SelfTestWinsock winsock;
1348 if (!winsock.fStarted)
1349 {
1350 errOut = "could not start Winsock";
1351 return false;
1352 }
1353#endif
1354
1355 SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
1356 if (listener == INVALID_SOCKET)
1357 {
1358 errOut = "could not create listener";
1359 return false;
1360 }
1361 SelfTestSetSocketTimeout(listener, 5);
1362
1363 struct sockaddr_in addr;
1364 memset(&addr, 0, sizeof(addr));
1365 addr.sin_family = AF_INET;
1366 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
1367 addr.sin_port = 0;
1368 if (bind(listener, (struct sockaddr*)&addr, sizeof(addr)) != 0 ||
1369 listen(listener, 1) != 0)
1370 {
1371 closesocket(listener);
1372 errOut = "could not bind/listen on loopback";
1373 return false;
1374 }
1375
1376#ifdef _WIN32
1377 int addrLen = sizeof(addr);
1378#else
1379 socklen_t addrLen = sizeof(addr);
1380#endif
1381 if (getsockname(listener, (struct sockaddr*)&addr, &addrLen) != 0)
1382 {
1383 closesocket(listener);
1384 errOut = "could not read listener port";
1385 return false;
1386 }
1387 unsigned short proxyPort = ntohs(addr.sin_port);
1388
1389 bool fServerOk = false;
1390 std::string serverErr;
1391 std::string requestedHost;
1392 unsigned short requestedPort = 0;
1393 std::thread server([&]() {
1394 SOCKET s = accept(listener, NULL, NULL);
1395 if (s == INVALID_SOCKET)
1396 {
1397 serverErr = "accept failed";
1398 return;
1399 }
1400 SelfTestSetSocketTimeout(s, 5);
1401
1402 unsigned char hello[3] = {0,0,0};
1403 if (!SelfTestReadN(s, hello, sizeof(hello)) ||
1404 hello[0] != 0x05 || hello[1] != 0x01 || hello[2] != 0x00)
1405 {
1406 serverErr = "bad SOCKS5 greeting";
1407 closesocket(s);
1408 return;
1409 }
1410 unsigned char choice[2] = {0x05, 0x00};
1411 if (!SelfTestWriteN(s, choice, sizeof(choice)))
1412 {
1413 serverErr = "could not write method choice";
1414 closesocket(s);
1415 return;
1416 }
1417
1418 unsigned char hdr[5] = {0,0,0,0,0};
1419 if (!SelfTestReadN(s, hdr, sizeof(hdr)) ||
1420 hdr[0] != 0x05 || hdr[1] != 0x01 || hdr[2] != 0x00 || hdr[3] != 0x03)
1421 {
1422 serverErr = "CONNECT did not use domain-name address type";
1423 closesocket(s);
1424 return;
1425 }
1426
1427 unsigned char len = hdr[4];
1428 std::vector<unsigned char> rest((size_t)len + 2);
1429 if (!SelfTestReadN(s, &rest[0], (int)rest.size()))
1430 {
1431 serverErr = "could not read CONNECT target";
1432 closesocket(s);
1433 return;
1434 }
1435 requestedHost.assign((const char*)&rest[0], (size_t)len);
1436 requestedPort = ((unsigned short)rest[len] << 8) | rest[len + 1];
1437
1438 unsigned char reply[10] = {0x05, 0x00, 0x00, 0x01, 0,0,0,0, 0,0};
1439 if (!SelfTestWriteN(s, reply, sizeof(reply)))
1440 {
1441 serverErr = "could not write CONNECT reply";
1442 closesocket(s);
1443 return;
1444 }
1445 fServerOk = true;
1446 closesocket(s);
1447 });
1448
1449 std::string err;
1450 BtfClearSocks5Proxy();
1451 bool fClientOk = BtfSetSocks5Proxy(strprintf("127.0.0.1:%u", (unsigned)proxyPort), err);
1452 SOCKET client = INVALID_SOCKET;
1453 if (fClientOk)
1454 client = BtfConnectSocket("relay.example", 443, SOCK_NOSTR, 5);
1455 if (client != INVALID_SOCKET)
1456 BtfCloseSocket(client);
1457 BtfClearSocks5Proxy();
1458 closesocket(listener);
1459 server.join();
1460
1461 if (!fClientOk)
1462 errOut = strprintf("could not enable proxy: %s", err.c_str());
1463 else if (client == INVALID_SOCKET)
1464 errOut = "client SOCKS5 connect failed";
1465 else if (!fServerOk)
1466 errOut = serverErr.empty() ? "mock server failed" : serverErr;
1467 else if (requestedHost != "relay.example" || requestedPort != 443)
1468 errOut = strprintf("unexpected CONNECT target %s:%u", requestedHost.c_str(), (unsigned)requestedPort);
1469 else
1470 return true;
1471 return false;
1472}
1473
1474static int RunSocks5ProxySelfTest()
1475{
1476 printf("socks5-proxy self-test\n");
1477 int nFail = 0;
1478 std::string host, err;
1479 unsigned short port = 0;
1480
1481 nFail += Check(BtfParseSocks5Proxy("127.0.0.1:9050", host, port, err) &&
1482 host == "127.0.0.1" && port == 9050,
1483 "parses local Tor proxy endpoint") ? 0 : 1;
1484 nFail += Check(BtfParseSocks5Proxy("localhost:9050", host, port, err) &&
1485 host == "localhost" && port == 9050,
1486 "parses hostname proxy endpoint") ? 0 : 1;
1487 nFail += Check(BtfParseSocks5Proxy("[::1]:9050", host, port, err) &&
1488 host == "::1" && port == 9050,
1489 "parses bracketed IPv6 proxy endpoint") ? 0 : 1;
1490 nFail += Check(!BtfParseSocks5Proxy("::1:9050", host, port, err),
1491 "rejects unbracketed IPv6 proxy endpoint") ? 0 : 1;
1492 nFail += Check(!BtfParseSocks5Proxy("localhost", host, port, err),
1493 "rejects missing port") ? 0 : 1;
1494 nFail += Check(!BtfParseSocks5Proxy("localhost:0", host, port, err),
1495 "rejects zero port") ? 0 : 1;
1496 nFail += Check(!BtfParseSocks5Proxy("localhost:70000", host, port, err),
1497 "rejects out-of-range port") ? 0 : 1;
1498 nFail += Check(!BtfParseSocks5Proxy("localhost:999999999999999999999999999999", host, port, err),
1499 "rejects overflowing port") ? 0 : 1;
1500 nFail += Check(!BtfParseSocks5Proxy("user:pass@localhost:9050", host, port, err),
1501 "rejects unsupported authenticated proxy syntax") ? 0 : 1;
1502 nFail += Check(BtfIsTorOnionHost("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion") &&
1503 BtfIsTorOnionHost("Relay.Example.ONION.") &&
1504 !BtfIsTorOnionHost("relay.example") &&
1505 !BtfIsTorOnionHost(".onion"),
1506 "detects .onion hosts without accepting lookalikes") ? 0 : 1;
1507
1508 BtfClearSocks5Proxy();
1509 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1;
1510 nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9050", err) &&
1511 BtfSocks5ProxyEnabled() &&
1512 BtfSocks5ProxyName() == "127.0.0.1:9050",
1513 "enables parsed SOCKS5 proxy") ? 0 : 1;
1514 BtfClearSocks5Proxy();
1515 nFail += Check(!BtfSocks5ProxyEnabled(), "clear disables proxy") ? 0 : 1;
1516
1517 nFail += Check(BtfSetSocks5Proxy("[::1]:9050", err) &&
1518 BtfSocks5ProxyEnabled() &&
1519 BtfSocks5ProxyName() == "[::1]:9050",
1520 "enables bracketed IPv6 proxy endpoint") ? 0 : 1;
1521 BtfClearSocks5Proxy();
1522
1523 nFail += Check(BtfEnableTorProxy("", err) &&
1524 BtfSocks5ProxyEnabled() &&
1525 BtfTorProxyEnabled() &&
1526 BtfSocks5ProxyName() == "127.0.0.1:9050",
1527 "enables Tor mode on the default local SOCKS5 endpoint") ? 0 : 1;
1528 BtfClearSocks5Proxy();
1529
1530 nFail += Check(BtfEnableTorProxy("[::1]:9050", err) &&
1531 BtfSocks5ProxyEnabled() &&
1532 BtfTorProxyEnabled() &&
1533 BtfSocks5ProxyName() == "[::1]:9050",
1534 "enables Tor mode on a custom IPv6 endpoint") ? 0 : 1;
1535 BtfClearSocks5Proxy();
1536
1537 nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9051", err) &&
1538 BtfSocks5ProxyEnabled() &&
1539 !BtfTorProxyEnabled(),
1540 "plain SOCKS5 mode is distinct from Tor mode") ? 0 : 1;
1541 BtfClearSocks5Proxy();
1542
1543 std::string handshakeErr;
1544 bool fHandshakeOk = RunSocks5HandshakeProbe(handshakeErr);
1545 nFail += Check(fHandshakeOk, handshakeErr.empty() ?
1546 "completes SOCKS5 domain-name CONNECT handshake" :
1547 strprintf("completes SOCKS5 domain-name CONNECT handshake (%s)",
1548 handshakeErr.c_str()).c_str()) ? 0 : 1;
1549
1550 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1551 nFail, nFail == 1 ? "" : "s");
1552 fflush(stdout);
1553 return nFail == 0 ? 0 : 1;
1554}
1555
1244int RunSelfTest(const std::string& name) 1556int RunSelfTest(const std::string& name)
1245{ 1557{
1246 AttachTerminal(); 1558 AttachTerminal();
@@ -1259,8 +1571,14 @@ int RunSelfTest(const std::string& name)
1259 return RunNetMessageSelfTest(); 1571 return RunNetMessageSelfTest();
1260 if (name == "consensus-limits") 1572 if (name == "consensus-limits")
1261 return RunConsensusLimitsSelfTest(); 1573 return RunConsensusLimitsSelfTest();
1574 if (name == "pool-stratum")
1575 return RunPoolStratumSelfTest();
1576 if (name == "parse-money")
1577 return RunParseMoneySelfTest();
1578 if (name == "socks5-proxy")
1579 return RunSocks5ProxySelfTest();
1262 1580
1263 printf("Unknown self-test '%s'\n", name.c_str()); 1581 printf("Unknown self-test '%s'\n", name.c_str());
1264 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits\n"); 1582 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy\n");
1265 return 1; 1583 return 1;
1266} 1584}