Tor v3 hidden-service support (BIP155) with fluxnode authentication (depends on #284) - #287
Open
MorningLightMountain713 wants to merge 54 commits into
Open
Conversation
Vendors the ECVRF-SECP256K1-SHA256-TAI (CFRG VRF draft-05, suite 0xFE) module
from aergo/secp256k1-vrf (MIT) into the bundled libsecp256k1 as an optional
module (--enable-module-vrf, enabled in the root build), and adds src/crypto/
ecvrf.{h,cpp} as the C++ boundary (ECVRF_Prove/ECVRF_Verify over CKey/CPubKey).
This is the cryptographic primitive for the PON VRF leader-election fix that
closes the leader-election grinding vulnerability: block eligibility becomes
y = VRF(operator_sk, epoch_seed) <= target, which the proposer cannot grind.
Verified: builds under Flux's exact secp256k1 flags (--with-bignum=no) and
reproduces the published draft-05 test vector byte-for-byte (prove/verify/
proof_to_hash); cross-checked against Witnet vrf-rs and an independent Python
reference. Constant-time audit of secret paths still pending before activation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the grindable PON eligibility lottery (GetPONHash over the proposer-
chosen prevBlockHash) with VRF-based eligibility, gated by UPGRADE_PON_VRF:
eligible(C) <=> y <= target, y = VRF(operator_key, epoch_seed)
y is unforgeable (operator secret key) and seeded by a buried block window the
proposer did not author (GetEpochSeed), so a producer can no longer shape its
own block to win the next lottery. Builds on the ECVRF primitive + ecvrf C++
boundary added in the previous commit.
- block.h: PON_VRF_VERSION=101; nodesVrfOutput + nodesVrfProof header fields,
committed under SER_GETHASH (covered by the operator signature).
- consensus/params.h, upgrades.cpp, chainparams.cpp: UPGRADE_PON_VRF
(NO_ACTIVATION_HEIGHT on all networks for now).
- pon-fork: IsPONVRFActive().
- pon.cpp: GetEpochSeed (buried-window accumulator); VRF eligibility in
CheckPONBlockHeader; proof verification (recomputed beta == nodesVrfOutput)
in ContextualCheckPONBlockHeader.
- pon-minter.cpp: compute the VRF proof with the operator key; coordinate via a
self-computable priority (lower y => shorter delay) since other nodes' VRF
outputs are unknowable; set the header fields before signing.
Pre-activation blocks use the legacy GetPONHash path unchanged. fluxd builds.
NOT yet exercised on a regtest/testnet fork; coordination/liveness and the
constant-time audit are pending (see pon-vrf/REVIEW.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makes competing same-height VRF blocks resolve deterministically so the network converges instead of forking back and forth: - CBlockIndexWorkComparator (main.cpp): for PON_VRF blocks at equal work/height, break ties by lowest nodesVrfOutput. The VRF output is un-grindable, so unlike GetPONHash (depends on proposer-chosen nTime, grindable to win ties) an attacker cannot bias which competitor wins. Legacy PON blocks keep the GetPONHash tie-break (mixed-version forks around activation). - block.h: commit only the VRF output to the block hash; exclude the proof (like the signature) — the proof is self-validating against the committed output. - chain.h / txdb.cpp: store nodesVrfOutput in CBlockIndex + CDiskBlockIndex so the comparator can read it and GetBlockHash() recomputes correctly across restarts. The minting-delay coordination (previous commit) is now only orphan reduction; convergence/safety rests on this deterministic comparator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gence Extracts the PON fork-choice tie-break from the anonymous-namespace comparator in main.cpp into a public, testable function ComparePonForkChoice (pon.cpp). The comparator now delegates to it, so the tests exercise the real deployed logic. Adds gtests (test_pon.cpp) verifying the convergence guarantee: - lowest VRF output is preferred (deterministic winner among competitors), - antisymmetric (swap args -> sign flips: all nodes agree), - deterministic (same inputs -> same result), - equal outputs -> undecided (fall back to first-seen). All 22 PONTest cases pass (flux-gtest). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds gtests (test_pon.cpp) exercising the VRF block lifecycle with unbypassed crypto: - VrfBlockHeaderSerializationRoundTrip: PON_VRF header serializes/deserializes intact and the hash is stable. - VrfOutputCommittedProofExcludedFromHash: changing the proof does not change the block hash (excluded, like the signature) while changing the VRF output does (committed) — pins the design that lets CBlockIndex store only the 32-byte output. - EcvrfProveVerifyRoundTrip: real ECVRF_Prove -> ECVRF_Verify round trip (the same crypto ContextualCheckPONBlockHeader runs); tampered proof, wrong key, and wrong seed are all rejected; proving is deterministic (RFC 6979). 25 PONTest cases pass (flux-gtest). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…roduced Live regtest testing revealed CreateNewBlock assembled a v100 PON block and ran TestBlockValidity on it BEFORE the minter/generate set the VRF fields — so once PON-VRF is active, block production failed with 'bad-pon-...' (version below PON_VRF_VERSION). The header build + validity check were producing/validating a block that could never pass the VRF eligibility rules. Fix: in CreateNewBlock, when PON-VRF is active, set nVersion = PON_VRF_VERSION and compute nodesVrfOutput/nodesVrfProof (via the operator key, or a deterministic placeholder when no key is configured, e.g. regtest generate) BEFORE TestBlockValidity. The minter and the regtest generate RPC now rely on this single authoritative path (generate's redundant post-assembly block removed). Verified on regtest: 'generate' past the PON-VRF activation height produces v101 blocks that pass validation (v100 before activation, v101 after). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…support
Three consensus/relay fixes (found via live testnet block production), all the same
root cause: the committed VRF output must be handled everywhere a block header is
built, hashed, validated, or relayed.
1. Per-slot VRF eligibility (pon.{h,cpp}, pon-minter.cpp, miner.cpp): the VRF input
is now H(epoch_seed || slot) (GetPonVrfMessage) instead of just epoch_seed. Without
the slot, a node's eligibility was constant for an entire epoch (eligible every slot
or none) — no leader rotation, broken liveness. The slot carries only the minor,
already-acknowledged ~10-slot future-time grind; the large prevBlockHash/coinbase
grind remains eliminated. Minter and ContextualCheckPONBlockHeader use it consistently.
2. CheckBlockHeader (main.cpp): for PON-VRF blocks, check the committed VRF output
(nodesVrfOutput) against target, not the legacy GetPONHash. The legacy value is
meaningless for VRF blocks and rejected ~half of valid v101 blocks as 'high-hash'.
3. CCompactBlockHeader (block.h): serialize the VRF output/proof for PON-VRF blocks.
It was omitted, so a peer decoded a v101 compact header with a null VRF output,
recomputed the wrong block hash, and rejected the chain as 'non-continuous
cmpheaders sequence' — breaking header sync between nodes.
Verified on a local testnet: a confirmed fluxnode mints v101 VRF blocks with clean
production (0 high-hash) and a second node syncs the VRF chain (0 non-continuous).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same root cause as the CheckBlockHeader fix: ReadBlockFromDisk re-validated every PON block against the legacy GetPONHash, so ~half of v101 blocks failed on disk-read with 'Errors in block header' — crashing the node shortly after it minted a VRF block. For PON-VRF blocks, check the committed nodesVrfOutput against target instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sixth and final instance of the same root cause: the on-disk block-index verification (LoadBlockIndex) re-checked every PON block against the legacy GetPONHash, so ~half of stored v101 blocks failed on startup with 'Error loading block database', preventing a node from restarting once it had synced/minted VRF blocks. Use the committed nodesVrfOutput for PON-VRF blocks. All header-eligibility check sites now agree: CheckPONBlockHeader, CheckBlockHeader, ReadBlockFromDisk, LoadBlockIndex. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sets the testnet PON_VRF upgrade to a placeholder height (9999999) so the activation
switch is staged in one obvious place. This is NOT a real schedule.
ACTION REQUIRED before tagging a testnet release:
- Replace 9999999 with a concrete testnet height comfortably above the current tip,
giving the fleet time to upgrade first.
Mainnet and regtest remain NO_ACTIVATION_HEIGHT (inert) and are unchanged here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PON-VRF changes the wire serialization: v101 block headers carry the VRF output (committed) + proof, and the cmpheaders compact-header format carries them too. That must be a distinct protocol version so VRF-capable nodes are distinguishable from prior 170021 (compact-headers) nodes and can be gated at activation. - PROTOCOL_VERSION: 170021 -> 170022 (VRF-capable nodes advertise this) - UPGRADE_PON_VRF.nProtocolVersion: 170020 -> 170022 (all networks) so peers below 170022 are rejected once PON_VRF activates, guaranteeing all connected peers speak the VRF wire format. UPGRADE_PON stays 170020 (unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…under shared operator keys The VRF message was H(epoch_seed || slot), keyed by the operator key — but operator keys are shared across an owner's fleet in practice (review finding). With the key alone, N same-key nodes compute the identical VRF output, which: 1. Collapses N lottery draws into one, shrinking the fleet's share of block production N-fold. Minting pays the dev fund (not the minter), so this is a leadership/liveness distortion — block production silently concentrates in uniquely-keyed operators — rather than lost operator revenue. 2. On a win, makes all N nodes eligible with the same VRF-derived priority delay, so they broadcast competing blocks simultaneously (broadcast storm). 3. Voids the lowest-VRF fork-choice tie-break — the outputs are identical, so convergence degrades to first-seen on every such win. The message is now H(epoch_seed || slot || collateral). The collateral outpoint is the canonical per-node identity and is already committed in the header (nodesCollateral) and already used by the verifier to look up the operator pubkey, so verification needs no new wire data. The outpoint is fixed at node registration — before any future epoch seed exists — so it adds no grinding surface beyond the known key-grinding residual. Adds gtest VrfMessagePerNodeUnderSharedOperatorKey: distinct collaterals yield distinct messages and independent verifiable outputs under one shared key, and a proof for node A does not verify as node B. 26 PONTest cases pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LogPONEligibility predicts per-node eligibility with the legacy GetPONHash formula, which is dead once VRF leader election activates — and under VRF other nodes' eligibility cannot be computed at all (each draw needs that node's secret key). Anything it printed post-activation would be actively misleading to operators debugging minting from logs. Log-only change, gated on the same IsPONVRFActive height check as the consensus paths: no behavior change before activation on any network. Also skips a full confirmed-fluxnode-cache iteration per connected tip after activation. 26 PONTest cases pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1 of the BIP155 / TORv3 backport from Bitcoin Core. Adds the
SHA3-256 implementation needed for the TORv3 ed25519 .onion address
checksum, bumps PROTOCOL_VERSION 170020 -> 170021, and defines the
SENDADDRV2_VERSION constant used by later phases to gate addrv2
support during the version handshake.
The SHA3 implementation is a C++11 port of Bitcoin Core's
src/crypto/sha3.{h,cpp}, with std::span replaced by (ptr, size)
pairs and std::rotl replaced by an inline Rotl64 helper. Validated
against the NIST SHA3-256 KAT vectors for the empty string and "abc".
See doc/build-journal-bip155-torv3.md section 3 (Phase 1).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phases 2-4 of the BIP155 / TORv3 backport from Bitcoin Core. These
three phases are tightly coupled (all touch the network layer) and
land together as one logical unit.
Phase 2 - CNetAddr refactor:
Replace the legacy fixed `unsigned char ip[16]` field with the
clean Bitcoin-style design: Network m_net + std::vector<uint8_t>
m_addr. A private SerializeV1Array(out[16]) helper reconstructs
the legacy 16-byte representation on demand so all
GetByte/GetHash/GetGroup paths produce byte-identical output for
IPv4/IPv6 addresses.
Validated byte-equivalent vs the legacy implementation across 39
test addresses (public IPv4, RFC1918 all three ranges, RFC2544,
RFC3927, RFC5737 all three blocks, RFC6598, loopback, IPv6 ULA,
link-local, Teredo, NAT64, IPv4-translated, he.net /36,
IPv4-mapped-IPv6, the existing netbase_tests GetGroup contract
addresses, and the historical FD87:D87E:EB43 OnionCat range).
Addrman bucket placement is therefore byte-identical across the
refactor — an upgraded fluxd loading an existing peers.dat keeps
every IPv4/IPv6 entry in the exact same bucket. The equivalence
harness also caught a real bug: SetRaw(NET_IPV6, ...) had to be
taught to normalize IPv4-mapped-IPv6 addresses to NET_IPV4 (the
legacy code did this implicitly via the byte-prefix check at
every IsIPv4() call site).
Phase 3 - TORv3 parser + torcontrol v3 service:
Real CNetAddr::SetTor() parses 56-char .onion strings, validates
the SHA3-256 checksum (".onion checksum" || pubkey || version),
and stores the 32-byte ed25519 pubkey. ToStringIP() encodes v3
onions back. torcontrol.cpp now requests NEW:ED25519-V3 instead
of NEW:RSA1024 from Tor's control port, and persists the key to
onion_v3_private_key (legacy v2 keys are intentionally not
migrated — TORv2 was removed from the live Tor network in 2021).
Phase 4 - BIP155 wire format + sendaddrv2 negotiation:
Adds explicit SerializeV2/UnserializeV2 template member functions
on CNetAddr/CService/CAddress, reached only through a CAddrVecV2
wrapper. The legacy SerializationOp dispatch is provably untouched
by anything in the V2 path. Adds the BIP155Network enum (TORV3=4),
the sendaddrv2/addrv2/getaddrv2 P2P message handlers, the per-peer
m_wants_addrv2 flag on CNode, the version-handshake injection
(sendaddrv2 sent before verack to peers >= SENDADDRV2_VERSION),
and the SendMessages relay branching that drops v3 onion addresses
for legacy peers.
Also includes the post-Phase-6 NET_TOR -> NET_ONION rename across
the affected files. The enum value at position 3 is preserved, so
addrman bucketing is unchanged by the rename.
See doc/build-journal-bip155-torv3.md sections 3 (Phases 2-4) and
4.1-4.3, 4.5 for the full record.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 5 of the BIP155 / TORv3 backport from Bitcoin Core. Bumps the
peers.dat format version from 1 to 2 so that v3 onion peers learned
via addrv2 gossip survive across restarts.
Each CAddrInfo entry is now encoded via SerializeV2/UnserializeV2,
which routes both the embedded CAddress and the source CNetAddr
through the explicit BIP155 V2 codecs added in the previous commit.
The outer envelope (nKey, nNew, nTried, nUBuckets XOR magic, bucket
positions) is structurally identical to v1.
The Unserialize path is also hardened: it strictly accepts version
1 (legacy) or 2 (BIP155-aware) and throws std::ios_base::failure on
anything else. The legacy code only checked != 0; a future format
bump will need to extend this whitelist, but there is no longer any
silent-misread risk.
Backwards compatibility:
- New fluxd reading format 1: works (legacy s >> info path)
- New fluxd reading format 2: works (UnserializeV2 path)
- Old fluxd reading format 2: throws -> addrman starts empty
(graceful failure; addrman is cache, not consensus state)
See doc/build-journal-bip155-torv3.md sections 3 (Phase 5) and 4.4.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 6 of the BIP155 / TORv3 backport from Bitcoin Core. Adds
operator-facing visibility for the BIP155 negotiation state and
documentation.
- getpeerinfo gains two new keys per peer:
"network": "ipv4" / "ipv6" / "onion" / "unroutable"
"addrv2": true / false (whether SENDADDRV2 was negotiated
before VERACK)
Operators can now answer "how many of my peers are onion?" at
a glance.
- CNodeStats grew matching m_network and m_wants_addrv2 fields,
populated in CNode::copyStats from addr.GetNetwork() and the
atomic flag.
- doc/tor.md is fully rewritten for v3: removes the v2-era
"do not assume Tor support does the correct thing" warning,
replaces every v2 example with the Phase 3 v3 test vector,
documents the new onion_v3_private_key file (and that legacy
onion_private_key is intentionally not migrated), explains
BIP155 negotiation in terms of getpeerinfo output, and covers
peers.dat format compatibility.
- doc/build-journal-bip155-torv3.md (NEW) is the complete
engineering record of the six-phase port for future maintainers.
Documents every architectural decision, every shortcut that was
cut and then reverted (the Phase 2 hybrid storage, the inlined
pchIPv4 bytes, the deferred NET_TOR rename), and the validation
strategy for each phase.
- doc/build-test-onion-linux.md (NEW) is the operator-facing
Linux build and two-node onion test guide. Distro-specific
package lists, the C++14 configure.ac bump that's required for
modern Boost, the four-tier test plan from "smoke test without
Tor" through "real onion peering" through "persistence" through
"mainnet shadow soak", and a common-gotchas section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds section 5.7 to the BIP155/TORv3 build journal flagging the generated-file-in-version-control footgun that bit during this work. The file captures the build environment of whoever last ran configure (wallet/ZMQ/Boost flags), so accidental commits would silently change the build for everyone. Recommended cleanup is documented but explicitly out of scope for the BIP155 branch — separate concern, separate review, separate revert path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
std::begin()/std::end() on C-style arrays requires <iterator>, which GCC 14 no longer pulls in transitively. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sendaddrv2 must arrive between version and verack per BIP155. Setting fSuccessfullyConnected at the end of the version handler caused the inbound side to reject the outbound peer's sendaddrv2 as "received after VERACK" with a misbehavior penalty, preventing addrv2 negotiation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Inbound hidden-service connections arrive from 127.0.0.1, so the normal FindNode() duplicate check misses them. Use the peer's self-announced addrFrom (.onion address) from the version message to detect if we already have an outbound connection to the same peer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Feature 1: Fluxnodes must keep IPv4 available — reject -onlynet configurations that limit IPv4, preventing Tor-only fluxnodes that would be invisible to the clearnet deterministic list. Feature 2: Cap outbound onion connections (default 2, configurable via -maxonionoutbound). Prevents addrman poisoning with fake .onion addresses from monopolizing outbound slots. Exempts Tor from the single-group check since all onion addresses share one group. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add challenge-response authentication for Tor P2P connections. After verack, fluxnodes send a random challenge to Tor peers. The peer must sign the challenge with its fluxnode private key and prove its identity against the deterministic fluxnode list. This prevents Sybil attacks over Tor — an attacker would need actual fluxnode collateral to pass authentication. Features: - Mutual authentication: both sides challenge each other - Self-connection detection via outpoint comparison - 60-second timeout for inbound Tor peers that fail to auth - Skipped during IBD when the fluxnode cache is empty - Non-fluxnode wallets silently ignored (no auth response) New P2P messages: torauthreq, torauthresp New CNode fields: nTorAuthChallenge, fTorAuthSent, fTorAuthenticated, torAuthOutpoint, nTorAuthTimestamp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The addrFrom-based duplicate check was unreliable — inbound Tor peers only advertise their .onion in addrFrom if using -torcontrol or -externalip, so externally-configured hidden services slip through. After torauth proves a peer's fluxnode identity, scan all connected peers for a matching outpoint and disconnect the duplicate, preferring to keep the outbound connection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extend the torauth protocol so peers prove they own their .onion address by signing the challenge with their Tor hidden service ed25519 key. After verification, the receiver updates the peer's addr from 127.0.0.1:<ephemeral> to the real .onion:port and network becomes "onion" in getpeerinfo. Key material is cached from the ADD_ONION response in torcontrol.cpp and exposed via GetTorServiceEd25519Key(). The wire format is backward compatible — older peers that don't send onion proof simply retain their 127.0.0.1 addr. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fluxd now generates the ed25519 seed itself and stores the raw 32-byte seed as the authoritative key in onion_v3_private_key. The Tor expanded format (SHA-512 + clamp) is derived on the fly for ADD_ONION. This lets us sign with libsodium (which requires the seed) while still providing Tor the expanded format it expects. The previous approach tried to use Tor's expanded key directly with libsodium's crypto_sign_seed_keypair, which double-hashed the scalar and produced wrong signatures. Existing nodes will regenerate a new onion address on first restart (the old Tor-format key file is replaced with the seed). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address 13 issues found during security/code audit of the torv3-bip155-port branch: CRITICAL: - Bind ed25519 onion proof to fluxnode outpoint (sign challenge||outpoint_hash) to prevent relay attacks where a MITM replays another node's onion signature HIGH: - Zero torEd25519SK/PK on shutdown via sodium_memzero in StopTorControl() - Secure-erase TorController::private_key after ADD_ONION and in destructor - Set chmod 0600 on onion_v3_private_key file (was inheriting umask) MEDIUM: - Extend torauth timeout to outbound .onion connections (was inbound-only) - Make fTorAuthSent/fTorAuthenticated/nTorAuthTimestamp atomic (cross-thread) - Add cs_addrName lock for pfrom->addr write in torauth onion proof handler - Fix race in duplicate detection: set fTorAuthenticated inside cs_vNodes scope LOW: - Remove BIP155-violating Misbehaving(20) for sendaddrv2-after-verack - Extract TorAuthSign() helper to deduplicate signing code - Replace magic numbers with named constants (misbehaving scores, timeout) - Deduplicate ToStringIP() onion path with OnionAddressFromEd25519Pubkey() - GetIn6Addr() returns false for Tor addresses instead of zeroed in6_addr Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BOOST_FOREACH replaced with range-based for in torauth duplicate detection. Restored key material erasure comment stripped during conflict resolution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Master already shipped PROTOCOL_VERSION 170021 in v9.1.0, so current mainnet peers present 170021 without understanding BIP155 messages. The first binary that understands addrv2 is this combined tor+PON-VRF build at 170022, so announce sendaddrv2 only to peers >= 170022. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
anchors.dat (a pre-existing feature) serialized CAddress in the legacy V1 format, which cannot represent TORv3 addresses: a saved onion anchor deserialized as [::], so anchor reconnection after a restart silently degraded (and on Linux accidentally dialed localhost, since connecting to the unspecified address loops back). Serialize anchors with the BIP155 type-tagged CAddrVecV2 encoding behind a format byte; unknown or legacy-format files are removed and ignored gracefully, as anchors are only a best-effort reconnection hint. This was on the build journals known-deferred list. qa/tor-smoke-test.sh is the two-node onion smoke test the journal calls for: private tor instance, ephemeral v3 hidden service via torcontrol, onion connect through SOCKS, sendaddrv2 negotiation both directions, block propagation over the onion link, and the anchors.dat onion round-trip + reconnect after restart. Verified passing end to end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…endcmpct The flag and the torauth-initiation block sat at the end of the optional BIP152 sendcmpct handler, so a peer that never sends sendcmpct completed version+verack yet was never marked successfully connected: excluded from anchors.dat, no 24h AdvertizeLocal rebroadcast, and never challenged for torauth. Move both to the end of the verack handler (matches upstream). As a side effect a sendaddrv2 arriving after verack is now correctly rejected, since fSuccessfullyConnected is set at verack rather than at sendcmpct. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rough GetEffectiveAddr The torauthresp handler reassigned pfrom->addr/addrName in place when it verified a peer's onion proof. Since CNetAddr now stores a heap std::vector<unsigned char>, that reassignment frees the old buffer while other threads read addr/addrName unlocked (ThreadSocketHandler, ThreadOpenConnections, eviction grouping, FindNode), so it could tear a read or use freed memory; the pre-vector ip[16] storage made the same reassignment benign. Make addr/addrName write-once at connect and record the verified onion in a new write-once CNode::torVerifiedAddr (published via std::atomic<bool> fTorAddrVerified under cs_addrName). Consumers that need the onion identity read it through GetEffectiveAddr()/GetEffectiveAddrName() (getpeerinfo and eviction grouping); everything else reads the now-immutable socket addr, so the race is gone. Behavior is otherwise preserved: a verified onion is still recorded for both inbound and outbound peers. Binding the proof to the dialed address (outbound), banning via the verified onion, and recording real services/time build on this getter in follow-up commits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on detection Inbound connections delivered by a Tor hidden service arrive from 127.0.0.1, indistinguishable from any other local connection. The code inferred "this is a Tor peer" from fInbound && addr.IsLocal(), so any loopback peer of a fluxnode (an ssh tunnel, a local monitor, a second daemon) was challenged for torauth and force-disconnected after the 60s timeout. Give the hidden service its own local target port instead of guessing. The Tor controller advertises the standard P2P port on the onion but forwards it to 127.0.0.1:(listenport+1), and we bind that port exclusively for hidden-service traffic (mirroring Bitcoin Core's -bind=...=onion). A connection accepted on that socket is provably an onion peer: CNode::fInboundOnion is set once at accept from the listening socket, the torauth challenge gate keys off it, and getpeerinfo reports such peers as network=onion. No localhost heuristic remains. The onion bind is best-effort: if listenport+1 is unavailable it logs a warning and inbound-onion detection is disabled rather than aborting the node. qa/tor-smoke-test.sh now asserts the hidden-service node tags its inbound peer as network=onion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
listenport+1 can collide with another local service — e.g. an RPC port set just above the P2P port — and on collision the dedicated bind failed while the Tor controller still forwarded the onion to that dead port, silently breaking inbound onion connectivity. Bind 127.0.0.1:0 instead, let the OS assign a free port, record it (GetOnionLocalPort), and have the controller forward the onion service to that port. Collision-proof; falls back to the P2P port if no dedicated onion bind is active. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The torauthresp handler recorded a peer's proven .onion for any valid proof, including outbound connections, without checking it was the onion we dialed. A registered fluxnode controlling a different onion could answer our outbound connection with a valid proof for its own address and relabel the connection, poisoning anchors.dat, duplicate detection, and the onion-outbound cap. For outbound peers, require the derived onion to equal the dialed address (compared as CNetAddr, ignoring port) before recording it; otherwise Misbehaving and leave the connection labelled with what we dialed. Inbound peers arrive as 127.0.0.1 and legitimately learn their onion from the proof, so they are unaffected. Adds src/gtest/test_net.cpp covering the GetEffectiveAddr contract (socket address before verification, verified onion after) that this path feeds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Banning an onion peer was a no-op: CSubNet forced the onion through the IPv4/IPv6 netmask normalization, which cannot represent it and zeroed it to ::/128, and Match() returned false for any .onion outright. So the torauth Misbehaving scores this branch adds had no ban effect against the very peers they target — a banned onion reconnected freely. Treat a non-IP CSubNet (e.g. v3 onion) as a single host: keep the address intact in the constructor (skip the IPv4/IPv6 normalization), match by exact equality in Match(), and print it without a netmask in ToString(). Ban() and the outbound IsBanned(addrConnect) check then work for onions unchanged. An inbound hidden-service peer arrives as 127.0.0.1, so additionally check the ban list against the proven onion once torauth verifies it and disconnect. Adds NetTests.OnionSubnetMatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…atch anchors.dat is a one-shot reconnection hint, but a corrupt or legacy-format file was only removed on a format-byte mismatch or after a successful read; a checksum mismatch or a deserialize exception returned an error without removing it, so such a file re-errored on every startup (a legacy 2-anchor file fails the checksum check first). Remove the file as soon as its bytes are read into memory, so every subsequent validation path (checksum, magic, format, wrapper) runs on the in-memory copy and leaves no file behind; also remove it in the raw-read catch. Drops the now-redundant per-branch removals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GetGroup() returned {NET_ONION} for every v3 onion, placing all onions in a
single netgroup (the comment wrongly claimed this matched Bitcoin Core). With
addrman's small per-group tried-bucket budget, an operator could cheaply
advertise many onions to monopolise that one group and evict honest onion
entries. Group onions by the top 4 bits of the ed25519 pubkey (16 buckets),
mirroring upstream.
Adds NetTests.OnionNetGroup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two public-Tor-network dependencies made the smoke test fail spuriously: a cold tor bootstrap (the DataDirectory was wiped every run) and the post-restart anchor reconnect (a single SOCKS rendezvous failure was fatal because the poll never re-issued the connection). Preserve the tor DataDirectory across runs so the consensus is cached (clearing only tor.log so the bootstrap check can't false-pass on a stale line), extend the bootstrap window to 180s, and re-issue the connection on each reconnect poll iteration. The deterministic anchor round-trip assertion is unchanged, so anchor-logic coverage is retained; only the Tor-circuit-dependent step is made resilient. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cached Tor key file was loaded and, if non-empty, used directly: the seed is read as a fixed 32 bytes by crypto_hash_sha512 and crypto_sign_seed_keypair. A truncated/corrupt file (or a legacy "ED25519-V3:<base64>" key) is non-empty but not 32 bytes, so it was read out of bounds and cached as a garbage keypair marked available. Validate the loaded key is exactly crypto_sign_ed25519_SEEDBYTES; otherwise wipe and discard it so a fresh seed is generated (overwriting the bad file), the same path as a missing key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The onion outbound cap tested ends_with(".onion") on the raw destination string,
but addnode/-connect strings keep their :port suffix, so "host.onion:port" did
not match and bypassed the cap (and -connect/one-shots to an onion:port). Split
the host:port first and test the host, matching the stated intent of covering
both the addrman and addnode paths.
Adds NetTests.OnionDestPortSuffix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CAddress::UnserializeV2 read the BIP155 services field with the range-checked ReadCompactSize, which throws when the value exceeds MAX_SIZE (2^25). But services is a 64-bit flag field, not a length — a peer advertising any bit >= 26 (legal per BIP155, and craftable) made the read throw, dropping the entire addrv2 batch (and aborting anchors.dat V2 load). Add a range_check parameter to ReadCompactSize (default true, preserving every existing call) and pass false for the services field. Wire format unchanged. Adds NetTests.Addrv2HighServiceBits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 64-byte onion-proof payload (challenge || outpoint.hash) was memcpy-assembled independently at the sign and verify sites; a one-sided edit would compile cleanly and break every onion proof network-wide. The challenge arm-and-record triple was likewise duplicated in two handlers. Extract BuildTorAuthPayload() and ArmTorAuthChallenge() and route all sites through them. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CAddrVecV2::Unserialize enforces the BIP155 bounds (max 1000 entries, 512-byte payload) by throwing, which fell through to the generic ProcessMessage handler that only logs — so an oversized addrv2 escaped the Misbehaving(20) that an oversized v1 addr message incurs. Catch the failure in the addrv2 path and apply the same score. Not exploitable (the message is rejected either way), but consistent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GetEffectiveAddr() returned CAddress(torVerifiedAddr) with the CAddress defaults (NODE_NETWORK, nTime=100000000), so the verified onion carried wrong services and time if ever persisted (e.g. anchors.dat). Construct it with the peer's nServices and a current timestamp. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Ban inbound onion peers by their verified .onion, not the 127.0.0.1 they connect from: route both misbehavior-ban sites through a shared CNode::OnionAwareBanTarget() so a hidden-service peer is actually bannable and a loopback address is never self-banned. - Use GetEffectiveAddr() as the addrman source for onion-learned gossip. - Snapshot eviction-candidate netgroups once before sorting so a concurrent torauth completion cannot change a sort key mid-sort. - Disable the Tor hidden service entirely when the dedicated onion bind fails, and close the socket on a getsockname failure (no orphan listener). - Make anchors.dat file_size + removes non-throwing so a corrupt or unremovable file cannot abort startup; persist the verified onion (real services/time). - Align the torauth timeout predicate with the dedicated-bind arming; refresh stale comments; document nServices handshake-immutability. Adds NetTests.OnionAwareBanTarget and NetTests.OnionBanRoundTrip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A node now stands up a Tor hidden service only when explicitly configured with -listenonion=1, rather than whenever a Tor control port happens to be reachable. The production image enables the control port on every node, so an on-by-default would make even directly-reachable hub nodes create a hidden service — which they must not, or their .onion enters the gossip pool and steals NAT nodes' scarce onion-outbound slots. Nodes that should be reachable over Tor opt in explicitly; configd emits listenonion=1 for NAT nodes and listenonion=0 for hubs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OpenNetworkConnection re-checks the onion outbound cap (added for the addnode/-connect paths) but did not exempt feelers, so on a maxonionoutbound=0 hub every onion feeler was selected by ThreadOpenConnections and then rejected here (0 >= 0). Hubs therefore never feeler-verified onion addresses — the addrman hygiene the hub role depends on. Route both cap sites through a shared OnionOutboundCapReached(fFeeler, nOnionOut) so the exemption lives in one place and the two checks cannot diverge again. +1 gtest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
doc/tor.md: the ephemeral hidden service is now off by default (opt-in via -listenonion=1), matching DEFAULT_LISTEN_ONION=false. doc/files.md: the cached key is onion_v3_private_key. Add doc/tor-production-deployment.md documenting the NAT/hub production model — one shared torrc, the per-node flux.conf split (hub listenonion=0), and the security analysis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
doc/build-journal-bip155-torv3.md (an implementation journal — its own words, "the engineering record... not marketing material") and its companion doc/build-test-onion-linux.md (a build-and-test walkthrough written around the dev phases) are development process notes, not shipped documentation. Drop both, and remove the deployment doc's cross-reference to the journal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DEFAULT_LISTEN_ONION is false, so the -listenonion help text reads default: 0. (The manpage is generated; this matches what a regeneration produces.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…inistically Onion outbound connections shared the single 16-slot outbound semaphore with clearnet. On a node already at the outbound cap, fast clearnet dials refilled every freed slot before a slow onion dial or the added-node thread could take one, so persistent onion peers were starved; only `addnode <onion> onetry`, which takes no semaphore grant, would connect. Hold clearnet outbound below MAX_OUTBOUND_CONNECTIONS - nMaxOnionOutbound whenever the onion proxy is configured, mirroring the existing onion cap, so the onion reservation is always reachable. Clearnet-only nodes are unaffected and feelers stay exempt. When two registered fluxnodes both dial each other over onion, each end saw a duplicate and dropped its own inbound connection -- but one end's inbound is the outbound the other end keeps, so both connections were torn down. Choose which to drop from the two fluxnode outpoints, known to both ends after mutual torauth: the lower outpoint's owner keeps its outbound and the peer keeps its inbound, so both ends elect the same connection and exactly one survives. Adds gtests for the clearnet reservation cap and the dedup convergence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DecrementNoteWitnesses only touches a note when its witnessHeight is at or below the decremented height, so the test's assertion that a reorg leaves the witness cache and final anchor untouched holds only when the decrement is strictly below the tip. The test decremented at heights 5 and 50; with MAX_REORG_LENGTH=40 the chain is WITNESS_CACHE_SIZE + 10 = 51 blocks, so 50 is the tip and the decrement pops the live witness. Derive the deeper decrement height from WITNESS_CACHE_SIZE so it stays below the tip regardless of the reorg-length constant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each Write*DirectToDb test drives a real on-disk wallet in its own temporary datadir, but the global BerkeleyDB environment (bitdb) opens once and CDBEnv::Open no-ops afterward, so every test after the first silently landed in the first test's environment. Accumulated environment state across their repeated LoadWallet/EncryptWallet-rewrite cycles intermittently tripped whichever test ran when it was most loaded, so a different wallet-DB test failed run to run while each still passed in isolation. Close and recreate the environment and clear the datadir cache at the start of each test so it opens a fresh environment in its own datadir. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MorningLightMountain713
marked this pull request as ready for review
July 3, 2026 07:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR #284 must merge first. This branch is based on
feat/pon-vrf-integrationand shares its protocol bump (170022) — BIP155addrv2/sendaddrv2is negotiated against that version. It must land after the PoN-VRF integration and will not apply to master until #284 does.Why
Roughly 80% of fluxnodes are behind NAT and can only make outbound connections. The ~20% with public IPs become de-facto hubs, producing a hub-and-spoke topology:
NAT → hub → NAT.Tor v3 hidden services give a NAT node a globally reachable inbound address. Two NAT nodes that could never connect directly can now peer through Tor, flattening the hub-and-spoke topology into a mesh. The goal is network resilience, not privacy.
What this adds
addrv2/sendaddrv2— gossip and store v3 onion (and other modern) addresses.sendaddrv2is negotiated betweenversionandverack.ADD_ONION, SAFECOOKIE auth); the node's.onionis advertised to peers..onionpeers through Tor's SOCKS proxy (-onion).-maxonionoutboundreserves that many of the outbound connection slots for onion; clearnet outbound is held to the remainder, so a node at its outbound limit keeps its onion peers instead of letting faster clearnet dials fill every slot. (A public-IP hub setsmaxonionoutbound=0to reserve none.)CNode::fInboundOnion) — no127.0.0.1heuristic, so ordinary loopback peers (ssh tunnels, monitors) are never misclassified.torauth) — see below.anchors.dat(persist and reconnect to onion anchors across restart), onion netgroup bucketing, and onion bannability.fluxnode Tor authentication (
torauth)A
.onionaddress is just a 32-byte ed25519 public key — anyone can generate one. To make onion peers Sybil-resistant, fluxnodes authenticate each other with a challenge/response (torauthreq→torauthresp) that binds a peer's network identity (its.onion) to its fluxnode identity (a registered collateral outpoint):challenge || signer_outpoint_hash..onionmust equal the address we dialed, so a registered fluxnode cannot relabel its connection to another node's.onion(which would otherwise poison anchors, dedup, and the onion cap)..onion(single-host subnet) through the normal misbehavior path.Node roles & configuration
The daemon supports both roles through configuration:
Every node ships the same static
torrc(SocksPort 127.0.0.1:9050+ControlPort 127.0.0.1:9051+CookieAuthentication 1+CookieAuthFileGroupReadable 1+ExitPolicy reject *:*); the NAT/hub split lives entirely influx.conf:NAT node (hidden service + SOCKS) —
flux.conf:listenonion=1,torcontrol=127.0.0.1:9051,onion=127.0.0.1:9050. Accepts inbound onion peers and makes a small number of onion outbound connections plus clearnet connections to hubs for fast block relay.Public-IP hub (SOCKS only) —
flux.conf:onion=127.0.0.1:9050,maxonionoutbound=0. The hidden service is opt-in (listenoniondefaults off), so a hub simply leaves it unset and stands up no.onionof its own — it never drops a hub.onioninto the gossip pool or competes for NAT nodes' scarce onion-outbound slots. It still makesNET_ONIONreachable so it stores, relays, and feeler-verifies onion addresses — without hubs as connective tissue, onion gossip dies.Authentication to the control port uses SAFECOOKIE (challenge-response) — no plaintext password in
flux.confand noHashedControlPasswordintorrc. fluxd is added to the Tor cookie group (debian-toron Debian/Ubuntu) on every node at image build, but only NAT nodes (those withtorcontrol=set) open a control connection; hubs leave the control port unused.Testing & review
src/gtest/test_net.cpp(NetTests): effective-address resolution before/after auth, onion subnet/ban matching, onion netgroup spread, addrv2 high-service-bit round-trip, the onion-aware ban decision + ban→IsBanned round-trip, the feeler exemption from the onion outbound cap, the clearnet/onion outbound reservation, and the duplicate-connection tiebreaker. Fullflux-gtestgreen.qa/tor-smoke-test.shruns two nodes over real Tor: hidden-service creation, outbound onion connect, addrv2 exchange, inbound onion tagging via the dedicated bind, block propagation over the onion link, and ananchors.datround-trip + reconnect after restart.torauthhandler flow requires two registered fluxnodes, which regtest cannot represent, so it is validated on a live pair: mutual authentication and onion-proof verification, the duplicate-connection collapse, and onion peers auto-connecting into their reserved slots. A fluxnode-regtest harness to unit-test the handler flow is a follow-up.Compatibility & follow-ups
sendaddrv2is negotiated, so pre-bump peers are unaffected.