btclib_node.p2p package¶
Subpackages¶
Submodules¶
btclib_node.p2p.address module¶
Where a peer is, and the table of the ones this node knows of.
The address itself is btclib’s, and btclib.p2p.addrv2.NetworkAddressV2 is the one this node holds a peer in: BIP155’s record is the only encoding that carries every network a peer can be on, so the narrower addr entry would lose an onion peer the moment one is gossiped. What goes on the wire is that entry all the same wherever the peer has not asked for BIP155, and addr_entry and peer_from_addr_entry are the translation.
What is left here is what btclib has no business holding: dialling a socket, and the table of addresses to dial. btclib is a codec – it speaks to nobody – so the question “can this be connected to” and the answer to it are this node’s.
- class btclib_node.p2p.address.PeerDB(chain: Chain, data_dir: Path | None)[source]¶
Bases:
objectThe table of addresses this node knows of, gossiped and self-confirmed.
addresses is every address heard about; active_addresses is the subset this node has itself dialled and heard back from recently. Each is behind its own lock, taken separately and never nested – the comment beside each lock’s own field says which thread reaches it and why sharing the other lock was declined.
- add_active_address(addr: NetworkAddressV2) None[source]¶
Record addr as dialled and answered, just now.
A repeat handshake with an already-held endpoint settles onto its one row rather than growing the table. Locked with _active_lock.
- add_addresses(addresses: Iterable[__annotationlib_name_1__]) None[source]¶
Merge addresses into self.addresses, checked and deduplicated.
BIP155’s embedded-IPv6 records are dropped; every other address settles onto its own endpoint_key row, up to _MAX_ADDRESSES distinct endpoints, past which a genuinely new one is dropped too. Locked with _addresses_lock.
- get_active_addresses() list[NetworkAddressV2][source]¶
Return active_addresses, pruned of every entry older than 3 hours.
A pruned entry’s durable answered- row is deleted too. Locked with _active_lock.
- async get_addr_from_dns() None[source]¶
Resolve every chain DNS seed and feed the answers to add_addresses.
A no-op unless ask_dns_nodes said, at construction time, that the durable table came back with nothing dialable.
- init_from_db() None[source]¶
Load every stored address into addresses or active_addresses.
One store keyed by two prefixes (the comment on _KNOWN and _ANSWERED above argues why), so this walks it whole and dispatches on the prefix rather than stopping at the first key without one.
- random_address() NetworkAddressV2 | None[source]¶
Return a random dialable address, or None if there is none.
Preferred from get_active_addresses’s own dialable subset; falls back to addresses whole, locked, only if that is empty.
- btclib_node.p2p.address.addr_entry(address: NetworkAddressV2) TimestampedNetworkAddress[source]¶
Return the addr version 1 entry a BIP155 record is, where it is one.
- btclib_node.p2p.address.can_addrv1(address: NetworkAddressV2) bool[source]¶
Answer whether an addr version 1 message has room for this peer.
- btclib_node.p2p.address.can_connect(address: NetworkAddressV2) bool[source]¶
Answer whether this node has a dial for the peer’s network.
- async btclib_node.p2p.address.dial(address: NetworkAddressV2) socket | None[source]¶
Return a socket connected to the peer, or nothing if it never came up.
dial and not connect, which is what P2pManager calls the whole of making a connection out of one: this is the socket alone.
loop.sock_connect is the kernel’s own answer rather than a guess at it: a refusal is SO_ERROR on the socket, read the moment the OS notifies the loop’s writer callback, not inferred after a fixed number of getpeername polls that cannot tell a refusal from a peer that is merely slow. And where connect completes without ever raising BlockingIOError – a local peer most often – sock_connect returns at once instead of an except arm that never runs.
No separate check for a host with no route to the family being dialled: _DIAL_TIMEOUT already bounds every attempt, and an unreachable family fails the same sock_connect a slow or refusing peer does, landing on the same None P2pManager already treats as “try someone else”. Bitcoin Core’s own default (ReachableNets, src/netbase.h at 58a7869f86: “Everything is reachable by default”) is the same bet – reachability is what a dial’s outcome says it is, not a property guessed at beforehand – so there is nothing here for a heavier check to buy.
- btclib_node.p2p.address.endpoint_key(address: NetworkAddressV2) bytes[source]¶
Return the octets a persisted address is keyed on.
The network id, the address and the port – what names an endpoint on the wire – and not timestamp or services: those are this node’s own opinion of the endpoint, not part of what it is, so two records differing only in them settle on the one row written last rather than growing the table an entry per gossip or per reconnect.
- btclib_node.p2p.address.ip_and_port(ip: str, port: int) str[source]¶
Return the endpoint the way Core’s CService::ToStringAddrPort does.
“[” + ToStringAddr() + “]:” + port_str for every network that function’s IsIPv4() || IsTor() || IsI2P() || IsInternal() does not name. The brackets are what tells a v6 host from its port: 2001:db8::1 on port 8333 and 2001:db8::1:8333 on some other port are both addresses, and without brackets both render as the second.
The host’s text rather than the NetworkAddress a peer is held in, because a socket’s getpeername has no such object to offer and answers with this.
A v4-mapped host is unwrapped rather than bracketed, which is Core’s answer too: CNetAddr::SetLegacyIPv6 files a mapped address under NET_IPV4, which that predicate names. Without the unwrapping a v4 peer would read [::ffff:1.2.3.4]:8333, a NetworkAddress holding every address in the sixteen octets of an IPv6 one.
Raises ValueError where the host is not an IP address, which is what ipaddress.ip_address answers with: a hostname is refused rather than shown with brackets guessed at.
- btclib_node.p2p.address.network_address(address: NetworkAddressV2) NetworkAddress[source]¶
Return the untimestamped form of a BIP155 record, where it has one.
What a version message’s two addresses are, and what an addr entry is built on. can_addrv1 is the question a caller asks first; the refusal below is what makes the answer binding rather than advisory, because the length would not catch it: BIP155 gives cjdns and yggdrasil the sixteen octets an IPv6 address has, so IPv6Address would take either for an IP address and hand back a peer that is not the one that was gossiped.
- btclib_node.p2p.address.peer_address(ip: str, port: int, timestamp: int = 0, services: int = 0) NetworkAddressV2[source]¶
Return the BIP155 record of a peer named by the text of its IP.
ipaddress.ip_address is what tells the two IP networks apart, and the octets it packs are what BIP155 asks for: four for a v4 peer and sixteen for a v6 one, where an addr entry would carry the v4 one mapped into sixteen.
- btclib_node.p2p.address.peer_from_addr_entry(entry: TimestampedNetworkAddress) NetworkAddressV2[source]¶
Return the BIP155 record an addr version 1 entry describes.
An addr entry holds every address in sixteen octets, a v4 one mapped into them, where BIP155 gives the two networks different ids and different lengths: ipv4_mapped is what tells them apart, and it is the reason this is not a field rename.
btclib_node.p2p.callbacks module¶
One handler per p2p message type, and the two tables that dispatch to them.
callbacks is read by p2p.main.handle_p2p for a connection past its handshake; handshake_callbacks is read by p2p.main.handle_p2p_handshake for a connection still completing one. Every handler shares the same signature, (node, msg, conn), whether or not its own body reads every argument – the dispatch table calls each one uniformly, and an unread msg or conn documents that rather than a mistake.
advance_getdata and advance_cfilters are the two exceptions to “one handler, one message”: getdata and get_cfilters below, and p2p.main.resume_getdata and resume_cfilters, each call one of them to pace an answer against the connection’s own send queue, across however many turns of Node’s own loop that answer takes to drain.
- btclib_node.p2p.callbacks.addr(node: Node, msg: bytes, conn: Connection) None[source]¶
Merge the addr-version-1 entries a peer gossiped into the table.
- btclib_node.p2p.callbacks.addrv2(node: Node, msg: bytes, conn: Connection) None[source]¶
Merge the BIP155 entries a peer gossiped into the address table.
- btclib_node.p2p.callbacks.advance_cfilters(node: Node, conn: Connection, heights: deque[int]) bool[source]¶
Send from the front of heights while conn’s own queue has room.
Shared by get_cfilters, dispatching a request for the first time, and by p2p.main.resume_cfilters, retrying one already paused – each pops what it sends off the front of the same deque, so a later call, on a later turn of Node’s own loop, picks up exactly where the last one left off rather than resending or skipping a height. Answers whether heights is now empty.
Checked before every send rather than after, against the same field advance_getdata above paces on, unlocked for the reason argued there: conn.send counts a filter on this thread before scheduling it, and what the read can still miss is a drain, which only ever makes this pause sooner. conn.status beside it is read the same way: seen one turn late it costs a filter serialized for a socket already closed, which Connection._send suppresses.
- btclib_node.p2p.callbacks.advance_getdata(node: Node, conn: Connection, items: deque[Inventory]) bool[source]¶
Serve from the front of items while conn’s own queue has room.
Shared by getdata below, dispatching a request for the first time, and by p2p.main.resume_getdata, retrying one already paused – each pops what it serves off the front of the same deque, the shape advance_cfilters below already gives get_cfilters.
A transaction is served from the mempool only if the peer wants it relayed, answered notfound on a miss; a requested block not held is silent. Both match Core – BIP37’s fRelay is written about announcements, “broadcast transactions will not be announced”, and says nothing about a transaction a peer asks for by hash, but Core answers nothing anyway: with fRelay false and NODE_BLOOM not offered, ProcessGetData skips every transaction item outright, and where NODE_BLOOM is offered, FindTxForGetData gates on m_last_inv_sequence, which never advances for a peer nothing is announced to. This node follows Core rather than the sentence, and the reason is what the sentence does not cover: serving the mempool by hash to a peer that declined announcements answers, for anyone willing to ask, whether a given transaction reached this node – and a peer that declined is the one with no other reason to be asking. Blocks are not affected: a peer that wants no transactions is still a peer syncing the chain. A block this node does not hold gets no notfound either: ProcessGetBlockData returns on one with no notfound of its own, vNotFound being ProcessGetData’s own local and never touched by the function it calls out to for a block item. _below_prune_threshold’s own docstring is where a pruned node’s other answer to a block item – disconnecting rather than staying silent – is argued against the same function.
conn.queued_send_bytes is read the same way advance_cfilters below reads it, and holds what conn.send has counted – this loop’s own previous items among them, since it counts on this thread before scheduling anything on P2pManager’s. A check reading only what that loop had got round to writing would see none of them and serve the whole request as fast as it can pop it, past MAX_QUEUED_SEND_BYTES and into the drop, for a peer asking for the blocks this node asks its own peers for (btclib-org/btclib-node#512).
What the read can miss is either half of a count it did not make. A drain is the loop’s – conn.send counts on this thread, but the decrement once the write completes is not – and that direction is the safe one, an unseen decrement making the number too large and this pause sooner. An increment can also be missed, and that one is not: P2pManager’s thread reaches _queue too, through _prune_stale_connections’s send_ping, so a read here can predate a ping and pause later rather than sooner. What makes that immaterial is the magnitude rather than the direction: one ping is a bare envelope and a nonce, where what MAX_QUEUED_SEND_BYTES leaves above this loop’s own bound is a whole block message and the room over it (connection.py) – so the read needs no lock, and a torn one is not a risk to guard against either (CPython never hands back a value that was not, at some point, actually written).
notfound batches whatever this call found missing, sent once this call is done serving – whether items ran out or this paused – rather than once for the whole original request: Core’s own vNotFound is a per-call local too, built and sent fresh by every ProcessGetData call rather than carried across them.
A miss is paced too, against the same bound, though nothing is sent for one the moment it is found. not_found_bytes is this call’s own running total of what a notfound batching every miss collected so far would cost – _NOTFOUND_ITEM_BYTES per entry, counted the instant a miss joins not_found rather than once the batch is finally sent. Read together with conn.queued_send_bytes at the top of the loop, it is what makes a run of misses pause the same way a run of blocks already does, rather than accumulating for free and landing in one send with no pacing check in front of it (btclib-org/btclib-node#529): before this, nothing charged a miss anything, so conn.queued_send_bytes could still read zero after fifty thousand of them, and the loop had no reason to stop before popping every item this request named.
A batch is also flushed – sent and reset, without pausing the call – once it reaches MAX_INV_SZ on its own, whatever conn.queued_send_bytes reads: NotFound.assert_valid refuses more entries than that, and node.pending_getdata can hand this function a backlog of MAX_PENDING_GETDATA_ITEMS (getdata below), twice MAX_INV_SZ, drawn from two stacked requests rather than the one this bound was sized against. All of that many being misses is an entirely mundane way to reach it – every hash in both requests having left the mempool between the first getdata and the second is enough – and at _NOTFOUND_ITEM_BYTES apiece the byte bound above alone would let it happen: the whole backlog’s own worth of misses is still short of MAX_GETDATA_INFLIGHT_BYTES. Chunking on the item count this class already enforces is what keeps that backlog from reaching NotFound’s own constructor as one batch that raises instead of one this connection can be paced on.
- btclib_node.p2p.callbacks.block(node: Node, msg: bytes, conn: Connection) None[source]¶
Store a requested block once its proof of work checks out.
A no-op if this block is already marked downloaded. Invalidates it first and re-raises on a failed check, so the next peer offering the same block is refused before being asked for it.
An unsolicited block whose own header this node has never indexed is not read as though getdata or headers already vouched for it: PeerManagerImpl::ProcessMessage’s own NetMsgType::BLOCK arm (net_processing.cpp, at bitcoin/bitcoin@ca7162cde5) runs every block through ChainstateManager::AcceptBlock, which calls AcceptBlockHeader (validation.cpp, same sha) on the block’s own header before anything else – a header already known is accepted outright, and one that is not has its own parent looked up, refused with BLOCK_MISSING_PREV where that parent is unknown too. Core punishes that refusal: MaybePunishNodeForBlock’s own switch (net_processing.cpp, same sha) calls Misbehaving for BLOCK_MISSING_PREV, unlike an unconnecting headers batch, which ProcessHeadersMessage’s own HandleUnconnectingHeaders answers by asking for more rather than by punishing – the same asymmetry this file already carries between headers below, which never discourages a batch connecting to nothing this node knows (btclib-org/btclib-node#233), and this function, which does. block_index.add_headers([block.header]) is AcceptBlockHeader’s own shape: it indexes the header where the parent is known, raises a BTClibException where the header itself is invalid – main. handle_p2p’s own except already drops and discourages the peer for either, the same way it already does for a block failing its own proof of work below – and, for a single header whose parent is missing, returns None rather than raising, which is headers’s own “ask again” case and not this one’s: BTClibValueError is raised here instead, for main.handle_p2p’s same except to discourage the peer over, matching Misbehaving. btclib-org/btclib-node#711
- btclib_node.p2p.callbacks.feefilter(node: Node, msg: bytes, conn: Connection) None[source]¶
Record the peer’s own BIP133 minimum feerate, or none if invalid.
- btclib_node.p2p.callbacks.get_cfcheckpt(node: Node, msg: bytes, conn: Connection) None[source]¶
Answer a BIP157 getcfcheckpt with one filter header per checkpoint.
Silent for an unsupported filter type or an unknown stop hash.
- btclib_node.p2p.callbacks.get_cfheaders(node: Node, msg: bytes, conn: Connection) None[source]¶
Answer a BIP157 getcfheaders with the requested range’s filter headers.
Silent on a request _filter_range refuses.
- btclib_node.p2p.callbacks.get_cfilters(node: Node, msg: bytes, conn: Connection) None[source]¶
Answer a BIP157 getcfilters with one cfilter per requested height.
Silent on a request _filter_range refuses. “sequentially in order by block height” is BIP157’s own words and the reason this is the one request answered by many messages rather than one; _filter_range already bounds how many, and advance_cfilters above is where the rate they are produced at is bounded too, registering what it could not finish on node.pending_cfilters for p2p.main.resume_cfilters to complete.
A second getcfilters arriving while conn’s own entry there is still paused extends that same deque rather than replacing it – MAX_PENDING_CFILTERS_HEIGHTS, beside advance_cfilters above, is where that bound and the reasoning behind it are. _filter_range has already validated and bounded this request’s own range before that check runs, so what is refused there is refused whole: no partial answer is ever started for a range this node will not finish.
- btclib_node.p2p.callbacks.getaddr(node: Node, msg: bytes, conn: Connection) None[source]¶
Answer a peer’s getaddr with a sample of known addresses, once.
The sample itself is a cache, shared and redrawn only once its own lifetime and jitter expire – the comment below argues why.
- btclib_node.p2p.callbacks.getdata(node: Node, msg: bytes, conn: Connection) None[source]¶
Answer a peer’s request for the transactions and blocks it named.
advance_getdata above is where every item is actually served, and where this request’s own place in Core’s getdata semantics is argued; this is only where a fresh request joins whatever this connection has not yet finished serving.
A second getdata arriving while conn’s own entry on node.pending_getdata is still paused extends the same deque rather than replacing it, up to MAX_PENDING_GETDATA_ITEMS – past which a third stacked request is silent, the same answer get_cfilters below already gives a request past its own MAX_PENDING_CFILTERS_HEIGHTS, and for the same reason: dropping the connection over pipelining this node already tolerates elsewhere would be disproportionate to what tripped it, and MAX_QUEUED_SEND_BYTES (connection.py) is still underneath this to catch a peer that is actually abusive.
Core’s own protection here is not a numeric cap either, whatever reading only Peer.m_getdata_requests (appended to at net_processing.cpp:4472) suggests. ProcessMessages (net_processing.cpp:5429-5436, at bitcoin/bitcoin@b91d983f66) is where it actually lives: “this maintains the order of responses and prevents m_getdata_requests to grow unbounded”, by returning before PollMessage – the call that reads this connection’s own next message off the wire – whenever m_getdata_requests is still non-empty, and again whenever fPauseSend is set. Core therefore never backlogs more than one request’s own MAX_INV_SZ items per connection: it simply stops reading that connection’s next message, getdata included, until the current one has drained.
That discipline does not port here without a larger redesign: P2pManager.messages (p2p/manager.py) is one deque shared by every connection, and handle_p2p (p2p/main.py) pops one message off its front regardless of which connection sent it, where Core’s own m_getdata_requests and PollMessage are both per connection to begin with – there is no single connection this node could “stop reading from” without reordering that shared queue or giving each connection a backlog of its own. MAX_PENDING_GETDATA_ITEMS above is this tree’s own bound in place of that redesign.
- btclib_node.p2p.callbacks.getheaders(node: Node, msg: bytes, conn: Connection) None[source]¶
Answer a peer’s getheaders with what its own locator resolves to.
Silent where the locator names nothing this node’s own header_index holds – there is nothing to answer with, not a refusal.
- btclib_node.p2p.callbacks.headers(node: Node, msg: bytes, conn: Connection) None[source]¶
Index a batch of headers, ask for more, or mark header sync finished.
A batch connecting to nothing known asks again from what this node already has; a full-sized batch asks for the next one; a shorter batch that still connected means the peer has nothing more to give, which is what finishes header sync.
- btclib_node.p2p.callbacks.inv(node: Node, msg: bytes, conn: Connection) None[source]¶
Ask for headers behind an announced block, queue missing transactions.
A no-op before this node’s own chain is synced.
- btclib_node.p2p.callbacks.not_found(node: Node, msg: bytes, conn: Connection) None[source]¶
Clear the in-flight record for a transaction the peer could not answer.
A block item carries no such bookkeeping to clear – the comment below argues why.
- btclib_node.p2p.callbacks.ping(node: Node, msg: bytes, conn: Connection) None[source]¶
Answer a ping with a pong carrying the same nonce.
- btclib_node.p2p.callbacks.pong(node: Node, msg: bytes, conn: Connection) None[source]¶
Match a pong to the outstanding ping and record the round trip.
A nonce that does not match the one this node last sent is a protocol violation, discouraged and dropped rather than matched.
- btclib_node.p2p.callbacks.reject(node: Node, msg: bytes, conn: Connection) None[source]¶
Log a peer’s reject message.
- btclib_node.p2p.callbacks.sendaddrv2(node: Node, msg: bytes, conn: Connection) None[source]¶
Record that the peer wants addrv2 gossip rather than addr.
- btclib_node.p2p.callbacks.sendheaders(node: Node, msg: bytes, conn: Connection) None[source]¶
Record that the peer wants new blocks announced as headers (BIP130).
- btclib_node.p2p.callbacks.tx(node: Node, msg: bytes, conn: Connection) None[source]¶
Validate an unsolicited transaction and queue it for announcement.
A no-op before this node’s own chain is synced, or if the mempool already holds it, or if add_tx itself declines to keep it.
- btclib_node.p2p.callbacks.verack(node: Node, msg: bytes, conn: Connection) None[source]¶
Complete a peer’s handshake: promote it and send the follow-up messages.
Refuses a verack ahead of its own version/wtxidrelay, and records the peer’s own address as reachable once promoted – the comment below is where that recording is argued.
- btclib_node.p2p.callbacks.version(node: Node, msg: bytes, conn: Connection) None[source]¶
Handle a peer’s version: refuse an incompatible peer, else continue.
A second version ahead of this connection’s own verack is ignored outright – Core’s own guard, pfrom.nVersion != 0 (net_processing.cpp:3823, at bitcoin/bitcoin@5f45583e43), which logs and returns before doing anything else. conn.status stays Open until verack promotes it, so #283’s own discourage-and-drop for a handshake command out of order never reaches a repeat sent before that point – unguarded, every repeat would resend WtxidRelay, SendAddrV2 and Verack in answer. btclib-org/btclib-node#482
Continuing means answering wtxidrelay, sendaddrv2 and verack, and recording whether the peer asked to have transactions relayed.
btclib_node.p2p.connection module¶
Connection, one peer-to-peer socket and the messages framed over it.
Reads btclib.p2p.message.Message`s off the wire and hands each one to `P2pManager, writes what Node’s own thread queues back out, and bounds what it will buffer in either direction – MAX_PROTOCOL_MESSAGE_LENGTH on what any one message may claim to be, MAX_QUEUED_RECV_BYTES on how much of what this connection has already handed to P2pManager.messages or P2pManager.handshake_messages may sit there unprocessed before this connection’s own run stops reading any further, and a send buffer capped the way Core’s own -maxsendbuffer caps one, per the comments beside each below.
- class btclib_node.p2p.connection.Connection(manager: P2pManager, client: socket.socket, address: NetworkAddressV2, connection_id: int, *, inbound: bool)[source]¶
Bases:
objectOne peer-to-peer socket and everything owed to or by it.
The module docstring above is where its own three jobs – framing, writing what Node’s thread queues, and bounding what it buffers – are argued.
- async async_send(payload: Payload) None[source]¶
Frame payload and send it, dropping the connection past the bound.
What send_version awaits: it runs on the loop already, and wants this node’s own version on the wire before run reads anything back. Every other sender in this tree reaches send below instead.
- parse_messages() None[source]¶
Parse every whole message in buffer, queueing each on the manager.
Leaves a trailing partial message in buffer for the next read, and routes each parsed one to handshake_messages or messages – ping/pong pushed to the front of the latter. Every item carries its own wire size alongside it, a fourth tuple element handle_p2p or handle_p2p_handshake (p2p/main.py) weighs back off queued_recv_bytes once it is processed (btclib-org/btclib-node#462); handshake_messages is still drained whole every pass of Node’s own loop rather than sharing messages’s own log2-scaled share, which bounds how long a backlog persists but not how large one can grow between two passes – what the size on this queue’s own items is for, argued beside consumed below. btclib-org/btclib-node#482
Peeks the header’s own length field in buffer before building a stream or calling Message.parse at all: a chunk that does not yet complete even the first message in buffer returns here without copying anything. That is the common case on a connection carrying one large message over many reads – a block during initial block download chief among them – and it is what keeps such a message copied a constant number of times overall rather than once per chunk. btclib-org/btclib-node#438
- async run() None[source]¶
Send version, then read and dispatch messages until stop.
Always ends in stop, whether by a graceful return, a caught exception, or the finally below catching a cancellation from outside this loop – the comment above this method argues why.
- send(msg: Payload) None[source]¶
Frame and count msg here, and schedule its write onto the loop.
The synchronous entry point, safe to call from any thread: run_coroutine_threadsafe is what lets both Node’s own thread (through the p2p.callbacks handlers) and P2pManager’s own (through send_ping) reach the loop without ever awaiting directly. Only the write is scheduled: _queue runs here, on the caller’s own thread, so that a caller sending several messages in a row – advance_getdata (p2p/callbacks.py) serving one block per turn of its own loop and pacing on queued_send_bytes between two of them – reads its own hand-off back rather than a count the loop has yet to make.
Serializing here rather than on the loop is what that costs, and it is paid by the thread that asked for the message: for the largest of them, a block, that is the thread which has just parsed the same block out of block_db to build the payload at all.
- send_ping() None[source]¶
Send a ping with a fresh nonzero nonce, recording it under lock.
Called from Node’s own thread, twice over (callbacks.verack once a handshake completes, and rpc.callbacks.ping through ping_all), and from P2pManager’s own thread once (manage_connections); _ping_lock is what keeps its own two writes one step against callbacks.pong’s read and clear.
- btclib_node.p2p.connection.frame_message(stream: BytesIO, magic: bytes) Message[source]¶
Parse one whole message off stream, checking it against magic.
The two-line body parse_messages’s own loop used to inline, split out so fuzz/fuzz_framing.py can drive it directly – matching Core’s own p2p_transport_serialization.cpp fuzz target, which likewise feeds raw octets to a V1Transport constructed with no wider node context (at bitcoin/bitcoin@ca7162cde5): the framing is a separable step, fed octets rather than a whole peer connection.
Raises IncompleteMessageError – Message.parse’s own refusal, rewinding stream to the start of the partial message – where stream does not yet hold a whole message, and WrongNetworkMagicError where it does but the message’s own magic disagrees with magic. Never touches stream beyond what Message.parse itself consumes, so a caller looping this over several whole messages in one buffer – parse_messages below – keeps every one of Message.parse’s own stream-position guarantees.
- btclib_node.p2p.connection.frame_message_bytes(data: bytes) Message[source]¶
Return the one-argument, octet-only shape fuzz/fuzz_framing.py drives.
RegTest’s own magic – this tree’s cheapest chain to construct, a bare no-argument Chain (chains.py) – rather than a magic threaded through the entry point: a bare magic mismatch is exactly the one check frame_message makes beyond Message.parse itself, and is worth fuzzing on its own footing rather than fixed away.
Trailing octets past the one message framed are left unread, the same way parse_messages leaves them in self.buffer for the next read rather than treating them as this message’s own problem – not a refusal, so a seed exercised by tests/fuzz_corpus_test.py’s own round-trip check (frame_message_bytes(seed).serialize() == seed) must not carry any.
btclib_node.p2p.filter_size module¶
One BIP158 basic filter’s own size, estimated once rather than twice.
connection.py’s own MAX_QUEUED_SEND_BYTES and callbacks.py’s own MAX_CFILTERS_INFLIGHT_BYTES (get_cfilters’s pacing bound) both need a peer-facing filter’s size to size a bound from. A module of its own is what keeps that estimate in one place rather than in both, where the two could silently drift apart – connection.py already imports callbacks.py for handshake_callbacks, so callbacks.py importing back from connection.py would cycle, and neither module’s own job is to hold a number the other one needs too.
btclib_node.p2p.main module¶
handle_p2p, handle_p2p_handshake, resume_cfilters and resume_getdata.
The first two pop one message off their own queue – P2pManager.messages or P2pManager.handshake_messages – and dispatch it through p2p.callbacks.callbacks or p2p.callbacks.handshake_callbacks depending on the connection’s own P2pConnStatus. An exception raised by a callback stops that connection rather than the loop, and is discouraged for where it is a parse failure from the peer’s own bytes rather than a bug in the handler.
Each also weighs its own queued item’s wire size back off the connection it came from, queued_recv_bytes, resuming that connection’s own reads (Connection.run) once enough of what it queued is off either queue – the other end of the pacing Connection.parse_messages and MAX_QUEUED_RECV_BYTES (p2p/connection.py) start, argued there. btclib-org/btclib-node#462, btclib-org/btclib-node#482
resume_cfilters and resume_getdata instead drain node.pending_cfilters and node.pending_getdata, the connections p2p.callbacks.get_cfilters and p2p.callbacks.getdata paused mid-answer rather than scheduling ahead of what a peer has drained – nothing queued triggers either, so both are called once every pass of run’s own loop regardless.
- btclib_node.p2p.main.handle_p2p(node: Node) None[source]¶
Pop one queued message and dispatch it, once its handshake is done.
A message ahead of verack, or one arriving out of order otherwise, gets the connection discouraged and stopped rather than dispatched; a callback that raises stops it too, discouraged only for a BTClibException (the comment below argues why that split matters).
Weighs the item’s own size back off the connection’s queued_recv_bytes the moment it is popped, whatever happens to it next – dispatched, ignored for want of a callback, or dropped along with a connection out of handshake order – since what MAX_QUEUED_RECV_BYTES paces is how much of a connection’s own traffic sits unprocessed, not how that traffic was resolved. A connection paused there is resumed, via call_soon_threadsafe rather than a direct set(), from Node’s own thread onto the connection’s (Connection.__init__’s own comment on _recv_resume argues why the indirection is required). btclib-org/btclib-node#462
- btclib_node.p2p.main.handle_p2p_handshake(node: Node) None[source]¶
Pop one queued handshake message and dispatch it, or drop the peer.
A message out of handshake order gets the connection discouraged and stopped rather than dispatched; a callback that raises stops it too, discouraged only where the exception is a BTClibException.
Weighs the item’s own size back off the connection’s queued_recv_bytes the moment it is popped, the same as handle_p2p below and for the same reason – argued there. btclib-org/btclib-node#482
- btclib_node.p2p.main.resume_cfilters(node: Node) bool[source]¶
Advance every paused getcfilters answer by what now fits.
Answers whether anything did – a connection dropped from node.pending_cfilters counts, same as one whose heights shrank from this function’s own vantage point (a getcfilters extending it runs inside get_cfilters, strictly before this is called again, so growth is never what a pass here sees), so this only answers False where every paused connection was tried and stayed exactly as paused as it already was. node.pending_cfilters maps a connection id to the connection itself and the heights advance_cfilters (p2p.callbacks) has not yet sent – entered there only when that call paused rather than finished, and read and written only here and in get_cfilters itself, both on Node’s own thread, so nothing here needs a lock any more than get_cfilters’s own loop over a fresh request does.
A connection already closed is dropped without trying it – stop can be called from P2pManager’s own thread too, but the flag it sets, P2pConnStatus.Closed, is read here the same way advance_cfilters already reads it mid-answer. An exception out of advance_cfilters is handled the same way handle_p2p’s own is above, since it is the same call raising it, just on a later turn.
- btclib_node.p2p.main.resume_getdata(node: Node) bool[source]¶
Advance every paused getdata answer by what now fits.
The same shape as resume_cfilters above, over node.pending_getdata and advance_getdata (p2p.callbacks) instead: answers whether anything did, a connection dropped counting the same as one whose items shrank; a connection already closed is dropped without trying it; and an exception out of advance_getdata is handled the same way handle_p2p’s own is above, being the same call raising it on a later turn.
btclib_node.p2p.manager module¶
P2pManager, the thread listening for and dialing peer connections.
Runs its own asyncio loop – manage_connections accepts inbound sockets, dials outbound ones from PeerDB, and prunes an idle or handshake-stuck Connection – and hands finished messages back to Node’s own thread through messages and handshake_messages. A coroutine enters this loop only through run_coroutine_threadsafe; Node’s own thread calls this class’s plain methods, such as verack’s own promote_connection, directly.
- class btclib_node.p2p.manager.P2pManager(node: Node, port: int | None, peer_db: PeerDB)[source]¶
Bases:
ThreadThe thread listening for and dialling peer connections.
The module docstring above is where its own loop, its two message queues and the boundary with Node’s thread are argued; connections/pending_connections and the lock that guards moving a connection between them are this class’s own state for that.
- add_pending_outbound_nonce(nonce: int) None[source]¶
Record nonce as this outbound, still-unhandshaken connection’s own.
The only caller is Connection.send_version, for an outbound connection. _connections_lock (__init__) is what every access to pending_outbound_nonces goes through – this write included – so it can never land between is_self_connect_nonce below reading the set and returning.
- async async_connect(address: NetworkAddressV2) None[source]¶
Dial address and, if it comes up, register the connection.
- broadcast_raw_transaction(tx: BtclibTx, fee: int) None[source]¶
Queue tx for the inv/getdata round trip, not a direct send.
The comment below is where this, and fee going unread here, are argued.
- connect(address: NetworkAddressV2) None[source]¶
Schedule async_connect(address) onto this manager’s own loop.
- create_connection(client: socket, address: NetworkAddressV2, *, inbound: bool) None[source]¶
Build a Connection for client, hold it pending, and start it.
Logs the id this connection is given beside the address it was accepted from or dialled to – the one point every path into a connection shares, before any wire message is parsed, and so the only point at which a handshake exception raised before callbacks.verack reaches its own pairing (p2p/callbacks.py) still leaves this id resolvable to a peer. verack’s own line is not redundant with this one despite both naming an address: that one marks the handshake completing, this one marks the connection existing, and an operator reading debug.log wants both moments where a connection dies between them. btclib-org/btclib-node#611
network_address never raises building that address here: an inbound address only ever comes from peer_address (server below), which only ever returns the two IP networks network_address accepts, and an outbound one only reaches this method once dial (p2p/address.py) has already returned a live socket for it, which dial itself never does for anything else (UnsupportedAddressTypeError) – random_address (p2p/address.py) filtering _maybe_dial_more_peers’s own draw to the same two networks first is belt on top of that braces, not what does the guarding.
info, matching verack’s own line: this runs once per connection actually made, dialled or accepted, never once per attempt – async_connect and _maybe_dial_more_peers below only call this once dial has already returned a socket, so a dial that goes nowhere never reaches here to begin with.
Unconditional on the address, like verack’s own line and for the same reason – argued there rather than twice here: Core’s analogous site, CNode’s own constructor (src/net.cpp, at bitcoin/bitcoin@05e49b342f), gates the address on fLogIPs.
- discourage(address: NetworkAddressV2) None[source]¶
Stop manage_connections from redialling this endpoint.
The caller is one of the conn.stop() sites that stops a connection this node dialled or accepted for cause – an incompatible peer or one that broke the protocol, never a connection this node closed on its own account. address is conn.address, keyed the same way already_connected below already compares live connections against a draw.
- is_self_connect_nonce(nonce: int) bool[source]¶
Whether nonce is a live, unhandshaken outbound connection’s own.
The only caller is callbacks.version. Matches Core’s own live, per-connection search – CConnman::CheckIncomingNonce, net.cpp:360-376 at bitcoin/bitcoin@b91d983f66 – which walks every node still short of fSuccessfullyConnected and not IsInboundConn(), rather than a fixed-size ring: pending_outbound_nonces (__init__) reproduces that search by never holding an inbound connection’s own nonce to begin with (add_pending_outbound_nonce above), not by filtering one out of a wider set at lookup time.
That same walk also excludes a private-broadcast connection’s own nonce, one candidate among the ones it visits – the reason given there is a peer taking such a connection down must not be able to infer this node dropped it and learn its clearnet address from the disconnect. This tree has no private-broadcast connection, so nothing here excludes on that account, and nothing here depends on that exclusion existing either.
Separately, net_processing.cpp:3886 only calls that walk at all for a version arriving on an inbound connection – a second restriction, on when the search runs rather than on what it searches, and not the one the paragraph above is about. Not reproduced here: the set already holds only outbound-origin nonces, so an ordinary peer’s own draw is never found in it regardless of which side received the version, and asking unconditionally costs nothing extra.
- async manage_connections() None[source]¶
Prune, prune some more, maybe dial, sleep – forever, every 0.1s.
_prune_stale_connections pings or drops an idle peer every pass; _maybe_prune_active_addresses runs far less often; _maybe_dial_more_peers dials one more only if this node still has room for it; _maybe_redial_specified is the standing redial issue #651 asked for, for -connect/-addnode alone.
- promote_connection(connection_id: int) None[source]¶
Move a connection out of the handshake and into the herd.
The only caller is callbacks.verack, right after it sets P2pConnStatus.Connected – the two are one step, kept as two calls only because the status belongs to the connection and the dict it lives in belongs to the manager. _connections_lock (__init__) is what makes the pop and the write one step too, against remove_connection’s own two pops below, on the other thread.
Successfully connected is exactly the state pending_outbound_nonces (__init__) has to stop answering for, so this connection’s own nonce leaves it here too, inside the same locked block – discard rather than a guarded pop, since an inbound connection’s nonce, never added there, is just as harmless to ask it to remove; the is not None guard is only for discard’s own typing, set[int] rather than set[int | None].
- remove_connection(connection_id: int) None[source]¶
Drop connection_id from either table and stop it, if it was held.
_connections_lock (__init__) is what makes the two pops one step, against promote_connection’s own pop-then-write. The same connection leaving pending_connections this way is one pending_outbound_nonces (__init__) has to stop answering for too, so its own nonce is discarded inside the same locked block, the same reason promote_connection above does it there rather than after. conn.stop() stays outside the lock, as every other call into Connection from in here does.
- run() None[source]¶
Method representing the thread’s activity.
You may override this method in a subclass. The standard run() method invokes the callable object passed to the object’s constructor as the target argument, if any, with sequential and keyword arguments taken from the args and kwargs arguments, respectively.
- send(msg: Payload, connection_id: int) None[source]¶
Send msg on connection_id, a no-op if that connection is gone.
- async server(loop: AbstractEventLoop, server_socket: socket) None[source]¶
Accept connections off server_socket, one create_connection each.
Reads through accepted, an asyncio.Queue a task of its own, _accept_loop, fills – rather than a bare await loop.sock_accept(server_socket) right here, which does not have the property the comment below argues for.
Module contents¶
The peer-to-peer protocol this node speaks.
Connections, the peer manager, the address book and the message handlers Node’s loop calls. manager.P2pManager is the thread; connection.Connection is one socket on it; callbacks.callbacks and callbacks.handshake_callbacks are the dispatch tables main.handle_p2p and main.handle_p2p_handshake read; address.PeerDB is the address book gossip and DNS both write to.