Repository navigation
fix(net): keep a dedicated onion listener when -bind is given and correct the bind release note - #7786
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 🧰 Additional context used📚 Code guidelines (1)No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (5)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review. WalkthroughWhen Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~12 minutes Merge Risk: ⚪ Minimal · up to With onion listening enabled, explicit binds must include a dedicated non-wildcard onion bind; this startup requirement is documented and covered by functional-test expectations. No actionable merge-blocking risk remains. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change removes a privacy-sensitive fallback and fails closed when a dedicated onion listener is missing. Existing configurations using explicit binds may require an update before restarting, including nodes without Tor configured. No introduced security finding was identified, but live Tor recovery and deployment behavior were not exercised. Retained concerns Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 3 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Final review complete — no blockers (commit 5f40d56) · triage: low |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Static verification at f8b56ba confirms that the automatic Tor service targets an onion-classified listener, the explicit-bind opt-out remains valid, and the documentation and regression tests match the implemented behavior. Both reviewers' clean assessments are supported by the source; no actionable in-scope defects or prerequisite claims were identified. No builds or tests were run in this lane: the supplied CI snapshot shows successful container-build and formatting checks, queued manifest jobs, and no completed functional-test results.
Review provenance
Source: reviewer 1: glm-5.3-flash (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
lowbygpt-6.1-sol(effort low) — The diff is a small, contained listener-selection fix in src/init.cpp with focused functional-test updates and a release-note correction, not a large or intricate change to a critical surface. - Phase 1 reviewers:
glm-5.3-flash— general (completed, effort high); agentphase1-reviewer - Phase 1 model:
glm-5.3-flash— zai quota: 5h 99% left, weekly 69% left; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 15% left, 5h 100% left) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— dash-core-commit-history (completed, effort medium); agentphase2-reviewer
Since bitcoin#22729 (backported in dashpay#7300), an explicit -bind=<addr> without =onion made the automatically created Tor onion service forward to the first -bind address instead of the dedicated 127.0.0.1:9996 listener. Connections arriving there are not in onion_binds, so peers reaching the node over its onion address were classified by their source address (Tor's loopback, or the node's own IP) rather than as onion. GetLocal() then offered them the node's clearnet addresses in self-advertisement, linking the onion service to the clearnet endpoint. Only skip the default onion bind when -bind is given and -listenonion is disabled, which keeps the ability to avoid the extra bind. Otherwise always add it to onion_binds and use it as the onion-service target. feature_proxy.py starts a node with -listenonion=1 on top of the framework's bind=127.0.0.1; give it an explicit =onion bind on its tor_port() so it does not now also bind the fixed regtest onion target port that feature_bind_extra.py uses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
-listenonion=0 on its own never dropped the 127.0.0.1:9996 bind. Describe the actual rule: it is skipped only when -bind is given together with -listenonion=0, and -whitebind alone does not affect it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
f8b56ba to
72b3c71
Compare
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Verified the complete diff at 5f40d56 and found no blocking correctness defects: automatic Tor now requires a dedicated, non-wildcard onion bind when explicit binds are configured. Four non-blocking issues remain concerning the stale PR description, omitted early validation, undocumented partial-backport exclusions, and wildcard-bind release notes. This was static verification only; the supplied CI snapshot shows lint passing, with source builds queued or running and completed build/test validation unavailable.
🟡 4 suggestion(s)
Review provenance
Source: reviewer 1: glm-5.3-flash (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: backport-reviewer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: backport-reviewer); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
lowbygpt-6.1-sol(effort low) — The diff makes a small, contained change to Tor listener initialization and startup validation in src/init.cpp, with focused tests and documentation rather than intricate changes to a critical surface. - Phase 1 reviewers:
glm-5.3-flash— general (completed, effort high); agentphase1-reviewer - Phase 1 model:
glm-5.3-flash— zai quota: 5h 98% left, weekly 99% left; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 15% left, 5h 100% left) - Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— backport-reviewer (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— dash-core-commit-history (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— backport-reviewer (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— dash-core-commit-history (completed, effort medium); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/init.cpp`:
- [SUGGESTION] src/init.cpp:2679-2689: PR description no longer matches the head commit's behavior
The PR description still says explicit -bind configurations retain the implicit onion listener and fail only if its port is unavailable. At this head, the default listener is added only when both bind vectors are empty; an explicit normal bind without an onion bind instead causes an unconditional startup error when -listenonion is enabled. Wildcard onion binds are also rejected now, although the description says that change is deferred. The documented node-3 regression scenario and its network=='onion' assertion were removed by the final commit. Update the implementation rationale, upstream relationship, breaking changes, and test evidence to describe the final policy, distinguishing earlier implementation results from validation of this head. These differences change the operator migration requirements, not just the wording.
- [SUGGESTION] src/init.cpp:2683-2685: Declared-partial omission: early Tor bind validation
Upstream bitcoin#36170 rejects explicit bind configurations without an =onion suffix in AppInitParameterInteraction, while retaining the defensive check here. Dash includes only this late check: LoadChainstate and wallet loading occur before it. Consequently, a configuration already known to be invalid can spend substantial time loading chainstate and wallets before reporting the dedicated-bind error. Carry the upstream early check, or explain its intentional exclusion from this partial backport. The current check preserves eventual rejection and the privacy fix; this is not a blocker or a missing API prerequisite.
In `test/functional/feature_bind_extra.py`:
- [SUGGESTION] test/functional/feature_bind_extra.py:90-105: Declared-partial omission: two upstream Tor test adaptations
The upstream diff also adds a dedicated onion bind in feature_torcontrol.py::restart_with_mock() and in the Tor-only scenario in p2p_private_broadcast.py. Their containing tests were introduced by bitcoin#34158 (569383356ecd5baa65a87563d776809a63f8f7a3) and bitcoin#29415 (e74d54e04896a86cad4e4b1bd9641afcc3a026c2), respectively, and neither file exists in this Dash base or head. This does not break an existing Dash test or establish a production dependency. However, CONTRIBUTING.md requires Bitcoin backports to explain omitted hunks and tests, and the final commit has only a subject while the PR description describes the superseded implementation. Document these two exclusions and their missing containing tests in the PR description or a backport note, and identify the upstream source revision used. Adding the unrelated prerequisite features is not required to resolve this documentation issue.
In `doc/release-notes-7300.md`:
- [SUGGESTION] doc/release-notes-7300.md:8-16: Release-note adaptation omits wildcard onion binds
The release note explains rejection when no onion bind is supplied, but omits rejection of an existing wildcard onion bind such as -bind=0.0.0.0:<port>=onion. That configuration satisfies the note's stated requirement for an =onion bind yet now fails startup when -listenonion is enabled. Upstream explicitly mentions shared and wildcard binds, and this head's help text and doc/tor.md require a non-wildcard address. Add the wildcard compatibility change here and state that the dedicated onion bind must use a specific, non-wildcard address.
| if (connOptions.onion_binds.empty() && connOptions.vBinds.empty()) { | ||
| connOptions.onion_binds.push_back(DefaultOnionServiceTarget()); | ||
| } | ||
|
|
||
| if (args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) { | ||
| if (connOptions.onion_binds.empty()) { | ||
| return InitError(_("The automatic Tor onion service requires a dedicated onion bind. Use a specific address such as -bind=127.0.0.1:<port>=onion, or disable the service with -listenonion=0.")); | ||
| } | ||
| if (std::ranges::any_of(connOptions.onion_binds, [](auto& b) { return b.IsBindAny(); })) { | ||
| return InitError(_("The automatic Tor onion service cannot use a wildcard onion bind because the Tor daemon wouldn't be able to forward incoming connections to us. Use a specific address such as -bind=127.0.0.1:<port>=onion, or disable the service with -listenonion=0.")); | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: PR description no longer matches the head commit's behavior
The PR description still says explicit -bind configurations retain the implicit onion listener and fail only if its port is unavailable. At this head, the default listener is added only when both bind vectors are empty; an explicit normal bind without an onion bind instead causes an unconditional startup error when -listenonion is enabled. Wildcard onion binds are also rejected now, although the description says that change is deferred. The documented node-3 regression scenario and its network=='onion' assertion were removed by the final commit. Update the implementation rationale, upstream relationship, breaking changes, and test evidence to describe the final policy, distinguishing earlier implementation results from validation of this head. These differences change the operator migration requirements, not just the wording.
source: glm-5.3-flash (phase1-reviewer: general)
| if (args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) { | ||
| if (connOptions.onion_binds.empty()) { | ||
| return InitError(_("The automatic Tor onion service requires a dedicated onion bind. Use a specific address such as -bind=127.0.0.1:<port>=onion, or disable the service with -listenonion=0.")); |
There was a problem hiding this comment.
🟡 Suggestion: Declared-partial omission: early Tor bind validation
Upstream bitcoin#36170 rejects explicit bind configurations without an =onion suffix in AppInitParameterInteraction, while retaining the defensive check here. Dash includes only this late check: LoadChainstate and wallet loading occur before it. Consequently, a configuration already known to be invalid can spend substantial time loading chainstate and wallets before reporting the dedicated-bind error. Carry the upstream early check, or explain its intentional exclusion from this partial backport. The current check preserves eventual rejection and the privacy fix; this is not a blocker or a missing API prerequisite.
source: gpt-6.1-sol (phase2-reviewer: general, backport-reviewer, dash-core-commit-history)
| self.log.info("Test -listenonion with a normal bind and no dedicated onion bind") | ||
| self.stop_node(2) | ||
| self.nodes[2].assert_start_raises_init_error( | ||
| self.extra_args[2] + ["-listenonion=1", "-torcontrol=127.0.0.1:1"], | ||
| "Error: The automatic Tor onion service requires a dedicated onion bind. Use a specific address such as -bind=127.0.0.1:<port>=onion, or disable the service with -listenonion=0.", | ||
| ) | ||
|
|
||
| self.log.info("Test -bind with dedicated onion bind starts when -listenonion=1") | ||
| self.restart_node(1, extra_args=self.extra_args[1] + ["-listenonion=1", "-torcontrol=127.0.0.1:1"]) | ||
|
|
||
| self.log.info("Test wildcard onion bind with -listenonion=1") | ||
| self.stop_node(0) | ||
| self.nodes[0].assert_start_raises_init_error( | ||
| [f"-bind=0.0.0.0:{p2p_port(0)}=onion", "-listenonion=1", "-torcontrol=127.0.0.1:1"], | ||
| "Error: The automatic Tor onion service cannot use a wildcard onion bind because the Tor daemon wouldn't be able to forward incoming connections to us. Use a specific address such as -bind=127.0.0.1:<port>=onion, or disable the service with -listenonion=0.", | ||
| ) |
There was a problem hiding this comment.
🟡 Suggestion: Declared-partial omission: two upstream Tor test adaptations
The upstream diff also adds a dedicated onion bind in feature_torcontrol.py::restart_with_mock() and in the Tor-only scenario in p2p_private_broadcast.py. Their containing tests were introduced by bitcoin#34158 (5693833) and bitcoin#29415 (e74d54e), respectively, and neither file exists in this Dash base or head. This does not break an existing Dash test or establish a production dependency. However, CONTRIBUTING.md requires Bitcoin backports to explain omitted hunks and tests, and the final commit has only a subject while the PR description describes the superseded implementation. Document these two exclusions and their missing containing tests in the PR description or a backport note, and identify the upstream source revision used. Adding the unrelated prerequisite features is not required to resolve this documentation issue.
source: glm-5.3-flash (phase1-reviewer: general); gpt-6.1-sol (phase2-reviewer: general, backport-reviewer, dash-core-commit-history)
| * Nodes configured with `-bind` but no specific `-bind=<addr:port>=onion` now | ||
| refuse to start when `-listenonion` is enabled. This includes nodes without | ||
| Tor configured, since `-listenonion` is enabled by default when listening. | ||
| Shared binds cannot distinguish Tor-forwarded connections from direct | ||
| connections, which can grant Tor peers unintended IP-based whitelist | ||
| permissions. Nodes without `-bind`, including those using only `-whitebind`, | ||
| continue to get the default onion target. Users should add a specific | ||
| `-bind=<addr:port>=onion` to accept incoming Tor connections, or set | ||
| `-listenonion=0` to disable automatic onion service creation. |
There was a problem hiding this comment.
🟡 Suggestion: Release-note adaptation omits wildcard onion binds
The release note explains rejection when no onion bind is supplied, but omits rejection of an existing wildcard onion bind such as -bind=0.0.0.0:=onion. That configuration satisfies the note's stated requirement for an =onion bind yet now fails startup when -listenonion is enabled. Upstream explicitly mentions shared and wildcard binds, and this head's help text and doc/tor.md require a non-wildcard address. Add the wildcard compatibility change here and state that the dedicated onion bind must use a specific, non-wildcard address.
source: gpt-6.1-sol (phase2-reviewer: general, backport-reviewer, dash-core-commit-history)
…given and correct the bind release note 5f40d56 partial Merge bitcoin#36170: net: require a dedicated bind for automatic Tor (Konstantin Akimov) 72b3c71 docs: correct when the implicit onion bind is added in release notes (pasta) 5bb8eb3 fix(net): keep a dedicated onion listener when -bind is given (pasta) Pull request description: ## Issue being fixed or feature implemented The backport of bitcoin#22729 (#7300) changed what happens when a node is started with an explicit `-bind=<addr>` but no `-bind=...=onion`. Before, Dash Core always added the dedicated onion listener `127.0.0.1:9996` (`19996` testnet, `19896` regtest) and pointed the automatically created Tor onion service (`-listenonion`, on by default) at it. Now it binds no onion listener and points the onion service at the first `-bind` address. That listener is not in `onion_binds`, so every peer Tor forwards to it is classified by its source address: Tor's loopback address, or the node's own IP when `-bind` is a routable address. It is not classified as an onion peer. As a result, a node that runs an onion service next to a clearnet address (`-externalip`, or a discovered/bound routable address) advertises its clearnet address to peers that reached it through its `.onion` address. That links the two identities, which the privacy-network check in `GetLocal()` is meant to prevent. Those peers also lose the rest of the onion-specific handling, such as `getpeerinfo` `network` and eviction protection for onion peers. The release note added for #7300 has a related problem. It says `-listenonion=0` drops the implicit onion bind, but it never did: the bind is added before `-listenonion` is looked at. An operator whose `127.0.0.1:9996` is taken who follows the note still gets a node that refuses to start. Both changes are only in v24.0.0-rc.1 and rc.2. ### Why this is a real problem Code path at c14104b: - [`src/init.cpp#L2678-L2695`](https://github.com/dashpay/dash/blob/c14104b172a2/src/init.cpp#L2678-L2695): with `vBinds` non-empty and no `=onion` bind, `onion_service_target = connOptions.vBinds.front()` ([L2682](https://github.com/dashpay/dash/blob/c14104b172a2/src/init.cpp#L2682)). It is not pushed into `onion_binds`, and `StartTorControl(onion_service_target)` still runs ([L2694](https://github.com/dashpay/dash/blob/c14104b172a2/src/init.cpp#L2694)). - [`src/net.h#L1291`](https://github.com/dashpay/dash/blob/c14104b172a2/src/net.h#L1291) copies only `onion_binds` into `m_onion_binds`. [`src/net.cpp#L1977`](https://github.com/dashpay/dash/blob/c14104b172a2/src/net.cpp#L1977) sets `inbound_onion` only for listeners in `m_onion_binds`. - [`src/net.cpp#L584-L592`](https://github.com/dashpay/dash/blob/c14104b172a2/src/net.cpp#L584-L592): without `m_inbound_onion`, `ConnectedThroughNetwork()` and `IsConnectedThroughPrivacyNet()` fall back to the peer's source address. The privacy filter in `GetLocal()` ([L186-L193](https://github.com/dashpay/dash/blob/c14104b172a2/src/net.cpp#L186-L193)) then skips the node's own onion address for that peer and lets the clearnet one through. I reproduced this on the unfixed build with a small functional-test-style script. A minimal fake Tor control port accepts `PROTOCOLINFO`/`AUTHENTICATE`/`ADD_ONION` and records the target dashd asks Tor to forward to. A P2P peer then connects to exactly that target, as Tor does for every inbound onion connection, and sends `getaddr`. The node runs with `-bind=127.0.0.1:<port> -listenonion=1 -externalip=1.2.3.4`. Unfixed (c14104b): ``` Tor control commands received: ['PROTOCOLINFO 1', 'AUTHENTICATE', 'GETINFO net/listeners/socks', 'ADD_ONION NEW:ED25519-V3 Port=19899,127.0.0.1:17626'] onion-service target handed to Tor: 127.0.0.1:17626 getnetworkinfo localaddresses: [{'address': '1.2.3.4', 'port': 17626, 'score': 4}, {'address': 'pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion', 'port': 19899, 'score': 4}] Connecting a peer to 127.0.0.1:17626, as Tor would for an inbound onion connection getpeerinfo: addr=127.0.0.1:63442 addrbind=127.0.0.1:17626 network=not_publicly_routable addresses self-advertised to this peer: ['1.2.3.4:17626'] ``` The onion service forwards to the clearnet listener, the forwarded peer is `not_publicly_routable` instead of `onion`, and it is sent the node's clearnet address `1.2.3.4`. In the fixed run the list is empty for two reasons. The privacy filter withholds `1.2.3.4` from an onion peer, and the test `P2PInterface` never sends `sendaddrv2`, so the node cannot send it the v3 onion address either. The fixed run also prints a `wait_until() failed` line after the 10 s wait for addresses; that line is left out of the excerpt above. With this PR: ``` Tor control commands received: ['PROTOCOLINFO 1', 'AUTHENTICATE', 'GETINFO net/listeners/socks', 'ADD_ONION NEW:ED25519-V3 Port=19899,127.0.0.1:19896'] onion-service target handed to Tor: 127.0.0.1:19896 getnetworkinfo localaddresses: [{'address': '1.2.3.4', 'port': 18132, 'score': 4}, {'address': 'pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion', 'port': 19899, 'score': 4}] Connecting a peer to 127.0.0.1:19896, as Tor would for an inbound onion connection getpeerinfo: addr=127.0.0.1:50581 addrbind=127.0.0.1:19896 network=onion addresses self-advertised to this peer: [] ``` <details><summary>Reproduction script (run with <code>WORKTREE=<src root> python3 repro_tor_target.py --configfile=<src root>/test/config.ini</code>)</summary> ```python #!/usr/bin/env python3 """Reproduction: with an explicit -bind (no =onion) and -listenonion=1, which listener does dashd hand to Tor as the onion-service target, and how is a peer that Tor forwards to that target classified? A minimal fake Tor control port answers PROTOCOLINFO/AUTHENTICATE/ADD_ONION and records the target from the ADD_ONION command. The test then connects a P2P peer to exactly that target (what Tor does for every inbound onion connection), prints getpeerinfo's network field, sends getaddr and prints the addresses the node self-advertises to that peer. Run from anywhere: python3 repro_tor_target.py --configfile=<worktree>/test/config.ini """ import os import socket import sys import threading WORKTREE = os.environ["WORKTREE"] sys.path.insert(0, os.path.join(WORKTREE, "test", "functional")) from test_framework.messages import msg_getaddr # noqa: E402 from test_framework.p2p import P2PInterface # noqa: E402 from test_framework.test_framework import BitcoinTestFramework # noqa: E402 from test_framework.util import p2p_port # noqa: E402 ONION_ID = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd" EXTERNAL_IP = "1.2.3.4" class FakeTorControl: def __init__(self): self.srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.srv.bind(("127.0.0.1", 0)) self.srv.listen(1) self.port = self.srv.getsockname()[1] self.commands = [] self.target = None threading.Thread(target=self._serve, daemon=True).start() def _serve(self): conn, _ = self.srv.accept() f = conn.makefile("rwb", buffering=0) while True: line = f.readline() if not line: return cmd = line.decode().strip() self.commands.append(cmd) if cmd.startswith("PROTOCOLINFO"): f.write(b'250-PROTOCOLINFO 1\r\n250-AUTH METHODS=NULL\r\n250-VERSION Tor="0.4.8.12"\r\n250 OK\r\n') elif cmd.startswith("ADD_ONION"): # ADD_ONION NEW:ED25519-V3 Port=<virtport>,<target> self.target = cmd.split("Port=")[1].split(",", 1)[1] f.write(f"250-ServiceID={ONION_ID}\r\n250-PrivateKey=ED25519-V3:AAAA\r\n250 OK\r\n".encode()) else: f.write(b"250 OK\r\n") class AddrCollector(P2PInterface): def __init__(self): super().__init__() self.addrs = [] def on_addr(self, message): self.addrs += [f"{a.ip}:{a.port}" for a in message.addrs] def on_addrv2(self, message): self.addrs += [f"{a.ip}:{a.port}" for a in message.addrs] class ReproTorTarget(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.bind_to_localhost_only = False self.num_nodes = 1 def setup_network(self): self.tor = FakeTorControl() self.extra_args = [[ f"-bind=127.0.0.1:{p2p_port(0)}", "-listenonion=1", f"-torcontrol=127.0.0.1:{self.tor.port}", f"-externalip={EXTERNAL_IP}", "-debug=tor", ]] self.setup_nodes() def run_test(self): node = self.nodes[0] self.log.info(f"dashd args: {self.extra_args[0]}") self.wait_until(lambda: self.tor.target is not None) self.log.info(f"Tor control commands received: {self.tor.commands}") self.log.info(f"onion-service target handed to Tor: {self.tor.target}") self.log.info(f"getnetworkinfo localaddresses: {node.getnetworkinfo()['localaddresses']}") self.generate(node, 1, sync_fun=self.no_op) # leave IBD so self-advertisement happens host, port = self.tor.target.rsplit(":", 1) self.log.info(f"Connecting a peer to {host}:{port}, as Tor would for an inbound onion connection") peer = node.add_p2p_connection(AddrCollector(), dstaddr=host, dstport=int(port)) info = [p for p in node.getpeerinfo() if p["addrbind"] == f"{host}:{port}"][0] self.log.info(f"getpeerinfo: addr={info['addr']} addrbind={info['addrbind']} network={info['network']}") peer.send_and_ping(msg_getaddr()) try: self.wait_until(lambda: len(peer.addrs) > 0, timeout=10) except AssertionError: pass self.log.info(f"addresses self-advertised to this peer: {peer.addrs}") if __name__ == "__main__": ReproTorTarget().main() ``` </details> Release-note claim, unfixed build, with `127.0.0.1:19896` held by another process: ``` $ src/dashd -regtest -datadir=$D -listenonion=0 -port=18555 -rpcport=18556 -printtoconsole=0 Error: Unable to bind to 127.0.0.1:19896 on this computer. Dash Core is probably already running. Error: Failed to listen on any port. Use -listen=0 if you want this. exit=1 ``` ### Why it matters Running an onion service alongside a clearnet address is a supported setup. Operators who also pass an explicit `-bind`, which is common on servers and masternodes, would silently lose the separation between the two once v24.0.0 ships: any peer that connects to the `.onion` address can learn the node's clearnet IP from self-advertisement. Exposure needs a working Tor control connection and a routable local address (`-externalip`, or discovery or a routable `-bind`), so it does not affect every node. Nodes with `-bind` that do not use Tor are unaffected either way. The release-note error is only an operational trap, but it would ship in the v24.0.0 notes. ## What was done? - `src/init.cpp`: drop the branch that used `vBinds.front()` as the onion-service target, so the target is always the first `onion_binds` entry. Without a `-bind=...=onion`, the default onion target is now added to `onion_binds` and used as the Tor target unless `-bind` is given **and** `-listenonion` is disabled. In that one case no onion bind is added and Tor control is not started. `-listenonion` is read after parameter interaction, so `-listen=0` (which soft-sets `-listenonion=0`) is handled as before. The `-bind` help text now mentions the exception. - `doc/release-notes-7300.md`: describe the actual rule. The implicit bind is added unless `-bind` is given together with `-listenonion=0`. `-bind=...=onion` moves it, `-listenonion=0` alone or `-whitebind` alone keeps it, and `-listen=0` disables all binds. - `test/functional/feature_bind_extra.py`: node 2 (explicit `-bind`, no extra port) now says `-listenonion=0` explicitly. Before, it relied on the framework's config default. A new node 3 with explicit `-bind` and `-listenonion=1` must listen on both its `-bind` port and `127.0.0.1:19896`, and a peer connecting to `19896` must show up with `network` `onion`. Node 3's `-torcontrol` points at an unused port so the test never creates an onion service through a Tor instance running on the test host. - `test/functional/feature_proxy.py`: one step starts node 1 with `-listenonion=1` on top of the framework's `bind=127.0.0.1`. With this PR that node would also bind the fixed regtest onion target `127.0.0.1:19896`, which is the port `feature_bind_extra.py` node 3 holds, and since #7300 a failed bind stops startup. Under `test_runner.py -jN` the two tests could then collide. The step now passes `-bind=127.0.0.1:<tor_port(1)>=onion`, the same per-node port the framework uses when it adds the binds itself. I kept this local to the one test rather than teaching `TestNode.start()` a new rule, because it is the only test that enables `-listenonion` together with an explicit bind. ### Why this is the correct minimal fix The root cause is that the onion service can be pointed at a listener that is not tagged as onion. The only listener that can be both the Tor target and correctly tagged is one in `onion_binds`, so the target must always come from `onion_binds`. The alternative of adding `vBinds.front()` to `onion_binds` would tag every clearnet peer on that port as onion, which leaks the onion address to clearnet peers and distorts eviction, so it is not an option. Two other designs would avoid the multi-instance cost described under Breaking Changes. Both are reasonable, and the choice is up to maintainers: - When `-bind` is given without `=onion` and `-listenonion` is not set explicitly, soft-set `-listenonion=0` during parameter interaction. This keeps rc.1's "respect `-bind`" behavior and avoids the extra bind. The cost is that nodes upgrading from v23 with `-bind` and an automatic onion service would silently lose that onion service. - Keep the default onion bind, but treat a failure to bind it as non-fatal and skip Tor control in that case. This brings back v23's tolerance for this one bind and gives up part of #7300's "fail on any bind error" rule. This PR does neither. It keeps v23's behavior for the onion service, plus the stricter bind handling from #7300. bitcoin#22729's goal was to let users avoid the extra `127.0.0.1:9996` bind. This PR keeps that, but only when the user also says they don't want an onion service (`-listenonion=0`). Without an onion service there is nothing to tag. Nothing changes for nodes without `-bind`, with `-bind=...=onion`, or with `-listen=0`. The stricter "fail startup if any bind fails" part of bitcoin#22729 is unchanged too. ### Relation to upstream Bitcoin Core has the same bug. The open upstream fix, bitcoin#36170 ("net: require a dedicated bind for automatic Tor"), has an Approach ACK. It also removes the `vBinds.front()` fallback, but handles the `-bind` + `-listenonion` case differently: instead of adding the default onion bind, it refuses to start until the operator adds `-bind=<addr>=onion` or `-listenonion=0`. That turns every existing `-bind` setup with the default `-listenonion=1` into a startup error, including many masternodes, so this PR keeps v23's default onion bind for that case. The `src/init.cpp` block uses the same layout as bitcoin#36170: the default bind is added up front under one condition, and the Tor target is always `onion_binds[0]` inside the `-listenonion` branch. Backporting bitcoin#36170 later then only changes that condition and adds its errors. bitcoin#36170 also rejects a wildcard `=onion` bind. That is left to the backport, since it is a separate new startup error. This should also go to the v24.0.x release branch, since the behavior was introduced in v24.0.0-rc.1. ## How Has This Been Tested? macOS arm64, `--enable-debug` build, functional tests run outside any sandbox. - Reproduction script above: unfixed build shows the onion target `127.0.0.1:<bind port>`, peer `network=not_publicly_routable`, and the peer is sent `1.2.3.4`. Fixed build shows target `127.0.0.1:19896`, peer `network=onion`, and no clearnet address is sent. - `feature_bind_extra.py` with the new cases. The test is Linux-only (it reads `/proc/net/tcp`), so I ran it locally through a wrapper that swaps `get_bind_addrs()` for an `lsof`-based equivalent and lifts the platform skip. The test itself is unchanged. Nodes 0–2 pass on both builds. - Unfixed (only the `src/init.cpp` hunks reverted): fails at node 3 with `AssertionError: not({('7f000001', 17013)} == {('7f000001', 17013), ('7f000001', 19896)})`. - Fixed: passes, including the `network == "onion"` check. - `feature_proxy.py` with `--nocleanup`, checking the `Bound to` lines in node 1's `debug.log`: - Fixed build without the `feature_proxy.py` change: node 1 binds `127.0.0.1:19896`. This is the collision described above. - Fixed build with the change: node 1 binds only its p2p port and its `tor_port()`, and nothing on `19896`. The test passes. - Unfixed build with the change: same binds, and the test passes. - Release-note cases on the fixed build, with `127.0.0.1:19896` held by another process: `-listenonion=0` alone still fails with `Unable to bind to 127.0.0.1:19896`, as the corrected note says. `-bind=127.0.0.1:18555 -listenonion=0` starts. - `test/functional/test_runner.py feature_proxy.py p2p_eviction.py rpc_net.py` (after the test changes) and earlier `feature_config_args.py`: all passed. `feature_config_args.py` failed once in a `-j4` run with `Unable to start HTTP server` (an RPC port collision with another run on the same host) and passed on rerun. `feature_bind_extra.py`, `feature_bind_port_*.py` and `rpc_bind.py` are skipped on macOS by the runner. CI covers them on Linux. - `test/lint/lint-whitespace.py` and flake8 with the codes enabled in `test/lint/lint-python.py` pass. The `-bind` help line keeps the single-line `AddArg` style of its neighbours rather than the clang-format suggestion. ## Breaking Changes Compared with v24.0.0-rc.1/rc.2 only: a node started with an explicit `-bind` and `-listenonion` enabled (the default) binds `127.0.0.1:9996` (`19996` testnet) again, as v23 did. With the stricter bind handling from #7300, it fails to start if that port is unavailable. This breaks one setup that works on rc.1/rc.2: several `dashd` instances on one host and one chain, each with its own explicit `-bind`, and `-listenonion` left at its default. For example, several masternodes on separate IPs on one server. On rc.1/rc.2 each instance binds only its own `-bind` address. With this PR every instance also tries to bind `127.0.0.1:9996`; the first one gets it and every later one fails to start. v23 started all of them because a failed bind was only a warning there, and avoiding this extra bind was the reason for bitcoin#22729. Operators with that setup need one of these per instance: - `-listenonion=0`, if the instance does not need an automatic onion service; or - a distinct `-bind=127.0.0.1:<port>=onion` for each instance. Maintainers should weigh this cost against the self-advertisement problem above before merging. The alternatives listed under "Why this is the correct minimal fix" avoid this cost but make other trade-offs. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) ACKs for top commit: knst: self-ACK 5f40d56 Tree-SHA512: 049a3879405756f81ac5fd5a7f1322cc56a1e78d705dd7774e6e0a144fd6a0da0439d9dd106901c91fb3c9dd8395c0143bfa58c363f0b2ec80ffe43ee123312c (cherry picked from commit d721bef)
fbe6526 Merge #7807: feat(consensus): hold EvoNode shares and multiple payouts behind a new evo_shares deployment (pasta) 57fd2d9 Merge #7776: fix: keep a pending ProUpServTx across a registrar update that keeps the operator key (pasta) ca69801 Merge #7803: fix: return non-zero amount of accounts after sethdseed (pasta) 57a8f07 Merge #7796: backport: bitcoin#26720 (remaining), partial bitcoin#29523: weight-check the knapsack exact-match selection (pasta) 4eb82f6 Merge #7786: fix(net): keep a dedicated onion listener when -bind is given and correct the bind release note (pasta) 70626a0 Merge #7793: backport: partial bitcoin#28868 (wallet: reload wallet if migration exited early, keep mixed watchonly txs) (pasta) 250a3cd Merge #7790: fix: guard cached governance object flags with the object lock (pasta) 700c195 Merge #7787: fix: only sign EHF signals inside the deployment's start/timeout window (pasta) a3b0f3c Merge #7785: fix: restore the txindex requirement when governance validation is enabled (pasta) 2d1735d Merge #7794: fix(wallet): floor the denomination gap threshold so rebalancing cannot oscillate (pasta) d9f6592 Merge #7783: fix: stop fish completion from re-running the typed dash-cli command (pasta) 89d44c8 Merge #7795: backport: bitcoin#34272, partial bitcoin#31650 (pass PSBT by const reference in PSBTInputSignedAndVerified) (pasta) a1d7411 Merge #7779: fix!: report legacy evonode Platform addresses when listdiff changes its address (pasta) 73f6ea2 Merge #7789: fix(qt): report malformed lang/font settings instead of aborting at startup (pasta) 40de493 Merge #7781: fix(rpc): use a consistent tip height in quorum dkgstatus (pasta) 7d1a90d Merge #7791: fix(net): request object votes from nPeersPerHashMax peers (pasta) 51b145c Merge #7778: test: use single-member in functional tests: feature_mnehf, feature_notifications, p2p_instantsend (pasta) Pull request description: ## Issue being fixed or feature implemented Backports for v24.0.0-rc.3. `v24.0.x` was cut at the v24.0.0-rc.2 tag (1e239d4), and develop has since moved to 24.1.0 (#7775), so rc.3 is built from this branch instead of being tagged on develop like rc.1 and rc.2. ## What was done? Cherry-picked (`git cherry-pick -m1 -x`) every develop merge since rc.2 that carries `backport-candidate-24.0.x`, plus #7778, which #7807's test needs. They are in develop merge order: - #7778 test: use single-member in functional tests: feature_mnehf, feature_notifications, p2p_instantsend (test-only. Needed because #7807's `feature_evo_shares_activation.py` depends on its `mine_quorum_single_member()` fix and fails without it.) - #7791 fix(net): request object votes from nPeersPerHashMax peers - #7781 fix(rpc): use a consistent tip height in quorum dkgstatus - #7789 fix(qt): report malformed lang/font settings instead of aborting at startup - #7779 fix!: report legacy evonode Platform addresses when listdiff changes its address - #7795 backport: bitcoin#34272, partial bitcoin#31650 (pass PSBT by const reference in PSBTInputSignedAndVerified) - #7783 fix: stop fish completion from re-running the typed dash-cli command - #7794 fix(wallet): floor the denomination gap threshold so rebalancing cannot oscillate - #7785 fix: restore the txindex requirement when governance validation is enabled - #7787 fix: only sign EHF signals inside the deployment's start/timeout window - #7790 fix: guard cached governance object flags with the object lock - #7793 backport: partial bitcoin#28868 (wallet: reload wallet if migration exited early, keep mixed watchonly txs) - #7786 fix(net): keep a dedicated onion listener when -bind is given and correct the bind release note. Includes partial bitcoin#36170 (5f40d56). Upstream's dedicated-onion-bind changes to `feature_torcontrol.py` and `p2p_private_broadcast.py` are not included, because those tests depend on upstream commits (5693833, e74d54e) that Dash does not have. - #7796 backport: bitcoin#26720 (remaining), partial bitcoin#29523: weight-check the knapsack exact-match selection - #7803 fix: return non-zero amount of accounts after sethdseed - #7776 fix: keep a pending ProUpServTx across a registrar update that keeps the operator key - #7807 feat(consensus): hold EvoNode shares and multiple payouts behind a new evo_shares deployment One pick needed conflict resolution, in a test only, described in its commit message: #7793, `test/functional/wallet_migration.py`. `test_conflict_txs()` comes from bitcoin#28542 (#7762), which is not on this branch, so it is left out. The C++ part of #7793 is identical to develop. Merged since rc.2 but intentionally not included: #7775 (24.1 version bump), #7260 and #7804 (new Qt feature and its CI fix), #7584 (refactor), routine upstream backports #7257, #7759, #7762, #7768, #7788, #7801, and test/CI-only changes #7777 and #7797. No version change is needed. `configure.ac` is already 24.0.0 with `CLIENT_VERSION_IS_RELEASE` false, so the build takes its version string from the `v24.0.0-rc.3` tag, as it did for rc.2. ## How Has This Been Tested? Built on macOS arm64 against depends with `--enable-debug --enable-werror` (dashd, dash-qt, tests). - `make check`: all unit test suites pass, including `test_dash-qt`. - Functional tests, all passing: `feature_evo_shares_activation.py`, `feature_masternode_shares.py`, `feature_mnehf.py`, `feature_governance.py --descriptors`, `feature_governance_objects.py`, `feature_governance_txindex_devnet.py`, `feature_proxy.py`, `feature_notifications.py`, `feature_llmq_connections.py`, `feature_llmq_chainlocks_automatic.py`, `feature_llmq_singlenode.py`, `p2p_instantsend.py`, `rpc_blockchain.py` (v1 and v2 transport), `rpc_netinfo.py`, `rpc_quorum.py`, `rpc_verifychainlock.py`, `wallet_hd.py` (legacy and descriptors), `wallet_migration.py`. - `feature_bind_extra.py` (from #7786) only runs on Linux and was skipped locally, so CI covers it. Every touched file was diffed against develop. The remaining differences come only from develop PRs that are not part of this backport. ## Breaking Changes Carried over from the picked PRs: - #7807: consensus change behind the new `evo_shares` deployment (bit 14). Mainnet and testnet are unaffected until it activates. Devnets with `v24` already active and an EvoNode carrying several owner payouts need to check or reset. New RPC `protx shared_register_prepare_evo`. - #7785: outside regtest, a node with `-txindex=0` and without `-disablegovernance` refuses to start again, as before v23.1.0. - #7786: carries a partial bitcoin#36170. A node started with `-bind` but no specific `-bind=<addr:port>=onion` now refuses to start while `-listenonion` is enabled. That is the default when listening, even without Tor configured. A wildcard onion bind is also refused. Such nodes need `-bind=127.0.0.1:<port>=onion` or `-listenonion=0`. Nodes without `-bind`, including those using only `-whitebind`, keep the default `127.0.0.1:9996` (`19996` testnet) onion target. See `doc/release-notes-7300.md`. - #7779: `protx listdiff` now includes `platform_p2p` and `platform_https` for a legacy-address evonode whose core P2P address changes. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone 🤖 Generated with [Claude Code](https://claude.com/claude-code) Top commit has no ACKs. Tree-SHA512: 8cb01bdb0234f31b5dd2c105b69673de148922991a7523627395c0e0ccfa22b94be5ffbecc842398d443bc90fd9baf2eee6a9cf6fcdbbd1626eb00b93726a55a
…ss -listenonion=1 Since dashpay#7786, a node with -bind but no specific -bind=<addr>=onion refuses to start while -listenonion is enabled, and so does one with only a wildcard =onion bind. -listenonion is on by default, so this hits every node with bind= and no Tor (dashmate evonodes, docker, multi-masternode hosts), which all started on v23. Keep the privacy fix: Tor is still never pointed at a listener that isn't tagged as onion. But without a usable onion bind, the default -listenonion now connects to Tor without a target: it still authenticates and discovers the onion SOCKS proxy, but skips ADD_ONION and logs once why no onion service was created. Only an explicit -listenonion=1 keeps the startup errors. When there are both wildcard and specific onion binds, Tor targets the first specific one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss -listenonion=1 Since dashpay#7786, a node with -bind but no specific -bind=<addr>=onion refuses to start while -listenonion is enabled, and so does one with only a wildcard =onion bind. -listenonion is on by default, so this hits every node with bind= and no Tor (dashmate evonodes, docker, multi-masternode hosts), which all started on v23. Keep the privacy fix: Tor is still never pointed at a listener that isn't tagged as onion. But without a usable onion bind, the default -listenonion now connects to Tor without a target: it still authenticates and discovers the onion SOCKS proxy, but skips ADD_ONION and logs once why no onion service was created. Only an explicit -listenonion=1 keeps the startup errors. When there are both wildcard and specific onion binds, Tor targets the first specific one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss -listenonion=1 Since dashpay#7786, a node with -bind but no specific -bind=<addr>=onion refuses to start while -listenonion is enabled. -listenonion is on by default, so this hits every node with bind= and no Tor (dashmate evonodes, docker, multi-masternode hosts), which all started on v23. Keep the privacy fix: Tor is still never pointed at a listener that isn't tagged as onion. But without an onion bind, the default -listenonion now connects to Tor without a target: it still authenticates and discovers the onion SOCKS proxy, but skips ADD_ONION and logs once why no onion service was created. Only an explicit -listenonion=1 keeps that startup error. A wildcard =onion bind stays a startup error while -listenonion is enabled, by default too, also next to a specific one. Writing =onion asks for Tor, and a wildcard listener can't honor it: an accepted socket reports its concrete local address, which never matches the wildcard entry, so peers that Tor forwards there would be treated as clearnet peers and could be told our clearnet address. Setups without Tor don't write =onion, so they are not affected. With -listenonion=0 a wildcard =onion bind is still accepted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss -listenonion=1 Since dashpay#7786, a node with -bind but no specific -bind=<addr>=onion refuses to start while -listenonion is enabled. -listenonion is on by default, so this hits every node with bind= and no Tor (dashmate evonodes, docker, multi-masternode hosts), which all started on v23. Keep the privacy fix: Tor is still never pointed at a listener that isn't tagged as onion. But without an onion bind, the default -listenonion now connects to Tor without a target: it still authenticates and discovers the onion SOCKS proxy, but skips ADD_ONION and logs once why no onion service was created. Only an explicit -listenonion=1 keeps that startup error. A wildcard =onion bind stays a startup error while -listenonion is enabled, by default too, also next to a specific one. Writing =onion asks for Tor, and a wildcard listener can't honor it: an accepted socket reports its concrete local address, which never matches the wildcard entry, so peers that Tor forwards there would be treated as clearnet peers and could be told our clearnet address. Setups without Tor don't write =onion, so they are not affected. With -listenonion=0 a wildcard =onion bind is still accepted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss -listenonion=1 Since dashpay#7786, a node with -bind but no specific -bind=<addr>=onion refuses to start while -listenonion is enabled. -listenonion is on by default, so this hits every node with bind= and no Tor (dashmate evonodes, docker, multi-masternode hosts), which all started on v23. Keep the privacy fix: Tor is still never pointed at a listener that isn't tagged as onion. But without an onion bind, the default -listenonion now connects to Tor without a target: it still authenticates and discovers the onion SOCKS proxy, but skips ADD_ONION and logs once why no onion service was created. Only an explicit -listenonion=1 keeps that startup error. A wildcard =onion bind stays a startup error while -listenonion is enabled, by default too, also next to a specific one. Writing =onion asks for Tor, and a wildcard listener can't honor it: an accepted socket reports its concrete local address, which never matches the wildcard entry, so peers that Tor forwards there would be treated as clearnet peers and could be told our clearnet address. Setups without Tor don't write =onion, so they are not affected. With -listenonion=0 a wildcard =onion bind is still accepted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… service unless -listenonion=1 8274f68 fix(net): restore the default onion bind as a best-effort listener (pasta) f2b78d3 fix(net): don't refuse to start over the automatic onion service unless -listenonion=1 (pasta) Pull request description: ## Issue being fixed or feature implemented Since #7786 (shipped to v24.0.x for rc.3 via #7808), a node with `-bind` but no `-bind=<addr>:<port>=onion` refuses to start while `-listenonion` is enabled. `-listenonion` is on by default, so this breaks every node with `bind=` and no Tor that worked on v23: dashmate evonodes, docker setups, hosts with several masternodes. It also hits 54 of DCG's 88 testnet hosts. The startup errors came from a partial backport of bitcoin#36170, which is still open upstream; released Bitcoin Core does not refuse to start here. Breakage matrix, develop / v24.0.0-rc.5 vs v23: | | Setup | rc.5 | v23 | |---|---|---|---| | S1 | `-bind`, no Tor, default `-listenonion` | refuses to start | starts | | S2 | `-bind`, zero-config Tor, default `-listenonion` | refuses to start | starts, onion service on the dedicated `127.0.0.1:9996` listener | | S3 | wildcard `-bind=0.0.0.0:P=onion`, `-listenonion` effective | refuses to start (kept by this PR) | starts, but Tor was pointed at a wildcard, which never worked, and Tor peers reaching it were treated as clearnet | | S4 | no `-bind`, default onion port already taken (e.g. a second instance with `-port`) | refuses to start, even with `-listenonion=0` | starts with a warning | The privacy fix from #7786 has to stay: Tor must never be pointed at a listener that isn't tagged as onion. Otherwise Tor-forwarded peers are treated as clearnet peers and are told the node's clearnet address. Before #7786, #7300 (backport of bitcoin#22729) caused exactly that by targeting the first `-bind`. The rule this PR implements: only an explicit `-listenonion=1` may turn a missing or unusable onion listener into a startup failure. The default `-listenonion` means "use Tor if it is there" (bitcoin#6639). The one exception is a wildcard `=onion` bind (S3). Writing `=onion` is explicit Tor intent, and a wildcard listener can't honor it: an accepted socket reports its concrete local address, which never matches the wildcard entry in `m_onion_binds`, so peers that Tor forwards there would be treated as clearnet and could be told our clearnet address. So while `-listenonion` is effective, default or explicit, any wildcard `=onion` bind, including one next to a specific bind, is a startup error with the existing message, as on rc.5 and in bitcoin#36170. With `-listenonion=0` it is accepted, as in v23 and released Bitcoin Core. Setups without Tor don't write `=onion`, so S1 always starts. ## What was done? Two commits. The second can be dropped on its own if reviewers prefer. **1. `fix(net): don't refuse to start over the automatic onion service unless -listenonion=1`** (fixes S1 and S2 at startup) - The `TorController` target is now `std::optional<CService>`. Without a target, Tor control still authenticates and runs `GETINFO net/listeners/socks` (outbound onion proxy discovery), but skips `ADD_ONION`. Once authenticated, it logs once that no onion service is created because there is no dedicated onion bind. Without Tor, the control-port connect just fails, as on v23. - In init, with `-listenonion` effective: - any wildcard onion bind: the existing wildcard `InitError`, default or explicit; - no onion bind and explicit `-listenonion=1`: the existing "requires a dedicated onion bind" `InitError`; - no onion bind and the default `-listenonion`: Tor control starts without a target. **2. `fix(net): restore the default onion bind as a best-effort listener`** (fixes S2 and S4 fully) - The default onion bind `127.0.0.1:<OnionServiceTargetPort>` is added again when there is no `=onion` bind and `-listenonion` is effective, even with `-bind`, as on v23 (the rule from #7786's first two commits). It is still added when there are no binds at all, for manual-torrc setups. With `-bind` and `-listenonion=0` it is not added. - Failing to bind it only logs a warning; explicit `-bind`, `-whitebind` and `=onion` failures stay fatal. With an explicit `-listenonion=1`, the default onion bind is required and its failure is fatal. That happens inside `InitBinds`, before network threads start. - A failed default onion bind is dropped from `CConnman`'s onion binds. Tor control now starts after `connman->Start()` and gets the target only if `CConnman::HasOnionBind()` still has it; otherwise it runs without a target. This avoids v23's misroute, where Tor forwarded to whatever held the port: another instance, or the node's own `-bind`/`-whitebind` on that address. Nothing between the old and new `StartTorControl` call sites depends on Tor control; onion proxy discovery was already asynchronous. - `p2p_permissions.py` gives its whitebind-only node a per-node onion bind, and the test framework's `p2p_port()`, `rpc_port()` and `tor_port()` never return the fixed regtest onion port `19896` (it is moved to the last port of its range, which no seed uses). `feature_bind_extra.py` and `interface_zmq.py` now get each extra port from `p2p_port()` instead of adding to one. That makes `feature_bind_extra.py` the only test binding `19896`, for every port seed and in parallel `test_runner.py` runs. Docs: the `-bind` and `-listenonion` help, `doc/release-notes-7300.md` (the v24 note that said such nodes "now refuse to start") and `doc/tor.md` describe the new behavior. Translated strings are unchanged. Commit 2 no longer uses the "requires a dedicated onion bind" message. Related: #7786, #7300, #7808, bitcoin#36170 (still open upstream), bitcoin#22729. ## How Has This Been Tested? Linux x86_64, `--enable-debug` build. - `feature_bind_extra.py`, which checks the node's actual listening sockets. Node 2 (`-bind` without an onion bind) is restarted with the framework's `listenonion=0` removed, so it runs with the default `-listenonion`: - commit 1: it starts and listens only on its `-bind`; - commit 2: it starts and also listens on the default onion target `127.0.0.1:19896`; - commit 2, with `127.0.0.1:19896` held by another socket: it still starts and listens only on its `-bind`, while an explicit `-listenonion=1` fails with the bind error; - a wildcard onion bind with the default `-listenonion` is the wildcard startup error, and with `-listenonion=0` it starts; - node 2's initial start sets `-listenonion=0` explicitly in commit 2, which checks that no default onion bind is added in that case. - Ran with both commits: `feature_bind_extra.py feature_proxy.py p2p_permissions.py interface_zmq.py rpc_bind.py feature_discover.py`, all pass (`rpc_bind.py --ipv6` skips on this host), also with `--portseed=8428` and in parallel with `TEST_RUNNER_PORT_MIN=19870`. Earlier revisions with the same `src/` also passed `rpc_net.py p2p_eviction.py feature_config_args.py p2p_addr_relay.py`, `test_dash --run_test=torcontrol_tests,net_tests,net_peer_eviction_tests`, `lint-whitespace.py` and `lint-python.py`. `check-doc.py` reports undocumented `-statsport` and `-printcrashinfo`, identically on develop. - Mutation checks, each failing `feature_bind_extra.py`: - develop's `src/`: node 2 refuses to start with the default `-listenonion`; - commit 1's `src/` under commit 2's test: no listener on `127.0.0.1:19896`; - the default onion bind made fatal again: node 2 doesn't start while the port is held; - the wildcard check removed: the node starts with a wildcard onion bind. - Commit 1 alone builds, and `feature_bind_extra.py feature_proxy.py p2p_permissions.py` pass at that commit. - Not covered by a functional test: which listener the automatic onion service is told to forward to (that would need a fake Tor control port), and that peers on our own non-onion listener at `127.0.0.1:19896` aren't treated as onion peers. Both were checked during development with a test that is no longer part of the PR. ## Breaking Changes Compared with v24.0.0-rc.1 to rc.5, none. Nodes that failed to start now start. Compared with v23: - a wildcard `=onion` bind is a startup error unless `-listenonion=0` is set; - if the default onion bind fails, no onion service is created, rather than pointing Tor at whatever holds the port; - an explicit `-listenonion=1` without a usable onion bind is a startup error; - `-bind` with `-listenonion=0` no longer binds the default onion port (from #7300). ## Checklist: - [x] I have performed a self-review of my own code - [x] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone 🤖 Generated with [Claude Code](https://claude.com/claude-code) Top commit has no ACKs. Tree-SHA512: 5218897911c4dd8a1c4f7b5d0228f869f6ad654cfd8fd06a97462e82ffd2a3e52ba192acfda75dfe04f7b1f56c4b8f985d021df0df3766c9b1d1bce46a341865
…c onion service unless -listenonion=1 8274f68 fix(net): restore the default onion bind as a best-effort listener (pasta) f2b78d3 fix(net): don't refuse to start over the automatic onion service unless -listenonion=1 (pasta) Pull request description: ## Issue being fixed or feature implemented Since dashpay#7786 (shipped to v24.0.x for rc.3 via dashpay#7808), a node with `-bind` but no `-bind=<addr>:<port>=onion` refuses to start while `-listenonion` is enabled. `-listenonion` is on by default, so this breaks every node with `bind=` and no Tor that worked on v23: dashmate evonodes, docker setups, hosts with several masternodes. It also hits 54 of DCG's 88 testnet hosts. The startup errors came from a partial backport of bitcoin#36170, which is still open upstream; released Bitcoin Core does not refuse to start here. Breakage matrix, develop / v24.0.0-rc.5 vs v23: | | Setup | rc.5 | v23 | |---|---|---|---| | S1 | `-bind`, no Tor, default `-listenonion` | refuses to start | starts | | S2 | `-bind`, zero-config Tor, default `-listenonion` | refuses to start | starts, onion service on the dedicated `127.0.0.1:9996` listener | | S3 | wildcard `-bind=0.0.0.0:P=onion`, `-listenonion` effective | refuses to start (kept by this PR) | starts, but Tor was pointed at a wildcard, which never worked, and Tor peers reaching it were treated as clearnet | | S4 | no `-bind`, default onion port already taken (e.g. a second instance with `-port`) | refuses to start, even with `-listenonion=0` | starts with a warning | The privacy fix from dashpay#7786 has to stay: Tor must never be pointed at a listener that isn't tagged as onion. Otherwise Tor-forwarded peers are treated as clearnet peers and are told the node's clearnet address. Before dashpay#7786, dashpay#7300 (backport of bitcoin#22729) caused exactly that by targeting the first `-bind`. The rule this PR implements: only an explicit `-listenonion=1` may turn a missing or unusable onion listener into a startup failure. The default `-listenonion` means "use Tor if it is there" (bitcoin#6639). The one exception is a wildcard `=onion` bind (S3). Writing `=onion` is explicit Tor intent, and a wildcard listener can't honor it: an accepted socket reports its concrete local address, which never matches the wildcard entry in `m_onion_binds`, so peers that Tor forwards there would be treated as clearnet and could be told our clearnet address. So while `-listenonion` is effective, default or explicit, any wildcard `=onion` bind, including one next to a specific bind, is a startup error with the existing message, as on rc.5 and in bitcoin#36170. With `-listenonion=0` it is accepted, as in v23 and released Bitcoin Core. Setups without Tor don't write `=onion`, so S1 always starts. ## What was done? Two commits. The second can be dropped on its own if reviewers prefer. **1. `fix(net): don't refuse to start over the automatic onion service unless -listenonion=1`** (fixes S1 and S2 at startup) - The `TorController` target is now `std::optional<CService>`. Without a target, Tor control still authenticates and runs `GETINFO net/listeners/socks` (outbound onion proxy discovery), but skips `ADD_ONION`. Once authenticated, it logs once that no onion service is created because there is no dedicated onion bind. Without Tor, the control-port connect just fails, as on v23. - In init, with `-listenonion` effective: - any wildcard onion bind: the existing wildcard `InitError`, default or explicit; - no onion bind and explicit `-listenonion=1`: the existing "requires a dedicated onion bind" `InitError`; - no onion bind and the default `-listenonion`: Tor control starts without a target. **2. `fix(net): restore the default onion bind as a best-effort listener`** (fixes S2 and S4 fully) - The default onion bind `127.0.0.1:<OnionServiceTargetPort>` is added again when there is no `=onion` bind and `-listenonion` is effective, even with `-bind`, as on v23 (the rule from dashpay#7786's first two commits). It is still added when there are no binds at all, for manual-torrc setups. With `-bind` and `-listenonion=0` it is not added. - Failing to bind it only logs a warning; explicit `-bind`, `-whitebind` and `=onion` failures stay fatal. With an explicit `-listenonion=1`, the default onion bind is required and its failure is fatal. That happens inside `InitBinds`, before network threads start. - A failed default onion bind is dropped from `CConnman`'s onion binds. Tor control now starts after `connman->Start()` and gets the target only if `CConnman::HasOnionBind()` still has it; otherwise it runs without a target. This avoids v23's misroute, where Tor forwarded to whatever held the port: another instance, or the node's own `-bind`/`-whitebind` on that address. Nothing between the old and new `StartTorControl` call sites depends on Tor control; onion proxy discovery was already asynchronous. - `p2p_permissions.py` gives its whitebind-only node a per-node onion bind, and the test framework's `p2p_port()`, `rpc_port()` and `tor_port()` never return the fixed regtest onion port `19896` (it is moved to the last port of its range, which no seed uses). `feature_bind_extra.py` and `interface_zmq.py` now get each extra port from `p2p_port()` instead of adding to one. That makes `feature_bind_extra.py` the only test binding `19896`, for every port seed and in parallel `test_runner.py` runs. Docs: the `-bind` and `-listenonion` help, `doc/release-notes-7300.md` (the v24 note that said such nodes "now refuse to start") and `doc/tor.md` describe the new behavior. Translated strings are unchanged. Commit 2 no longer uses the "requires a dedicated onion bind" message. Related: dashpay#7786, dashpay#7300, dashpay#7808, bitcoin#36170 (still open upstream), bitcoin#22729. ## How Has This Been Tested? Linux x86_64, `--enable-debug` build. - `feature_bind_extra.py`, which checks the node's actual listening sockets. Node 2 (`-bind` without an onion bind) is restarted with the framework's `listenonion=0` removed, so it runs with the default `-listenonion`: - commit 1: it starts and listens only on its `-bind`; - commit 2: it starts and also listens on the default onion target `127.0.0.1:19896`; - commit 2, with `127.0.0.1:19896` held by another socket: it still starts and listens only on its `-bind`, while an explicit `-listenonion=1` fails with the bind error; - a wildcard onion bind with the default `-listenonion` is the wildcard startup error, and with `-listenonion=0` it starts; - node 2's initial start sets `-listenonion=0` explicitly in commit 2, which checks that no default onion bind is added in that case. - Ran with both commits: `feature_bind_extra.py feature_proxy.py p2p_permissions.py interface_zmq.py rpc_bind.py feature_discover.py`, all pass (`rpc_bind.py --ipv6` skips on this host), also with `--portseed=8428` and in parallel with `TEST_RUNNER_PORT_MIN=19870`. Earlier revisions with the same `src/` also passed `rpc_net.py p2p_eviction.py feature_config_args.py p2p_addr_relay.py`, `test_dash --run_test=torcontrol_tests,net_tests,net_peer_eviction_tests`, `lint-whitespace.py` and `lint-python.py`. `check-doc.py` reports undocumented `-statsport` and `-printcrashinfo`, identically on develop. - Mutation checks, each failing `feature_bind_extra.py`: - develop's `src/`: node 2 refuses to start with the default `-listenonion`; - commit 1's `src/` under commit 2's test: no listener on `127.0.0.1:19896`; - the default onion bind made fatal again: node 2 doesn't start while the port is held; - the wildcard check removed: the node starts with a wildcard onion bind. - Commit 1 alone builds, and `feature_bind_extra.py feature_proxy.py p2p_permissions.py` pass at that commit. - Not covered by a functional test: which listener the automatic onion service is told to forward to (that would need a fake Tor control port), and that peers on our own non-onion listener at `127.0.0.1:19896` aren't treated as onion peers. Both were checked during development with a test that is no longer part of the PR. ## Breaking Changes Compared with v24.0.0-rc.1 to rc.5, none. Nodes that failed to start now start. Compared with v23: - a wildcard `=onion` bind is a startup error unless `-listenonion=0` is set; - if the default onion bind fails, no onion service is created, rather than pointing Tor at whatever holds the port; - an explicit `-listenonion=1` without a usable onion bind is a startup error; - `-bind` with `-listenonion=0` no longer binds the default onion port (from dashpay#7300). ## Checklist: - [x] I have performed a self-review of my own code - [x] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone 🤖 Generated with [Claude Code](https://claude.com/claude-code) Top commit has no ACKs. Tree-SHA512: 5218897911c4dd8a1c4f7b5d0228f869f6ad654cfd8fd06a97462e82ffd2a3e52ba192acfda75dfe04f7b1f56c4b8f985d021df0df3766c9b1d1bce46a341865 [v24.0.x: doc/release-notes-7300.md was folded into doc/release-notes.md for v24.0.0 (dashpay#7970), so its hunk is applied there in the next commit.] (cherry picked from commit 92f2bfb)
d6c066f chore: add v23.1.10 to the flatpak release history (pasta) 202632c chore: release v24.0.0 (pasta) 2bf133c docs: cite #8042 for the index fixes in the v24.0.0 release notes (pasta) 8aa11f6 docs: add the v24.0.0-rc.7 picks and index fixes to the v24.0.0 release notes (pasta) ad4e5d6 docs: drop the index caveat from the -reindex-chainstate man page entry (pasta) 19a90bb test: blocks orphaned while the address index is disabled are removed from it (pasta) d2f2a1f fix(index): don't adopt an empty or rebuilt async index as migrated legacy data (pasta) e3bafdb fix(index): reject an uncommitted tip the block index lost (pasta) 9198962 fix(index): check the blocks governance resumes from at startup (pasta) baff347 fix(index): resume from the tip a skipped commit leaves behind (pasta) 2dbf5ca fix(index): write a rewound block's erasures in one batch with its locator (pasta) 252acbe fix(index): move the committed locator below the entries a rewind erases (Konstantin Akimov) e9f9c8a Merge bitcoin#36150: indexes: set prune lock to genesis before first block (Konstantin Akimov) 2edeb98 partial Merge bitcoin#34897: indexes: Don't commit ahead of the flushed chainstate (Konstantin Akimov) 3eba84c Merge bitcoin#32875: index: handle case where pindex_prev equals chain tip in NextSyncBlock() (Konstantin Akimov) 399125c Merge bitcoin#32882: index: remove unnecessary locator cleaning in BaseIndex::Init() (Konstantin Akimov) bb724c6 Merge bitcoin#33212: index: Don't commit state in BaseIndex::Rewind (Konstantin Akimov) d5ff431 Merge bitcoin#32878: index: fix wrong assert of current_tip == m_best_block_index (Konstantin Akimov) 31c7d8f partial Merge bitcoin#32580: wallet, test: best block locator matches scan state follow-ups (Konstantin Akimov) 94f5c58 partial Merge bitcoin#30221: wallet: Ensure best block matches wallet scan state (Konstantin Akimov) 8e0b1e8 partial Merge bitcoin#32069: test: fix intermittent failure in wallet_reorgsrestore.py (Konstantin Akimov) 4d17947 Merge bitcoin#25193: indexes: Read the locator's top block during init, allow interaction with reindex-chainstate (Konstantin Akimov) 0127df8 Merge #8045: test: scale the mempool-loaded wait by timeout_factor so TSAN reindex restarts stop timing out (pasta) 62d0cef Merge #8036: fix(qt): let a shared masternode participant follow a revised invitation that keeps their share (merge-script) 775d31c Merge #8018: fix(consensus): require IPv4 masternode primaries until IPv4 is optional, pin mainnet ports (merge-script) e1b826e Merge #8026: fix(qt): keep shared masternode contribution fields locked while coins are reserved (merge-script) 9fa3df3 Merge #8020: fix(rpc): make protx shared_sign sign only dissolutions that return every principal (merge-script) 246a465 Merge #8024: fix(qt): keep the shared masternode dissolve dialog consistent with what it will send (merge-script) 590ad0a Merge #8021: fix(qt): don't load a newer shared masternode session copy that changes your own share (merge-script) 974aaa3 Merge #8034: fix(qt): check a shared masternode's service and refund addresses before coins are reserved (merge-script) 0026e3e Merge #8027: fix(qt): fund a shared masternode's service update from this wallet by default (merge-script) 6a5d2dc Merge #8035: fix(qt): don't strand coins reserved for a shared masternode session (merge-script) df6083a Merge #8032: fix(qt): don't offer a new operator key when reopening a shared masternode session (merge-script) 2b5bf30 Merge #8029: fix: let dashd -version, -help and first start work without an existing data directory (merge-script) e33d9b1 Merge #8037: docs: list the config file path each Linux init system uses (merge-script) 6030ea0 docs: add the #8014 migratewallet fixes to the v24.0.0 release notes (pasta) 1b1ea95 Merge #8014: backport: bitcoin#28609, bitcoin#34215, bitcoin#34226, bitcoin#34370, partials bitcoin#31423, bitcoin#34156 (migratewallet fixes from 28.x) (merge-script) 608ff63 Merge bitcoin#28542: wallet: Check for uninitialized last processed and conflicting heights in MarkConflicted (DCG-Claude) 3af6f63 Merge bitcoin#28038: wallet: address book migration bug fixes (DCG-Claude) 2fe41fb docs: add ecosystem notes on Asset Unlock txids, coinbase payees and RPC errors (pasta) 0e3f653 docs: correct operator guidance in the v24.0.0 release notes for rc.7 (pasta) 11c8140 Merge #8019: fix(qt): stop reporting refused shared masternode broadcasts as sent (merge-script) cdb250c doc: note operator-relevant v23.1.x patch-release changes for v24.0.0 (pasta) fcd332b doc: state the provider-transaction InstantSend change on its own (pasta) e6f3ad4 fix(bls): stop weighted contribution batch verification on an invalid partial aggregate (pasta) e04acba fix(llmq): weight batched DKG contribution verification (UdjinM6) ed96227 fix(bls): weight batch-verified signatures so complementary errors cannot cancel (UdjinM6) b5c7985 fix(governance): don't vote on triggers while not synced (pasta) a79f432 fix(governance): don't vote NO-FUNDING on triggers before their maturity window (pasta) 8615244 fix(llmq): accept equal DKG secret key shares when aggregating a member's share (pasta) 1c356f4 test: cover DKG members dealing identical contributions (UdjinM6) f23f748 fix(llmq): drop DKG members whose verification vectors share coefficients (UdjinM6) 914c052 docs: add #8003 to the v24.0.0 release notes and man pages (pasta) e81c511 Merge #8003: fix(net): don't refuse to start over the automatic onion service unless -listenonion=1 (merge-script) 41b4578 ci: run cppcheck as test/lint/cppcheck-dash.py, as develop does since #7966 (pasta) c97f3b3 docs: regenerate man pages for v24.0.0-rc.6 (pasta) 784312c docs: add the v24.0.0-rc.6 changes to the v24.0.0 release notes (pasta) f15a53a Merge #7991: fix(qt): shared masternode wizard fixes for taken shares, unconfirmed funding and reply handling (merge-script) 1c46224 Merge #7995: fix(wallet): don't let a waiting CoinJoin promotion/demotion queue block standard mixing (merge-script) 546db2f Merge #7996: fix(wallet): fill a CoinJoin promotion with sibling coins when needed (merge-script) 349859f Merge #7957: fix(qt): validate shared-masternode shares with the consensus rules (merge-script) 9091f0b Merge #8005: backport: bitcoin#27114, bitcoin#33395 (merge-script) e915ab5 Merge #7990: fix(wallet): pay for 3 byte input and output counts so 253+ input sends work (merge-script) 4f4d188 Merge #7984: chore: Translations 2026-10 (pasta) 129a1fc Merge #7980: chore: update seeds, checkpoints, minimum chain work, assumevalid and tx stats for v24 (pasta) 5800e5d docs: state that SPORK_3/SPORK_9 were already hardened on mainnet (pasta) ddc82a8 docs: separate extended addresses from shared masternodes in v24 notes (pasta) 8660e54 docs: complete the v24.0.0 release notes against the full v24 changeset (pasta) 11ea306 Merge #7947: fix: stop quorum key warming from stalling DKG on 2-core masternodes (pasta) 25d7c85 Merge #7826: fix: skip repeated authenticated Platform bans (pasta) 18073dd Merge #7850: perf: let CHECKDATASIG use the signature cache (pasta) 3759397 Merge #7833: fix: gate signing intake before quorum retrieval (pasta) db98ee2 fix: keep flat-file positions 64-bit clean at the remaining raw seek/tell sites (Konstantin Akimov) 54c9738 Merge bitcoin#36667: net: enforce timeout interval for paused send peers (Konstantin Akimov) f1adb2c Merge bitcoin#36665: sock: align exception monitoring between poll() and select() backends (Konstantin Akimov) 8becf6a Merge bitcoin#36669: refactor: store positions as a 64 bits integer in flatfiles (Konstantin Akimov) 4c0cbd6 fix(llmq): judge every sig-share sender by its own verified shares (pasta) 649259f fix(governance): cap the orphan parent objects re-requested per tick (pasta) 418f5ee fix(llmq): verify DKG contributions individually when a batch vvec check fails (pasta) abf2eb0 fix(llmq): reject invalid decrypted DKG shares before aggregation (pasta) 39bda8f fix(consensus): reject negative transaction nVersion after DIP3 (pasta) 1ba2609 Merge #7951: fix(net): stop peers lingering after disconnect under edge-triggered epoll (pasta) 696d45f Merge #7861: feat: keep at least 4-days worth blocks after pruning (merge-script) 9cd1c72 Merge #7869: fix: let startup block replay handle quorum commitments and asset unlocks (pasta) 73e7254 Merge #7682: build: finish relic_conf.h regeneration before compiling BLS consumers (pasta) 7312925 ci: bake the macOS SDK into the CI runner image (pasta) d885ecd Merge #7916: backport: bitcoin#27800, bitcoin#28125, bitcoin#28232, bitcoin#27695 (merge-script) f55ad1d Merge bitcoin#28139: test: create wallet specific for test_locked_wallet case (DCG-Claude) 80f6cbe chore: add the v23.1.9 release entry to flatpak metainfo (pasta) 5ba801f chore: use translation source strings generated from v24.0.x (pasta) a3b9e30 Merge #7974: chore: update man pages, translation strings, SECURITY.md and flatpak for v24.0.0 (pasta) b5d26bd feat(consensus): schedule V24 and evo_shares on mainnet for November 9, 2026 (pasta) fe78211 docs: state the earliest v24 activation height and the v23.1.9 fixes (pasta) 3bbe3e2 docs: address review of v24.0.0 release notes (pasta) 28c39fc docs: add v24.0.0 release notes and remove individual release notes files (pasta) e8cbfaf docs: archive v23.1.8 and v23.1.9 release notes (pasta) 6c02b2b ci: let DEPENDS_ARCH override the machine arch for multiprocess and tsan (pasta) ce5dbf3 Merge #7888: fix(consensus): canonical ExtNetInfo encoding and strict netInfo decoding (pasta) 54cba5c Merge #7537: fix: data races and check-then-act races in the CoinJoin server (pasta) 08d8bca Merge #7909: feat(mining): build the masternode list incrementally during block assembly (pasta) 5633761 Merge #7895: test: cover retroactive provider tx refusal, fix wording (pasta) cbb9500 Merge #7846: fix: treat pending owner keys and Platform node IDs as one conflict namespace (pasta) 8efdf5e Merge #7857: fix: mine a pending ProUpServTx before a key change of its masternode (pasta) e56e6ae Merge #7842: fix(wallet): reject mnemonic passphrases that seed derivation truncates (pasta) 79480ff test: follow-up for #7821: check if wallet is locked after failed upgrade instead creating brand-new scenario (Konstantin Akimov) a56ed47 fix(net): announce sporks to new peers instead of requesting them during mnsync (pasta) cdaea5f Merge #7894: fix: due to logical conflicts feature_pruning.py had been broken while (pasta) 98d92f6 Merge #7817: backport: partial bitcoin#34156 restore cleanup ownership (pasta) 35e6d18 Merge #7858: fix: do not InstantSend-lock provider transactions (pasta) 904d73d Merge #7881: fix: refuse to sign shared registrar updates that move the funder's coins (pasta) 95982f5 Merge #7782: fix(wallet): persist pending-observation coin locks once the record becomes readable (pasta) cdf65d0 Merge #7836: backport: partial bitcoin#25812, bitcoin#29855 (pasta) cc5716f Merge #7824: fix: retain initialized DKG sessions during inventory reads (pasta) 588e458 Merge #7827: backport: partial bitcoin#35984 (pasta) 795c6f1 Merge #7839: fix: release InstantSend tracking after mempool eviction (pasta) dc1a6b2 Merge #7875: docs(rpc): clarify getchainlockbyheight availability (pasta) e4798ff Merge #7876: fix(consensus): capture operator key scheme changes with unchanged bytes in state diffs (pasta) fe68ecb Merge #7871: backport!: Merge bitcoin#27217: wallet: Replace use of purpose strings with an enum (pasta) 8348d22 Merge #7867: fix: refuse masternode list RPCs for blocks outside the active chain (pasta) 861a579 Merge #7866: fix: stop `masternode payments` at the genesis block (pasta) efcef71 Merge #7521: fix: update masternode rate limit on failed governance trigger path (pasta) 0623182 Merge #7831: fix: bound governance object signatures and reset the governance cache (pasta) b230657 Merge #7841: fix: authenticate cached InstantSend proof bytes (pasta) a1f37ea Merge #7820: fix: hold chainstate lock during CoinJoin input validation (pasta) 50a5617 Merge #7829: fix: preserve wallet permissions when changing passphrases (pasta) 2ff0103 Merge #7818: fix: return REST errors for unavailable deployment data (pasta) cb39369 Merge #7834: fix: validate Electrum import input boundaries (pasta) e7e3daa Merge #7821: fix: relock encrypted wallets after HD upgrade failures (pasta) 2fea45f Merge #7792: fix: treat missing snapshots during evodb repair as repair errors (pasta) 3c5c039 Merge #7838: fix(wallet): authenticate unlock deadline extensions (pasta) 0bf80f8 Merge #7810: fix(rpc): handle unavailable masternode payment inputs (pasta) bcefee4 Merge #7822: fix: reject malformed detached quorum share batches (pasta) 86a7737 Merge #7828: fix: omit recovery words from invalid mnemonic errors (pasta) 886d5c4 Merge #7816: fix: check credit pool amount accumulation (pasta) 00fc3b9 Merge #7851: fix(rpc): correct masternode status help for top-level service field (pasta) 0e75c22 Merge #7814: fix(qt): restore HTML escaping of send confirmation recipients (pasta) fbe6526 Merge #7807: feat(consensus): hold EvoNode shares and multiple payouts behind a new evo_shares deployment (pasta) 57fd2d9 Merge #7776: fix: keep a pending ProUpServTx across a registrar update that keeps the operator key (pasta) ca69801 Merge #7803: fix: return non-zero amount of accounts after sethdseed (pasta) 57a8f07 Merge #7796: backport: bitcoin#26720 (remaining), partial bitcoin#29523: weight-check the knapsack exact-match selection (pasta) 4eb82f6 Merge #7786: fix(net): keep a dedicated onion listener when -bind is given and correct the bind release note (pasta) 70626a0 Merge #7793: backport: partial bitcoin#28868 (wallet: reload wallet if migration exited early, keep mixed watchonly txs) (pasta) 250a3cd Merge #7790: fix: guard cached governance object flags with the object lock (pasta) 700c195 Merge #7787: fix: only sign EHF signals inside the deployment's start/timeout window (pasta) a3b0f3c Merge #7785: fix: restore the txindex requirement when governance validation is enabled (pasta) 2d1735d Merge #7794: fix(wallet): floor the denomination gap threshold so rebalancing cannot oscillate (pasta) d9f6592 Merge #7783: fix: stop fish completion from re-running the typed dash-cli command (pasta) 89d44c8 Merge #7795: backport: bitcoin#34272, partial bitcoin#31650 (pass PSBT by const reference in PSBTInputSignedAndVerified) (pasta) a1d7411 Merge #7779: fix!: report legacy evonode Platform addresses when listdiff changes its address (pasta) 73f6ea2 Merge #7789: fix(qt): report malformed lang/font settings instead of aborting at startup (pasta) 40de493 Merge #7781: fix(rpc): use a consistent tip height in quorum dkgstatus (pasta) 7d1a90d Merge #7791: fix(net): request object votes from nPeersPerHashMax peers (pasta) 51b145c Merge #7778: test: use single-member in functional tests: feature_mnehf, feature_notifications, p2p_instantsend (pasta) Pull request description: ## Issue being fixed or feature implemented Merges the v24.0.0 release back into develop, as the release process asks once `master` is fast-forwarded. `master`, `v24.0.x` and the signed `v24.0.0` tag all point to the same commit (938fba5). ## What was done? One merge commit of `master` into develop. `v24.0.x` was cut from develop, so all of its code is already here. Every commit on it that develop lacks by patch is one of these: - a `cherry-pick -x` of a develop commit; - covered by a develop PR: the index fixes by #8042, the mainnet V24 schedule by #7973, the cppcheck CI move by #7966; - release bookkeeping. Git reports conflicts in many files, because `v24.0.x` carries adapted copies of develop's changes. All of them resolve to develop's side, which keeps develop's code, translations and version (`configure.ac` stays at 24.1.0, `CLIENT_VERSION_IS_RELEASE` false). Taken from v24.0.0: - `doc/release-notes.md`: the v24.0.0 release notes. The v23.1.10 notes move to `doc/release-notes/dash/`. - The man pages regenerated for v24.0.0, replacing develop's older v24.0.0 pages. A few entries (`-prune`, `-par`, `-parbls`, `-maxconnections`) still trail develop's help text, as they did before, until the next regeneration. - `feature_addressindex.py`: the v24.0.x test that blocks orphaned while the address index is disabled are removed from it once it is re-enabled. Release notes: - Individual `doc/release-notes-*.md` files whose content is in the v24.0.0 notes are removed. This includes `release-notes-8018.md`, `release-notes-8029.md` and `release-notes-27114.md`, whose changes shipped in v24.0.0 without the files. - `release-notes-28113.md` and `release-notes-28414.md` also covered `descriptorprocesspsbt`, which only develop has. They are trimmed to that RPC instead of removed. - Notes for changes v24.0.0 doesn't include stay: #7812, #7847, #7860, #7862, bitcoin#25943, bitcoin#26485 and bitcoin#27501. ## How Has This Been Tested? macOS arm64 against depends, `--enable-debug --enable-werror --disable-hardening --without-gui`. `dashd`, `dash-cli` and `test_dash` build cleanly. - `src/` is byte-identical to develop's. The only non-doc change is the new `feature_addressindex.py` case. - Functional tests, all passing: `feature_addressindex.py` (with the new case) and `feature_spentindex.py`. - Unit suites, all passing: `txindex_tests`, `blockfilter_index_tests`, `coinstatsindex_tests`, `index_migration_tests`, `baseindex_tests`. - `lint-python.py`, `lint-whitespace.py`, `lint-includes.py` and `lint-circular-dependencies.py` pass. - An independent review checked every non-merge commit in `develop..master` against develop (by patch, by cherry-pick source, and line by line for the rest), re-merged each `v24.0.x` merge commit to look for content added in a merge, and checked each release-note file decision. I did not run the full functional suite locally; CI covers it. ## Breaking Changes None. Develop's code is unchanged; the only non-doc change is the new functional test case. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have added or updated relevant unit/integration/functional/e2e tests - [x] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone _(if you have permission to set one)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Top commit has no ACKs. Tree-SHA512: 3c2d1b7d2885f983f1457a18920f42907009c8ca77522a6116739d387b011d8d5583da6a4b6a1227bcaa21fa07a7ef635aae58fb8b19e4f31a95582ee1ce2318
Issue being fixed or feature implemented
The backport of bitcoin#22729 (#7300) changed what happens when a node is started with an explicit
-bind=<addr>but no-bind=...=onion. Before, Dash Core always added the dedicated onion listener127.0.0.1:9996(19996testnet,19896regtest) and pointed the automatically created Tor onion service (-listenonion, on by default) at it. Now it binds no onion listener and points the onion service at the first-bindaddress.That listener is not in
onion_binds, so every peer Tor forwards to it is classified by its source address: Tor's loopback address, or the node's own IP when-bindis a routable address. It is not classified as an onion peer. As a result, a node that runs an onion service next to a clearnet address (-externalip, or a discovered/bound routable address) advertises its clearnet address to peers that reached it through its.onionaddress. That links the two identities, which the privacy-network check inGetLocal()is meant to prevent. Those peers also lose the rest of the onion-specific handling, such asgetpeerinfonetworkand eviction protection for onion peers.The release note added for #7300 has a related problem. It says
-listenonion=0drops the implicit onion bind, but it never did: the bind is added before-listenonionis looked at. An operator whose127.0.0.1:9996is taken who follows the note still gets a node that refuses to start.Both changes are only in v24.0.0-rc.1 and rc.2.
Why this is a real problem
Code path at c14104b:
src/init.cpp#L2678-L2695: withvBindsnon-empty and no=onionbind,onion_service_target = connOptions.vBinds.front()(L2682). It is not pushed intoonion_binds, andStartTorControl(onion_service_target)still runs (L2694).src/net.h#L1291copies onlyonion_bindsintom_onion_binds.src/net.cpp#L1977setsinbound_oniononly for listeners inm_onion_binds.src/net.cpp#L584-L592: withoutm_inbound_onion,ConnectedThroughNetwork()andIsConnectedThroughPrivacyNet()fall back to the peer's source address. The privacy filter inGetLocal()(L186-L193) then skips the node's own onion address for that peer and lets the clearnet one through.I reproduced this on the unfixed build with a small functional-test-style script. A minimal fake Tor control port accepts
PROTOCOLINFO/AUTHENTICATE/ADD_ONIONand records the target dashd asks Tor to forward to. A P2P peer then connects to exactly that target, as Tor does for every inbound onion connection, and sendsgetaddr. The node runs with-bind=127.0.0.1:<port> -listenonion=1 -externalip=1.2.3.4.Unfixed (c14104b):
The onion service forwards to the clearnet listener, the forwarded peer is
not_publicly_routableinstead ofonion, and it is sent the node's clearnet address1.2.3.4.In the fixed run the list is empty for two reasons. The privacy filter withholds
1.2.3.4from an onion peer, and the testP2PInterfacenever sendssendaddrv2, so the node cannot send it the v3 onion address either. The fixed run also prints await_until() failedline after the 10 s wait for addresses; that line is left out of the excerpt above.With this PR:
Reproduction script (run with
WORKTREE=<src root> python3 repro_tor_target.py --configfile=<src root>/test/config.ini)Release-note claim, unfixed build, with
127.0.0.1:19896held by another process:Why it matters
Running an onion service alongside a clearnet address is a supported setup. Operators who also pass an explicit
-bind, which is common on servers and masternodes, would silently lose the separation between the two once v24.0.0 ships: any peer that connects to the.onionaddress can learn the node's clearnet IP from self-advertisement. Exposure needs a working Tor control connection and a routable local address (-externalip, or discovery or a routable-bind), so it does not affect every node. Nodes with-bindthat do not use Tor are unaffected either way. The release-note error is only an operational trap, but it would ship in the v24.0.0 notes.What was done?
src/init.cpp: drop the branch that usedvBinds.front()as the onion-service target, so the target is always the firstonion_bindsentry. Without a-bind=...=onion, the default onion target is now added toonion_bindsand used as the Tor target unless-bindis given and-listenonionis disabled. In that one case no onion bind is added and Tor control is not started.-listenonionis read after parameter interaction, so-listen=0(which soft-sets-listenonion=0) is handled as before. The-bindhelp text now mentions the exception.doc/release-notes-7300.md: describe the actual rule. The implicit bind is added unless-bindis given together with-listenonion=0.-bind=...=onionmoves it,-listenonion=0alone or-whitebindalone keeps it, and-listen=0disables all binds.test/functional/feature_bind_extra.py: node 2 (explicit-bind, no extra port) now says-listenonion=0explicitly. Before, it relied on the framework's config default. A new node 3 with explicit-bindand-listenonion=1must listen on both its-bindport and127.0.0.1:19896, and a peer connecting to19896must show up withnetworkonion. Node 3's-torcontrolpoints at an unused port so the test never creates an onion service through a Tor instance running on the test host.test/functional/feature_proxy.py: one step starts node 1 with-listenonion=1on top of the framework'sbind=127.0.0.1. With this PR that node would also bind the fixed regtest onion target127.0.0.1:19896, which is the portfeature_bind_extra.pynode 3 holds, and since backport: Merge bitcoin/bitcoin#30397, 22729, 29633 #7300 a failed bind stops startup. Undertest_runner.py -jNthe two tests could then collide. The step now passes-bind=127.0.0.1:<tor_port(1)>=onion, the same per-node port the framework uses when it adds the binds itself. I kept this local to the one test rather than teachingTestNode.start()a new rule, because it is the only test that enables-listenoniontogether with an explicit bind.Why this is the correct minimal fix
The root cause is that the onion service can be pointed at a listener that is not tagged as onion. The only listener that can be both the Tor target and correctly tagged is one in
onion_binds, so the target must always come fromonion_binds. The alternative of addingvBinds.front()toonion_bindswould tag every clearnet peer on that port as onion, which leaks the onion address to clearnet peers and distorts eviction, so it is not an option.Two other designs would avoid the multi-instance cost described under Breaking Changes. Both are reasonable, and the choice is up to maintainers:
-bindis given without=onionand-listenonionis not set explicitly, soft-set-listenonion=0during parameter interaction. This keeps rc.1's "respect-bind" behavior and avoids the extra bind. The cost is that nodes upgrading from v23 with-bindand an automatic onion service would silently lose that onion service.This PR does neither. It keeps v23's behavior for the onion service, plus the stricter bind handling from #7300.
bitcoin#22729's goal was to let users avoid the extra
127.0.0.1:9996bind. This PR keeps that, but only when the user also says they don't want an onion service (-listenonion=0). Without an onion service there is nothing to tag. Nothing changes for nodes without-bind, with-bind=...=onion, or with-listen=0. The stricter "fail startup if any bind fails" part of bitcoin#22729 is unchanged too.Relation to upstream
Bitcoin Core has the same bug. The open upstream fix, bitcoin#36170 ("net: require a dedicated bind for automatic Tor"), has an Approach ACK. It also removes the
vBinds.front()fallback, but handles the-bind+-listenonioncase differently: instead of adding the default onion bind, it refuses to start until the operator adds-bind=<addr>=onionor-listenonion=0. That turns every existing-bindsetup with the default-listenonion=1into a startup error, including many masternodes, so this PR keeps v23's default onion bind for that case.The
src/init.cppblock uses the same layout as bitcoin#36170: the default bind is added up front under one condition, and the Tor target is alwaysonion_binds[0]inside the-listenonionbranch. Backporting bitcoin#36170 later then only changes that condition and adds its errors. bitcoin#36170 also rejects a wildcard=onionbind. That is left to the backport, since it is a separate new startup error.This should also go to the v24.0.x release branch, since the behavior was introduced in v24.0.0-rc.1.
How Has This Been Tested?
macOS arm64,
--enable-debugbuild, functional tests run outside any sandbox.127.0.0.1:<bind port>, peernetwork=not_publicly_routable, and the peer is sent1.2.3.4. Fixed build shows target127.0.0.1:19896, peernetwork=onion, and no clearnet address is sent.feature_bind_extra.pywith the new cases. The test is Linux-only (it reads/proc/net/tcp), so I ran it locally through a wrapper that swapsget_bind_addrs()for anlsof-based equivalent and lifts the platform skip. The test itself is unchanged. Nodes 0–2 pass on both builds.src/init.cpphunks reverted): fails at node 3 withAssertionError: not({('7f000001', 17013)} == {('7f000001', 17013), ('7f000001', 19896)}).network == "onion"check.feature_proxy.pywith--nocleanup, checking theBound tolines in node 1'sdebug.log:feature_proxy.pychange: node 1 binds127.0.0.1:19896. This is the collision described above.tor_port(), and nothing on19896. The test passes.127.0.0.1:19896held by another process:-listenonion=0alone still fails withUnable to bind to 127.0.0.1:19896, as the corrected note says.-bind=127.0.0.1:18555 -listenonion=0starts.test/functional/test_runner.py feature_proxy.py p2p_eviction.py rpc_net.py(after the test changes) and earlierfeature_config_args.py: all passed.feature_config_args.pyfailed once in a-j4run withUnable to start HTTP server(an RPC port collision with another run on the same host) and passed on rerun.feature_bind_extra.py,feature_bind_port_*.pyandrpc_bind.pyare skipped on macOS by the runner. CI covers them on Linux.test/lint/lint-whitespace.pyand flake8 with the codes enabled intest/lint/lint-python.pypass. The-bindhelp line keeps the single-lineAddArgstyle of its neighbours rather than the clang-format suggestion.Breaking Changes
Compared with v24.0.0-rc.1/rc.2 only: a node started with an explicit
-bindand-listenonionenabled (the default) binds127.0.0.1:9996(19996testnet) again, as v23 did. With the stricter bind handling from #7300, it fails to start if that port is unavailable.This breaks one setup that works on rc.1/rc.2: several
dashdinstances on one host and one chain, each with its own explicit-bind, and-listenonionleft at its default. For example, several masternodes on separate IPs on one server. On rc.1/rc.2 each instance binds only its own-bindaddress. With this PR every instance also tries to bind127.0.0.1:9996; the first one gets it and every later one fails to start. v23 started all of them because a failed bind was only a warning there, and avoiding this extra bind was the reason for bitcoin#22729.Operators with that setup need one of these per instance:
-listenonion=0, if the instance does not need an automatic onion service; or-bind=127.0.0.1:<port>=onionfor each instance.Maintainers should weigh this cost against the self-advertisement problem above before merging. The alternatives listed under "Why this is the correct minimal fix" avoid this cost but make other trade-offs.
Checklist:
🤖 Generated with Claude Code