Skip to content

fix(net): keep a dedicated onion listener when -bind is given and correct the bind release note - #7786

Merged
PastaPastaPasta merged 3 commits into
dashpay:developfrom
PastaPastaPasta:fix/keep-a-dedicated-onion-listener-when-bind-is-giv
Oct 6, 2026
Merged

PastaPastaPasta merged 3 commits into
dashpay:developfrom
PastaPastaPasta:fix/keep-a-dedicated-onion-listener-when-bind-is-giv

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

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: with vBinds non-empty and no =onion bind, onion_service_target = connOptions.vBinds.front() (L2682). It is not pushed into onion_binds, and StartTorControl(onion_service_target) still runs (L2694).
  • src/net.h#L1291 copies only onion_binds into m_onion_binds. src/net.cpp#L1977 sets inbound_onion only for listeners in m_onion_binds.
  • 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) 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: []
Reproduction script (run with WORKTREE=<src root> python3 repro_tor_target.py --configfile=<src root>/test/config.ini)
#!/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()

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 backport: Merge bitcoin/bitcoin#30397, 22729, 29633 #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 backport: Merge bitcoin/bitcoin#30397, 22729, 29633 #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:

  • 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 (for repository code-owners and collaborators only)

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (1)
doc/developer-notes.md — auto-discovered

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 46ee9b33-c881-48f3-97bd-fc9994ab94bb
📥 Commits

Reviewing files that changed from the base of the PR and between 3789ec0 and 5f40d56.

📒 Files selected for processing (5)
  • doc/release-notes-7300.md
  • doc/tor.md
  • src/init.cpp
  • test/functional/feature_bind_extra.py
  • test/functional/feature_proxy.py

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.


Walkthrough

When -listenonion is enabled with explicit binds, startup now requires a non-wildcard onion bind and uses the first onion bind as the Tor service target. Startup reports an error when no onion bind exists or an onion bind is wildcard. When neither regular nor onion binds are configured, Dash Core adds the default onion target. Documentation and functional tests reflect these rules.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~12 minutes

Merge Risk: ⚪ Minimal · up to 5f40d

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 Review

Security architecture risk: 🔵 Low · up to 5f40d

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
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The affected security boundary is each configured node's inbound P2P identity and address advertisement. At the base, a remote onion peer could reach an ordinary listener through Tor and be treated as a non-onion peer, potentially linking that node's onion and clearnet identities. The head removes this automatic forwarding path rather than expanding remote authority.

Security Findings and Attack Paths

  • inferred — The inspected startup-to-classification path supports rejecting an introduced privacy-disclosure concern: the automatic Tor target is now an onion-bind member, and the existing address-selection filter excludes clearnet local addresses for onion-classified peers. This conclusion is source-based and does not assert that every external Tor deployment has been validated.

Trust Boundaries and Controls

  • observed — A direct connection to an explicitly routable =onion listener is still classified as onion, and Tor-forwarded connections can still match source-address whitelist rules. Both behaviors existed at the base. Dedicated listener selection fixes target/classifier alignment; it should not be interpreted as Tor authentication or complete whitelist isolation.

Resilience and Maintainability Implications

  • observed — Tor control starts before listener initialization at both base and head, so service publication and successful binding remain non-atomic. Bind initialization fails on a configured-listener failure; the daemon then follows interruption and shutdown. Tor disconnect or controller destruction removes local service advertisement, reconnect retains the immutable target, and network shutdown clears listening sockets. These paths do not reinstate the removed ordinary-bind fallback.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the dedicated onion listener fix and the related release-note correction.
Description check ✅ Passed The description explains the onion-listener issue, the fix, the compatibility impact, and the reported tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

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.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit 5f40d56) · triage: low

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: low by gpt-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); agent phase1-reviewer
  • Phase 1 model: glm-5.3-flash — zai quota: 5h 99% left, weekly 69% left; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 15% left, 5h 100% left)
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort medium); agent phase2-reviewer, gpt-6.1-sol — dash-core-commit-history (completed, effort medium); agent phase2-reviewer

@thepastaclaw thepastaclaw added the pastaclaw:approved thepastaclaw's latest review approved this PR label Oct 4, 2026
PastaPastaPasta and others added 2 commits October 5, 2026 00:32
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>
@PastaPastaPasta
PastaPastaPasta force-pushed the fix/keep-a-dedicated-onion-listener-when-bind-is-giv branch from f8b56ba to 72b3c71 Compare October 5, 2026 05:33
@thepastaclaw thepastaclaw removed the pastaclaw:approved thepastaclaw's latest review approved this PR label Oct 5, 2026

@knst knst left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self-ACK 5f40d56

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: low by gpt-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); agent phase1-reviewer
  • Phase 1 model: glm-5.3-flash — zai quota: 5h 98% left, weekly 99% left; passed over gemini-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; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort medium); agent phase2-reviewer, gpt-6.1-sol — backport-reviewer (completed, effort medium); agent phase2-reviewer, gpt-6.1-sol — dash-core-commit-history (completed, effort medium); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort medium); agent phase2-reviewer, gpt-6.1-sol — backport-reviewer (completed, effort medium); agent phase2-reviewer, gpt-6.1-sol — dash-core-commit-history (completed, effort medium); agent phase2-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.

Comment thread src/init.cpp
Comment on lines +2679 to +2689
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."));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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)

Comment thread src/init.cpp
Comment on lines 2683 to +2685
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."));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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)

Comment on lines +90 to +105
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.",
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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)

Comment thread doc/release-notes-7300.md
Comment on lines +8 to +16
* 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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)

@thepastaclaw thepastaclaw added the pastaclaw:commented thepastaclaw's latest review was comment-only label Oct 5, 2026
@PastaPastaPasta
PastaPastaPasta merged commit d721bef into dashpay:develop Oct 6, 2026
45 of 50 checks passed
@thepastaclaw thepastaclaw removed the pastaclaw:commented thepastaclaw's latest review was comment-only label Oct 6, 2026
PastaPastaPasta added a commit that referenced this pull request Oct 7, 2026
…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=&lt;src root&gt; python3 repro_tor_target.py --configfile=&lt;src root&gt;/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)
PastaPastaPasta added a commit that referenced this pull request Oct 7, 2026
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
@UdjinM6 UdjinM6 added this to the 24 milestone Oct 16, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Oct 31, 2026
4 tasks done
PastaPastaPasta added a commit to PastaPastaPasta/dash that referenced this pull request Nov 1, 2026
…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>
PastaPastaPasta added a commit to PastaPastaPasta/dash that referenced this pull request Nov 1, 2026
…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>
PastaPastaPasta added a commit to PastaPastaPasta/dash that referenced this pull request Nov 1, 2026
…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>
PastaPastaPasta added a commit to PastaPastaPasta/dash that referenced this pull request Nov 2, 2026
…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>
knst pushed a commit to PastaPastaPasta/dash that referenced this pull request Nov 2, 2026
…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>
knst added a commit that referenced this pull request Nov 2, 2026
… 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
PastaPastaPasta pushed a commit to PastaPastaPasta/dash that referenced this pull request Nov 2, 2026
…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)
PastaPastaPasta added a commit that referenced this pull request Nov 6, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants