Adds src/test/onion_v3_tests.cpp with 8 Boost.Test cases that validate
every hardcoded seed in src/onionseed.h against the v3 hidden service
checksum algorithm (SHA3-256 of ".onion checksum" || pubkey || version).
Test cases:
* onion_v3_valid_known_seeds - all 7 hardcoded seeds must validate
* onion_v3_detects_transposition - catches the btb6/gtb6 bug from 2026-06-21
* onion_v3_detects_wrong_length - too short, too long
* onion_v3_detects_missing_suffix - .com instead of .onion
* onion_v3_detects_invalid_base32 - chars 0,1,8,9 + uppercase rejected
* onion_v3_detects_bad_version_byte - all-'a' body has invalid checksum
* onion_v3_round_trip_encoding - base32 encode/decode is deterministic
* onion_v3_audit_summary - overall summary check
The C++ validator mirrors scripts/validate_onion_seeds.py exactly so the
two implementations stay in sync. Catches corruption at CI/build time
instead of daemon runtime.
Also fixes an unrelated build break: GetPeerInflightCap() was called from
syncmanager.cpp:533 but never declared in syncmanager.h. The function
intent was 'windowSize / peerCount + 1' - inlined that here so the test
build can succeed.
Triangles already has a node-identity signing system (getwalletaddr/walletaddr
in onion_v3.cpp:4793-4848) that lets peers cryptographically prove they own
their .onion address. The problem: that handshake only fires at startup, so
a long-running sync daemon that takes 12+ hours to bootstrap gets exactly ONE
discovery round at minute 0 — and then never asks again.
This commit wires the existing signing + discovery machinery into the main
peer-connection loop, not just startup:
* src/net.h: add nLastGetaddrTrigger + nSignedPeerBonus fields to CNode
* src/net.cpp: in ThreadOpenConnections2, when connected onion peers < 4
AND 5min cooldown elapsed, re-fire getaddr + getseederlist on every
connected .onion peer. getwalletaddr is left alone (it generates a new
receiving key per call; signed peers are cached 24h anyway).
* src/tor/onion_v3.cpp: when HandleWalletAddrResponse verifies a peer's
signature, set nSignedPeerBonus=1 so sync peer selection prefers them.
* src/syncmanager.cpp: signed-peer bonus used as tiebreaker in peer sort
(after reliability score, before blocks-delivered).
Why this matters: real-world from-zero sync of the Triangles chain took
~18 hours because only 2-3 of the 14 seed .onion nodes were reliably
reachable from any given Tor instance. With periodic re-discovery, the
daemon now has a chance to find the 12 others when the 2-3 drop.
Verified: built clean (15:59), test daemon climbed from 70,828 → 73,997+
at ~1.9 blk/s with new binary, SYNC-SIGN message confirmed firing.
Sync-freeze patch (original):
- Backpressure ceiling HEADER_FRONT_MAX_AHEAD=8000
- PruneHeaders protects live sync window (nProtectFloor)
- Hard-cap eviction from highest-height first
- Bridge-repair getheaders from connected tip via PathReachesChain
Additional fix:
- Skip PoW check on PoS headers (nonce=0) in AddHeaderNode
Block 1026 is PoS but within the 0-9000 PoW range — old code
rejected valid PoS headers and severed the chain at height 1025
Verified: from-zero no-snapshot sync reached block 17k+ past the
old 15k freeze zone. 132/132 unit tests pass.
CSyncManager extracts the headers-first IBD planner from main.cpp into
its own translation unit. main.cpp loses ~570 lines of file-scope state
and helper functions; the headers handler, block-delivery latency
tracking, stall-recovery, and per-peer Tick cadence now route through
g_syncManager.
MaybeMigrateLevelDbToRocksDb() is now reachable via -migratechaindb /
-migratechaindbforce in init.cpp. Reads from <datadir>/txleveldb and
writes byte-for-byte identical records into <datadir>/rocksdb via a
new CRocksTxDB::WriteRawRecordForMigration() shim over WriteRaw.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>