Repository navigation
p2p: apply Core's message size and count limits, and bound queued sends per peer - #939
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>
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>
This was referenced Oct 13, 2026
bkeroack
added a commit
that referenced
this pull request
Oct 15, 2026
…ds per peer (#939) * 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: 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> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit b66aa26)
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
Several P2P paths took no notice of the limits Bitcoin Core applies to a single message, or to what is queued for one peer. Core references are to
src/at current master.Queued sends and
getdata. A peer's outbound queue held 256 messages and counted no bytes.handle_getdataread, deserialized and queued every block a request named, with atry_sendwhose failure it ignored: whatever did not fit was read, dropped, and still charged to-maxuploadtarget, and what did fit stayed in memory for as long as the peer did not read it. The write loop awaited eachwriter.sendwith nothing else in theselect!, so while a write was parked, neither the disconnect signal (disconnectnode, a ban) nor the ping timeout reached the peer.Core stops serving a peer once the bytes queued to it pass
-maxsendbuffer(DEFAULT_MAXSENDBUFFER, 1,000,000 bytes,net.h:99;fPauseSend,net.cpp:1670).ProcessGetDatabreaks on it (net_processing.cpp:2534, 2556) and keeps the rest inm_getdata_requests(:4172);ProcessMessagestakes no further message from the peer while that backlog is non-empty or the buffer is full (:5153,:5157). A peer whose socket has taken no bytes forTIMEOUT_INTERVAL(20 minutes,net.h:59) is dropped (net.cpp:2036-2041, "socket sending timeout"), andm_last_sendmoves on every partial send (net.cpp:1644).Message limits. rust-bitcoin decodes vectors of any length the frame can hold. satd checked no counts, and accepted v1 frames up to 32 MiB (
net/connection.rs), allocating the announced payload before reading any of it and keeping the receive buffer at its largest size for the life of the connection. Core:MAX_PROTOCOL_MESSAGE_LENGTH= 4,000,000; a header announcing more ends the connection (net.h:65,net.cpp:760-765). The receive buffer grows at most 256 KiB ahead of the bytes received (net.cpp:781-782; v2MAX_RESERVE_AHEAD,net.cpp:1327-1357).inv/getdataoverMAX_INV_SZ(50,000) areMisbehaving(net_processing.cpp:126, 4040, 4131).headersoverMAX_HEADERS_RESULTS(2000) isMisbehaving, checked before the headers are read (net_processing.h:50,net_processing.cpp:4740).addr/addrv2overMAX_ADDR_TO_SEND(1000) isMisbehaving, afterSetupAddressRelay(net_processing.cpp:190, 5635).notfoundoverMAX_PEER_TX_ANNOUNCEMENTS + MAX_BLOCKS_IN_TRANSIT_PER_PEER(5016) entries is ignored (net_processing.cpp:5066).Peer text. The user agent was not bounded and was stored, logged at INFO and reported in
getpeerinfo.subveras sent, line breaks and escape sequences included; unknown message types were logged as sent. Core reads the user agent asLIMITED_STRING(strSubVer, MAX_SUBVERSION_LENGTH)(net_processing.cpp:3636), which throws for a longer one (serialize.h:617-628): the exception is caught (net_processing.cpp:5195), theversionis dropped, and the peer stays without a handshake. It keeps the user agent only asSanitizeString(strSubVer)(:3637;util/strencodings.cpp:23-40), and sanitizes message types in its log lines.BIP 157. satd silently ignored
getcfilters/getcfheaders/getcfcheckptrequests with an unsupported filter type (or any request while it serves no filters), an unknown stop hash, a start above the stop, or too long a range. Core'sPrepareBlockFilterRequestdisconnects the peer for each (net_processing.cpp:3262-3310;fDisconnect, no misbehaviour).Fix
net/send_queue.rs(new):PeerSenderreplaces the peer'smpsc::Senderand counts the v1 wire size of each queued message; the write loop takes it off after writing.SendQueueholds the count, the unservedgetdataentries (Core'sm_getdata_requests), and whether the write loop may read the peer's next message.getdatais served asProcessGetDatadoes: entries go to the backlog and are served, oldest first, until the queue passes 1,000,000 bytes or has no free slot. The write loop asks for more (NetEvent::GetDataResume) once the queue is back under the limit. From the moment it hands agetdataover until the backlog is empty, and while the queue is over the limit, the write loop leaves the peer's next message unread, asProcessMessagesdoes; that keeps answers in order and the backlog to one request. Handing over agetdataand asking for more both wake the manager's drain, so a peer fetching blocks is served at once rather than once per 500 ms maintenance tick.-maxuploadtargetis charged only for a block that was queued.send_watched) also watches the disconnect signal, and gives up when the socket has taken no bytes for 20 minutes. v1 and v2 writes count sent bytes as the socket takes them (write_counted), which is what that watch reads.Misbehavingmaps to satd's existingadd_ban_score(id, 100, …), the same mapping used for the otherMisbehavingcases inmanager.rs.versionwith a user agent over 256 bytes is ignored and the handshake keeps waiting, so the existing-peertimeouthandshake deadline ends the connection, as in Core; a well-formedversionafter it is accepted.net/limits.rs(new) has Core'sSanitizeString, applied to the stored user agent (and so to the "Peer connected" line andgetpeerinfo.subver) and to message types in log lines.prepare_block_filter_requestimplementsPrepareBlockFilterRequestfor all three BIP 157 messages, and disconnects where it does. A build withoutblock-filter-indexdisconnects any such request, since it serves no filter type.Tests
Unit tests (
node):net::connection::bounds_tests:a_frame_over_four_million_bytes_is_refused,the_payload_buffer_grows_with_the_bytes_received,the_receive_buffer_is_released_after_a_large_message,a_send_counts_its_bytes_as_the_socket_takes_them.net::v2transport::bounds_tests:a_packet_buffer_grows_with_the_bytes_received,a_send_counts_its_bytes_as_the_socket_takes_them.net::manager::p2pbounds_tests:inv_and_getdata_past_max_inv_sz_are_misbehaviour,headers_past_max_headers_results_are_misbehaviour,addr_and_addrv2_past_max_addr_to_send_are_misbehaviourandnotfound_past_the_limit_is_ignored(each with the at-the-limit case as the must-succeed half);getdata_stops_serving_past_the_send_buffer,getdata_resumes_as_the_queue_drains;peer_user_agent_is_sanitized,version_with_an_oversized_user_agent_is_ignored; write loop:a_parked_write_ends_on_disconnect,a_write_that_makes_no_progress_times_out(paused clock),reading_waits_for_a_getdata_to_be_served,the_loop_asks_for_more_once_the_queue_drains; BIP 157:bip157::{a_valid_request_is_served, bad_requests_disconnect, a_request_to_a_node_not_serving_filters_disconnects, a_range_past_the_limit_disconnects, an_off_chain_stop_hash}.net::send_queue::testsandnet::limits::testsfor the new modules.Integration (
satd/tests/p2p_limits.rs, new target, against a live regtest node):a_frame_over_four_million_bytes_ends_the_connection,an_inv_over_max_inv_sz_ends_the_connection,a_large_getdata_is_served_in_full_as_the_peer_reads(one 50,000-entrygetdata, about 12 MB of blocks, read 3 s late: every block arrives, in order).The existing BIP 157 regtests that asserted the silent drop now assert the disconnect, edited in place and renamed:
test_p2p_disconnect_when_peer_serve_disabled,test_p2p_disconnect_invalid_filter_type,test_p2p_disconnect_oversized_range,test_p2p_disconnect_unknown_stop_hash, and the 2001-header case intest_p2p_getcfheaders_accepts_2000_headers.Fails without the fix: yes. The new unit tests were run on the unfixed tree first (the test files only, plus the two receive-buffer constants they name): 15 failed, each on its own assertion, for example
inv_and_getdata_past_max_inv_sz_are_misbehaviour: "inv of 50001 entries disconnects"headers_past_max_headers_results_are_misbehaviour: "2001 headers disconnect"addr_and_addrv2_past_max_addr_to_send_are_misbehaviour: "case 0: 1001 addresses disconnect"notfound_past_the_limit_is_ignored: "notfound of 5017 entries: 0 heights still in flight"getdata_stops_serving_past_the_send_buffer: "30 blocks, 3007814 bytes queued at once; the bound is 1000000 plus one block"a_frame_over_four_million_bytes_is_refused: the 4,000,001-byte frame was acceptedthe_payload_buffer_grows_with_the_bytes_received: "asked for 4000000 bytes at once"the_receive_buffer_is_released_after_a_large_message: "kept 1000024 bytes after a 1 MB message"a_send_counts_its_bytes_as_the_socket_takes_them(v1): "3900024 sent, 3900024 in whole messages"peer_user_agent_is_sanitized,version_with_an_oversized_user_agent_is_ignored("an oversized user agent must not be answered, got … Version"), and the BIP 157bad_requests_disconnect,a_request_to_a_node_not_serving_filters_disconnects,an_off_chain_stop_hash("a header satd never validated is not a block it would serve").The guards were then removed one at a time in a single perturbed build (read gate, resume request, the send-buffer check in
serve_getdata, the disconnect branch and the timeout insend_watched), and the named tests failed:reading_waits_for_a_getdata_to_be_served,the_loop_asks_for_more_once_the_queue_drains,getdata_stops_serving_past_the_send_buffer,getdata_resumes_as_the_queue_drains,a_parked_write_ends_on_disconnect("the write loop ends while its write is parked: Elapsed"),a_write_that_makes_no_progress_times_out("a write with no progress ends: Elapsed"). The integration tests were not run against the unfixed tree.Notes
Misbehavingis mapped toadd_ban_score(id, 100, …), as it already is elsewhere inmanager.rs, so it currently bans the address. Core's ladder (discourage, and no punishment for manual, local and inbound-onion peers) is a separate change.-maxsendbufferstays an unsupported Core option (warned and skipped, as before); Core's default of 1,000,000 bytes applies.BlockRequestAllowedadmits (a validated block within 30 days of the best header;net_processing.cpp:1951). satd keeps filters for the active chain only, so it cannot serve that request; it ignores it without a disconnect, as before. Anything else off the active chain, such as a header-only or never-validated block, now disconnects. The work-equivalent-age half ofBlockRequestAllowedis not applied, so satd ignores slightly more such requests than Core would refuse.versionwhose user agent is not valid UTF-8 still fails rust-bitcoin's decode and is skipped, where Core would accept and sanitize it. Unchanged here.pingdirectly on the peer's task, so a pong can still go out ahead of messages already queued to the peer. Unchanged here.contrib/core-functional/inventory.toml's note forp2p_addrv2_relay.py("L83 an oversized addrv2 is not disconnected") describes the old behaviour. The row isskipand was not re-measured here.Release notes
satd now applies Bitcoin Core's limits to single P2P messages: a frame over 4,000,000 bytes ends the connection, and an
invorgetdataover 50,000 entries,headersover 2000 oraddr/addrv2over 1000 is treated as misbehaviour. Blocks a peer asks for withgetdataare served as it reads them, with about 1 MB queued per peer as in Core's default-maxsendbuffer, and a peer whose connection has taken no data for 20 minutes is disconnected;disconnectnodeand bans now take effect on a peer that has stopped reading. A peer's user agent is limited to 256 bytes and sanitized before it is logged or reported ingetpeerinfo. BIP 157 filter requests that Core refuses (unknown filter type, unknown stop hash, bad range, or any request while filters are not served) now disconnect the peer instead of being ignored.🤖 Generated with Claude Code