Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
751dl_start_size = self.node.debug_log_size(encoding="utf-8")753# Non-blocking send: When the server stops reading from the buffer,754# Python will raise an error. That stop could be due to actual755# backpressure from the server or any other throttle in the TCP chain.756# If the socket was set to blocking, we would have to wait for a timeout.757conn.conn.sock.setblocking(False)759# Use a background thread to push flood data until either an absurd760# limit has been reached (server is not throttling) or the foreground761# thread has determined that the server is throttling correctly.762# We can't rely on TCP backpressure at the client socket to assert763# server behavior because different platforms (macos in particular)764# like to open and close the TCP window size based on heuristics765# out of our control, and delay transmission when windows get too small.766flood_stop = threading.Event()768def send_flood(self, conn):769sent_total = 0770blocked = False771while not flood_stop.is_set():772try:773sent = conn.conn.sock.send(flood)774sent_total += sent775blocked = False776self.log.debug(f"Client sent {sent} bytes (total {sent_total})")777except BlockingIOError:778if not blocked:779blocked = True780self.log.debug(f"Client socket blocked after sending {sent_total} bytes")781if sent_total > len(flood) * 10:782raise Exception(f"Client sent too much data: {sent_total} bytes")783flood_stop.wait(0.05)785executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)786flood_thread = executor.submit(787send_flood,788self,789conn)791# Now watch the server-side read counter for this connection. While a792# request is in flight the throttled server should stop reading.793# Expect a 5-second stall sometime within the timeout factor. That gives794# the flood thread plenty of time to trigger the throttle.795prev_total = -1796total = 0797buffered = 0799def progress_stalled():800nonlocal buffered, total, prev_total802# Check the background thread for assertion errors803if flood_thread.done():804flood_thread.result()806with open(self.node.debug_log_path, encoding="utf-8", errors="replace") as dl:807# Skip everything written before the flood started.808dl.seek(dl_start_size)809log = dl.read()810matches = re.findall(811rf"Received \d+ bytes from [^\s]*:{local_port} \(id=\d+\): total=(\d+) buffered=(\d+)",812log)813total = max((int(m[0]) for m in matches), default=total)814buffered = max((int(m[1]) for m in matches), default=buffered)815if total == prev_total and total > 0:816return True817prev_total = total818self.log.debug(f"Server read {total} bytes, {buffered} buffered")819return False821try:822self.wait_until(progress_stalled, check_interval=5)823finally:824# Stop the flooding thread.825# Wait for it without a timeout so it can't turn an826# already-passed verdict into a spurious TimeoutError. If the flood827# thread hit its own assertion (server never throttled), result()828# re-raises it here.829flood_stop.set()830flood_thread.result()831executor.shutdown(wait=True)833self.log.info(834f"Pipelined flood: server received {total} bytes, buffered {buffered} bytes "835"before the read throttle engaged.")837# Unblock the client request queue.838conn.conn.sock.settimeout(10)839generated_block = self.generate(self.node, 1, sync_fun=self.no_op)[0]840# First reply is for the blocking request.841response = conn.recv_raw().decode()842assert generated_block in response845def check_slow_read_throttle(self):846self.log.info("Check that request processing is throttled if the client is not draining the socket")847self.restart_node(0, extra_args=["-rest=1"])848# Generate a big block849self.wallet = MiniWallet(self.node)850mine_large_block(self, self.wallet, self.node)851big_block_hash = self.node.getbestblockhash()853conn = BitcoinHTTPConnection(self.node)855# Request the big block JSON once to check its size and establish the connection856URI = f"/rest/block/{big_block_hash}.json"857response_body_size = len(conn.get(URI).read())858assert response_body_size > 7 * 1024 * 1024, f"Big block JSON response size is {response_body_size} bytes"860# Prepare a batch of requests. We want to fill up at861# least one server-side read operation (about 65kB, see HTTPRemoteClient::Receive()).862single_req = f"GET {URI} HTTP/1.1\r\nHost: somehost\r\n\r\n"863num_req = 0x10000 // len(single_req)864batch = single_req * num_req865self.log.info(f"Sending {num_req} big block JSON requests")867# Save a debug log checkpoint868dl_start_size = self.node.debug_log_size(encoding="utf-8")870# Send the batch but do not read any of the responses, leaving871# that data on the server-side of the socket.872conn.send_raw(batch.encode("ascii"))874# Open the debug log and count how many of the batch requests were processed.875# Expect progress to stall after a few seconds.876count = 0877prev_count = -1878min_count = MAX_BODY_SIZE // response_body_size880def progress_stalled():881nonlocal count, prev_count882with open(self.node.debug_log_path, encoding="utf-8", errors="replace") as dl:883dl.seek(dl_start_size)884count = dl.read().count(f"Received a GET request for {URI} ")885if count == prev_count and count > min_count:886self.log.info(f"Response progress stalled after {count} requests were handled.")887assert count < num_req, f"Server handled the whole batch of {num_req}: nothing was throttled"888return True889prev_count = count890return False892# Large enough interval, to ensure throttling occurred, rather than893# merely slow JSON serialization.894self.wait_until(progress_stalled, check_interval=5)896# Drain the responses that were handled up to the stall point,897# plus a few more to confirm that pulling out the cork restores the flow.898conn.conn.sock.settimeout(10)899num_res = 0900while num_res < count + 2:901response = conn.conn.sock.recv(10 * 1024 * 1024)902num_res += response.count(b"HTTP/1.1 200")903self.log.info(f"Drained {num_res} responses")905if __name__ == '__main__':906HTTPBasicsTest(__file__).main()
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.