Repository navigation
p2p: bound learned addresses and outbound dials; manual peers outside the automatic cap - #952
Merged
Merged
Conversation
…ds per peer - v1 frames are capped at Core's MAX_PROTOCOL_MESSAGE_LENGTH (4,000,000 bytes) instead of 32 MiB. The v1 and v2 receive buffers grow as payload bytes arrive, at most 256 KiB ahead of them (V1Transport::readData, V2Transport MAX_RESERVE_AHEAD), and a large v1 buffer is released after its message. - inv/getdata over MAX_INV_SZ (50,000), headers over MAX_HEADERS_RESULTS (2000) and addr/addrv2 over MAX_ADDR_TO_SEND (1000) are misbehaviour, as in Core; a notfound over 5016 entries is ignored. - A peer's queue counts its bytes. getdata is served as Core's ProcessGetData does: entries are kept in a per-peer backlog and served until the queue passes 1,000,000 bytes (-maxsendbuffer's default), and the rest as it drains. The peer's next message is not read while a getdata of its is being served or its queue is over the limit (ProcessMessages). -maxuploadtarget is charged only for blocks queued. - The write loop watches the disconnect signal during a write, and drops a peer whose socket takes no bytes for 20 minutes (TIMEOUT_INTERVAL, "socket sending timeout"). Sent bytes are counted as the socket takes them. - A version whose user agent is over MAX_SUBVERSION_LENGTH is dropped, as Core's LIMITED_STRING read does; the user agent and unknown message types are sanitized with Core's SanitizeString before they are stored or logged. - getcfilters/getcfheaders/getcfcheckpt requests that Core's PrepareBlockFilterRequest refuses now disconnect instead of being ignored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The write loop reads nothing more from a peer until its getdata has been served, so the manager's 500 ms drain tick bounded a peer fetching blocks to about one getdata, or one send buffer's worth of a large one, per tick. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deal with a peer whose misbehaviour reaches the threshold as Core's MaybeDiscourageAndDisconnect does (net_processing.cpp), in its order: a noban peer and a manual connection are not punished; a peer on a local address (CNetAddr::IsLocal: 127.0.0.0/8, 0.0.0.0/8, ::1) is disconnected without punishing the address; anyone else is disconnected and its address banned for -bantime, as before (Core discourages it). satd banned every peer but noban ones. Every inbound onion peer arrives from the address Tor dialled from, usually 127.0.0.1, as does every unwhitelisted local client, so one such peer's misbehaviour refused all of them until the ban expired, across restarts. A manual peer was banned and then skipped by the redial loop. Peers accepted on the onion listener are now marked (Core's m_inbound_onion), and getpeerinfo reports them on the onion network, as Core's ConnectedThroughNetwork does. An outbound onion peer is banned by its onion host, the address Core punishes, rather than by the 0.0.0.0 placeholder socket every onion peer shares. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
serve_compact_block reported a MSG_CMPCT_BLOCK entry as answered even when its cmpctblock could not be queued, so the entry left the getdata backlog with nothing sent. It now says whether the message was queued, and an entry that was not stays first, as every other entry does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…/discourage-not-ban-local-onion-manual
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s from Core decides the disconnect-only rung on the address alone (pnode.addr.IsLocal(), net_processing.cpp), and Tor on the same host dials from loopback. With Tor on another host (-bind=<lan address>=onion) every onion peer arrives from that host's address. Core discourages it, and still admits it while inbound slots are free (net.cpp); satd's ban would refuse every onion peer for -bantime. An inbound onion peer is now disconnected without a ban whatever address it arrives from. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the automatic cap - Core's address rate limit (ProcessAddrs): a token bucket per peer, one token to start, 0.1 a second up to 1000, plus 1000 when the node sends getaddr; entries are shuffled first and the addr permission is exempt. getpeerinfo reports addr_processed and addr_rate_limited. - A gossiped address joins the dial list only if the address book holds it. The list is a set in arrival order, capped at the book's size. - reserve_dial checks a dial's limit and takes its place under one lock, counting dials in flight: automatic dials against the automatic target, added nodes at most 8 at a time (Core's semAddnode), -connect, -seednode and onetry without a limit. - The reconnect tick redials manual peers whatever the automatic count, then fills the free automatic slots. Automatic dials skip banned addresses, onion hosts included; manual dials do not (Core's OpenNetworkConnection). Gossip naming a banned onion host is not kept. - Manual backoff stops at 60 s; backoff is cleared only by a dial that connects, not by a new tip. - addnode onetry is outside every limit and succeeds for a peer already connected or being dialled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on-manual' into fix/addr-list-and-dial-limits
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged with B2's own tip (7c8b6ed) as the base, since this branch already carried it and release/0.6 now holds its squashed equivalent. No conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss's backoff goes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the backoff while checking membership Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bkeroack
added a commit
that referenced
this pull request
Oct 14, 2026
Add CHANGELOG [Unreleased] bullets and 0.6.1-pre release-notes entries for #933, #936-#948, #952-#955, #957 and #958: new Consensus and validation, P2P, Mempool, Electrum and Esplora, Indexes and Configuration sections, additions to Chain/IBD/RPC, five new Highlights and the behaviour changes clients and operators will notice in Upgrade notes. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bkeroack
added a commit
that referenced
this pull request
Oct 15, 2026
… the automatic cap (#952) * p2p: apply Core's message size and count limits, and bound queued sends per peer - v1 frames are capped at Core's MAX_PROTOCOL_MESSAGE_LENGTH (4,000,000 bytes) instead of 32 MiB. The v1 and v2 receive buffers grow as payload bytes arrive, at most 256 KiB ahead of them (V1Transport::readData, V2Transport MAX_RESERVE_AHEAD), and a large v1 buffer is released after its message. - inv/getdata over MAX_INV_SZ (50,000), headers over MAX_HEADERS_RESULTS (2000) and addr/addrv2 over MAX_ADDR_TO_SEND (1000) are misbehaviour, as in Core; a notfound over 5016 entries is ignored. - A peer's queue counts its bytes. getdata is served as Core's ProcessGetData does: entries are kept in a per-peer backlog and served until the queue passes 1,000,000 bytes (-maxsendbuffer's default), and the rest as it drains. The peer's next message is not read while a getdata of its is being served or its queue is over the limit (ProcessMessages). -maxuploadtarget is charged only for blocks queued. - The write loop watches the disconnect signal during a write, and drops a peer whose socket takes no bytes for 20 minutes (TIMEOUT_INTERVAL, "socket sending timeout"). Sent bytes are counted as the socket takes them. - A version whose user agent is over MAX_SUBVERSION_LENGTH is dropped, as Core's LIMITED_STRING read does; the user agent and unknown message types are sanitized with Core's SanitizeString before they are stored or logged. - getcfilters/getcfheaders/getcfcheckpt requests that Core's PrepareBlockFilterRequest refuses now disconnect instead of being ignored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: wake the manager when a getdata or a getdata resume is handed over The write loop reads nothing more from a peer until its getdata has been served, so the manager's 500 ms drain tick bounded a peer fetching blocks to about one getdata, or one send buffer's worth of a large one, per tick. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: don't ban local, inbound-onion or manual peers for misbehaviour Deal with a peer whose misbehaviour reaches the threshold as Core's MaybeDiscourageAndDisconnect does (net_processing.cpp), in its order: a noban peer and a manual connection are not punished; a peer on a local address (CNetAddr::IsLocal: 127.0.0.0/8, 0.0.0.0/8, ::1) is disconnected without punishing the address; anyone else is disconnected and its address banned for -bantime, as before (Core discourages it). satd banned every peer but noban ones. Every inbound onion peer arrives from the address Tor dialled from, usually 127.0.0.1, as does every unwhitelisted local client, so one such peer's misbehaviour refused all of them until the ban expired, across restarts. A manual peer was banned and then skipped by the redial loop. Peers accepted on the onion listener are now marked (Core's m_inbound_onion), and getpeerinfo reports them on the onion network, as Core's ConnectedThroughNetwork does. An outbound onion peer is banned by its onion host, the address Core punishes, rather than by the 0.0.0.0 placeholder socket every onion peer shares. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: keep a getdata cmpctblock entry that could not be queued serve_compact_block reported a MSG_CMPCT_BLOCK entry as answered even when its cmpctblock could not be queued, so the entry left the getdata backlog with nothing sent. It now says whether the message was queued, and an entry that was not stays first, as every other entry does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: tidy the MisbehaviourAction docs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: disconnect an inbound onion peer without a ban wherever Tor dials from Core decides the disconnect-only rung on the address alone (pnode.addr.IsLocal(), net_processing.cpp), and Tor on the same host dials from loopback. With Tor on another host (-bind=<lan address>=onion) every onion peer arrives from that host's address. Core discourages it, and still admits it while inbound slots are free (net.cpp); satd's ban would refuse every onion peer for -bantime. An inbound onion peer is now disconnected without a ban whatever address it arrives from. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: bound learned addresses and outbound dials; manual peers outside the automatic cap - Core's address rate limit (ProcessAddrs): a token bucket per peer, one token to start, 0.1 a second up to 1000, plus 1000 when the node sends getaddr; entries are shuffled first and the addr permission is exempt. getpeerinfo reports addr_processed and addr_rate_limited. - A gossiped address joins the dial list only if the address book holds it. The list is a set in arrival order, capped at the book's size. - reserve_dial checks a dial's limit and takes its place under one lock, counting dials in flight: automatic dials against the automatic target, added nodes at most 8 at a time (Core's semAddnode), -connect, -seednode and onetry without a limit. - The reconnect tick redials manual peers whatever the automatic count, then fills the free automatic slots. Automatic dials skip banned addresses, onion hosts included; manual dials do not (Core's OpenNetworkConnection). Gossip naming a banned onion host is not kept. - Manual backoff stops at 60 s; backoff is cleared only by a dial that connects, not by a new tip. - addnode onetry is outside every limit and succeeds for a peer already connected or being dialled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: exempt a two-address test peer from the address rate limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: count only the announced addresses in the addr-fetch test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: setban of an onion host disconnects that onion peer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: a full address book replaces a terrible entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: a full network group replaces a terrible entry; a replaced address's backoff goes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: a full book of terrible tried entries takes new addresses; lock the backoff while checking membership Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit 77e7fbd)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Core references are to
src/at current master.Addresses a peer announces. satd took every entry of every
addr/addrv2message (#939 caps a message at 1000 entries;ProcessAddrs,net_processing.cpp:5625-5704). Each entry went to the address book and, whether the book kept it or not, onto the reconnect loop's dial list, aVecsearched end to end on every insert, with no cap.getpeerinforeportedaddr_processedandaddr_rate_limitedas 0.Core meters the addresses each peer announces with a token bucket (
net_processing.cpp:5640-5698): one token to start (m_addr_token_bucket{1.0},:380),MAX_ADDR_RATE_PER_SECOND= 0.1 more a second up toMAX_ADDR_PROCESSING_TOKEN_BUCKET= 1000 (:193-197), andMAX_ADDR_TO_SENDmore, past that limit, when it sends the peergetaddr(:3767). Entries are shuffled first; one that finds no token is dropped unless the peer has theaddrpermission; a banned address is not counted as processed. Core dials only from its address book.Outbound dials. One limit,
check_outbound_limit, covered automatic and manual dials alike. It counted connected outbound peers only, manual ones included, against the automatic target (8, or 64 during IBD):addnode … onetryfailed with "max outbound connections reached", and the dial fromaddnode add/-addnodefailed silently.onetryto a connected peer failed with "already connected to …"..onion(setban, or a ban for misbehaviour from p2p: don't ban local, inbound-onion or manual peers for misbehaviour (Core parity) #944) went on, and gossip naming one was kept.setban <host>.oniondid not disconnect a connected peer at that host: the disconnect matched bans against peers' IPs, and an onion peer's is the shared0.0.0.0placeholder (BanTarget::contains_addris false for an onion target). Core'ssetbandisconnects the peers the ban covers.Core gives each kind its own limit, taken as a semaphore grant before the dial (
net.cpp): automatic connections takesemOutbound(ThreadOpenConnections); added nodes takesemAddnode,MAX_ADDNODE_CONNECTIONS= 8 (net.h:71), inThreadOpenAddedConnections, which tries again every 60 s whatever the automatic count (net.cpp:2972-3000);-connect(:2539-2556) andaddnode onetry(rpc/net.cpp:352-357) take no grant, andonetryanswers null, also for a host already connected (OpenNetworkConnection,net.cpp:3023-3030).OpenNetworkConnectionchecks bans and discouragement only for an automatic connection (nopszDest); a manual one is opened by the operator's name, banned or not.Fix
net/addr_rate.rs(new): Core's token bucket, kept onPeerInfowith its counters. Theaddrandaddrv2arms shuffle the entries and keep those that find a token (or all of them for a peer with theaddrpermission); sendinggetaddr(addr-fetch peers, and outbound peers under a proxy) credits 1000.getpeerinforeportsaddr_processedandaddr_rate_limited.reserve_dialchecks a dial's limit and takes its place under one lock, counting dials in flight along with the peers (connected or in the handshake): automatic dials against the automatic target, manual connections apart from it; added nodes at most 8 at once;-connect,-seednodeandonetrywith no limit. The place is held until the peer is in the peer map.AddrInfo::IsTerrible,addrman.cpp:49-72: unseen 30 days, three failed attempts and never reached, or ten failures in a week), new entries of the group first, then new, then tried ones; the dropped address leaves the dial list and its backoff state goes too. Failed redials now count as attempts in the book (Core'sAttempt), which nothing recorded before.setbandisconnects an onion peer by its onion host; an IP-range ban no longer reaches onion peers through their placeholder address.addnode onetry(connect_peer_addr_onetry) is outside every limit and succeeds for a peer already connected or being dialled. A dial that fails is still reported, as satd always has.#895: counting dials in flight removes the part of the overshoot that came from one tick starting more dials than there were free slots: the automatic count can no longer pass the target in force when a dial starts. It does not close connections opened while the IBD target (64) applied, which is #895 itself. #894 is unchanged (the dial order is the list order, as before).
Tests
Unit tests,
net::addr_rate::tests(new file) andnet::manager::addrdials_tests(new filenode/src/net/manager_addrdials_tests.rs):addr_rate::tests: one address to start, ten after 100 s, the refill limit of 1000, thegetaddrcredit past it, theaddrexemption, a clock that goes backwards.a_peers_addresses_are_rate_limited_unless_it_has_the_addr_permission,addrv2_is_rate_limited_too,the_answer_to_a_getaddr_is_taken_whole(throughhandle_message).an_address_the_book_has_no_room_for_is_not_dialled(100 addresses in one /16: 64 in the book, 64 on the list),the_dial_list_stops_at_its_cap.a_reconnect_tick_starts_no_more_automatic_dials_than_free_slots(20 learned listeners, 3 automatic peers, target 8: 5 dials, then none).an_added_node_is_dialled_with_the_automatic_slots_full(and an automatic dial is still refused),added_nodes_stop_at_eight_and_other_manual_peers_do_not,onetry_is_outside_the_limits_and_succeeds_for_a_connected_peer(a failed dial is still an error).a_manual_peers_backoff_stops_at_a_minute,a_failed_manual_redial_backs_off_by_the_manual_cap,a_new_tip_leaves_reconnect_backoff_alone(drives the run loop),a_dial_that_connects_clears_its_backoff.a_banned_manual_peer_is_dialled_and_a_banned_learned_one_is_not(reconnect loop and seed dial),a_banned_onion_host_is_neither_learned_nor_dialled_automatically(a banned onion host the operator named is still dialled),setban_of_an_onion_host_disconnects_that_peer_only.a_full_book_replaces_a_terrible_entry,a_full_group_replaces_a_terrible_entry_and_its_backoff,a_book_full_of_stale_tried_entries_takes_a_new_address,a_failed_redial_counts_as_an_attempt(each fails with its guard reverted).Integration tests,
satd/tests/p2p_addr_dials.rs(new target, live regtest node):a_peers_addresses_are_rate_limited_unless_it_has_the_addr_permission: 1000 addresses from an inbound peer →getpeerinfoaddr_processed1,addr_rate_limited999;setmocktime+100 s → 10 more; a peer on a-whitebind=addr@…listener → all 1000 (Core'sp2p_addr_relay.pysteps).an_addr_fetch_peers_answer_is_taken_whole: an addr-fetch peer's 600-address answer togetaddris learned whole.manual_connections_are_outside_the_automatic_outbound_target:-maxconnections=3, out of IBD, three automatic peers;addnode onetryconnects and answers null, again null for the connected peer;addnode addto a port not yet listening is dialled again once it listens.Existing tests edited in place:
gossiped_addresses_keep_their_services_and_source(manager.rs, two addresses from one peer: the peer now hasaddr);an_address_that_names_no_host_is_neither_learned_nor_dialledandoff_regtest_a_gossiped_private_address_is_neither_learned_nor_dialled(satd/tests/regtest.rs) announce several addresses from one inbound peer and expect all of them to be looked at; their nodes now run with-whitelist=addr@127.0.0.1, as Core's own address tests do.Fails without the fix: yes. Each guard was reverted on its own and the named test failed on its assertion:
a_reconnect_tick_starts_no_more_automatic_dials_than_free_slots"left: 20, right: 5";an_added_node_is_dialled_with_the_automatic_slots_fulltimes out;onetrytaking the automatic limit / erroring for a connected peer →onetry_is_outside_the_limits_and_succeeds_for_a_connected_peer(expectfails both ways);added_nodes_stop_at_eight_and_other_manual_peers_do_not"left: 10, right: 8";a_manual_peers_backoff_stops_at_a_minute"left: 300s, right: 60s";a_new_tip_leaves_reconnect_backoff_alone"left: Some(0), right: Some(3)";an_address_the_book_has_no_room_for_is_not_dialled"left: 100, right: 64";the_dial_list_stops_at_its_cap;getaddrcredit →the_answer_to_a_getaddr_is_taken_whole"(1, 999) vs (1000, 0)" and the bucket test;a_banned_manual_peer_is_dialled_and_a_banned_learned_one_is_not("left: 1, right: 0", and the seedexpect_err);setbantest failed on the unfixed code: "left: {1, 2, 3}, right: {2, 3}".Notes
-bantimerather than discouraged (unchanged, as in p2p: don't ban local, inbound-onion or manual peers for misbehaviour (Core parity) #944); the ban checks above use satd's ban list.-maxconnections).-seednodepeers stay manual connections (CORE_DIFFERENCES.md), so they are now redialled like other manual peers whatever the automatic count.getaddronly to addr-fetch peers and, under a proxy, to outbound peers (Core sends it to every outbound full-relay peer); unsolicitedaddrtraffic is metered exactly as Core meters it.getaddrto a block-relay-only outbound peer, which Core never does (manager.rs, the proxygetaddrin the peer setup).Release notes
RELEASE_- Ordinary peers are still banned for
-bantimerather than discouraged (unchanged, as in #944); the ban checks above use satd's ban list.-maxconnections).-seednodepeers stay manual connections (CORE_DIFFERENCES.md), so they are now redialled like other manual peers whatever the automatic count.getaddronly to addr-fetch peers and, under a proxy, to outbound peers (Core sends it to every outbound full-relay peer); unsolicitedaddrtraffic is metered exactly as Core meters it.getaddrto a block-relay-only outbound peer, which Core never does (manager.rs, the proxygetaddrin the peer setup).🤖 Generated with Claude Code