btclib-node 2026.8.27__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- btclib_node/__init__.py +555 -0
- btclib_node/block_db/__init__.py +385 -0
- btclib_node/chains.py +269 -0
- btclib_node/chainstate/__init__.py +59 -0
- btclib_node/chainstate/block_index.py +722 -0
- btclib_node/chainstate/contextual.py +190 -0
- btclib_node/chainstate/filter_index.py +199 -0
- btclib_node/chainstate/utxo_index.py +142 -0
- btclib_node/config.py +147 -0
- btclib_node/constants.py +40 -0
- btclib_node/db.py +190 -0
- btclib_node/download.py +695 -0
- btclib_node/exceptions.py +219 -0
- btclib_node/interpreter.py +164 -0
- btclib_node/log.py +48 -0
- btclib_node/main.py +387 -0
- btclib_node/mempool.py +574 -0
- btclib_node/p2p/__init__.py +15 -0
- btclib_node/p2p/address.py +599 -0
- btclib_node/p2p/callbacks.py +1178 -0
- btclib_node/p2p/connection.py +870 -0
- btclib_node/p2p/filter_size.py +41 -0
- btclib_node/p2p/main.py +242 -0
- btclib_node/p2p/manager.py +833 -0
- btclib_node/p2p/messages/__init__.py +18 -0
- btclib_node/p2p/messages/errors.py +143 -0
- btclib_node/py.typed +0 -0
- btclib_node/rpc/__init__.py +15 -0
- btclib_node/rpc/callbacks.py +837 -0
- btclib_node/rpc/connection.py +382 -0
- btclib_node/rpc/errors.py +166 -0
- btclib_node/rpc/main.py +121 -0
- btclib_node/rpc/manager.py +347 -0
- btclib_node-2026.8.27.dist-info/METADATA +114 -0
- btclib_node-2026.8.27.dist-info/RECORD +38 -0
- btclib_node-2026.8.27.dist-info/WHEEL +4 -0
- btclib_node-2026.8.27.dist-info/licenses/AUTHORS.md +5 -0
- btclib_node-2026.8.27.dist-info/licenses/LICENSE +21 -0
btclib_node/__init__.py
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
# Copyright (c) The btclib developers
|
|
2
|
+
# Distributed under the MIT software license, see the accompanying
|
|
3
|
+
# LICENSE file or https://opensource.org/license/mit for the full text.
|
|
4
|
+
|
|
5
|
+
"""`Node`, the thread that drives everything else in this package.
|
|
6
|
+
|
|
7
|
+
One loop: drain the handshake queue, then a share of the RPC queue and
|
|
8
|
+
a share of the peer-to-peer queue, then step the download manager and
|
|
9
|
+
extend the chain. A message that raises is logged and the loop
|
|
10
|
+
continues; a failure under `update_chain` leaves the loop, because the
|
|
11
|
+
databases the submodules below open have to be closed on the way out.
|
|
12
|
+
|
|
13
|
+
`P2pManager` and `RpcManager` are each a thread of their own, running
|
|
14
|
+
an asyncio loop of their own; this module is what calls into them and
|
|
15
|
+
what they hand work back to.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import signal
|
|
20
|
+
import sys
|
|
21
|
+
import threading
|
|
22
|
+
import time
|
|
23
|
+
from math import log2
|
|
24
|
+
from multiprocessing.pool import Pool, ThreadPool
|
|
25
|
+
from typing import TYPE_CHECKING, override
|
|
26
|
+
|
|
27
|
+
from btclib_node.block_db import BlockDB
|
|
28
|
+
from btclib_node.chainstate import Chainstate
|
|
29
|
+
from btclib_node.config import Config
|
|
30
|
+
from btclib_node.constants import NodeStatus
|
|
31
|
+
from btclib_node.download import DownloadManager
|
|
32
|
+
from btclib_node.exceptions import NodeShutdownTimeoutError
|
|
33
|
+
from btclib_node.interpreter import warm
|
|
34
|
+
from btclib_node.log import Logger
|
|
35
|
+
from btclib_node.main import update_chain
|
|
36
|
+
from btclib_node.mempool import Mempool
|
|
37
|
+
from btclib_node.p2p.address import PeerDB
|
|
38
|
+
from btclib_node.p2p.main import (
|
|
39
|
+
handle_p2p,
|
|
40
|
+
handle_p2p_handshake,
|
|
41
|
+
resume_cfilters,
|
|
42
|
+
resume_getdata,
|
|
43
|
+
)
|
|
44
|
+
from btclib_node.p2p.manager import P2pManager
|
|
45
|
+
from btclib_node.rpc.main import handle_rpc
|
|
46
|
+
from btclib_node.rpc.manager import RpcManager
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from collections import deque
|
|
50
|
+
from types import FrameType
|
|
51
|
+
|
|
52
|
+
from btclib.p2p.inventory import Inventory
|
|
53
|
+
|
|
54
|
+
from btclib_node.p2p.connection import Connection
|
|
55
|
+
|
|
56
|
+
# Everything above this line is imported for `Node` to build on, not to
|
|
57
|
+
# be handed to a caller: `handle_p2p`, `RpcManager` and the rest are
|
|
58
|
+
# named here so this module can wire them together, and nothing outside
|
|
59
|
+
# this repository has ever imported one of them from the package root
|
|
60
|
+
# rather than from its own module. `Node` and `install_signal_handlers`
|
|
61
|
+
# below are the two names a caller reaches for -- `scripts/chains/`
|
|
62
|
+
# reaches for both, and every functional test builds a `Node` without
|
|
63
|
+
# ever calling the other -- so together they are the whole of `__all__`
|
|
64
|
+
# (btclib-org/.github#239).
|
|
65
|
+
__all__ = ["Node", "install_signal_handlers"]
|
|
66
|
+
|
|
67
|
+
# How long `stop` waits for the loop to come back before saying it did
|
|
68
|
+
# not. The flag is read at the top of the loop, so the wait is however
|
|
69
|
+
# long the pass already running takes: `update_chain` validates a fork
|
|
70
|
+
# block by block, and checks this same flag between blocks, so what sets
|
|
71
|
+
# the scale is one block's own `worker_pool.starmap` over its inputs --
|
|
72
|
+
# thousands of signature checks on mainnet -- rather than the whole fork
|
|
73
|
+
# a deep reorg walks. An idle stop costs milliseconds. Well under the
|
|
74
|
+
# per-test limit `pyproject.toml` sets, so that a node which will not
|
|
75
|
+
# stop is reported here rather than by whichever bound expires first;
|
|
76
|
+
# `tests/unit/init_test.py` asserts that ordering rather than leaving it
|
|
77
|
+
# to this comment.
|
|
78
|
+
STOP_TIMEOUT = 30
|
|
79
|
+
|
|
80
|
+
# How long the loop below sleeps once a pass finds nothing waiting in
|
|
81
|
+
# either queue. The figure it replaces, 0.0001, sat below the
|
|
82
|
+
# platform timer's own granularity, so what set the pace of an idle
|
|
83
|
+
# node was the OS rather than the number, and every one of those passes
|
|
84
|
+
# still ran `download_manager.step()` and `update_chain()` in full
|
|
85
|
+
# (btclib-org/btclib-node#440).
|
|
86
|
+
#
|
|
87
|
+
# Core's own message loop takes this shape as a wait on a condition
|
|
88
|
+
# variable that a producer signals, so a node with work is woken at
|
|
89
|
+
# once and one without costs nothing until the wait's own ceiling
|
|
90
|
+
# (`CConnman::ThreadMessageHandler`, `src/net.cpp`, up to 100 ms,
|
|
91
|
+
# at bitcoin/bitcoin@b91d983f66). A plain sleep cannot do that -- nothing
|
|
92
|
+
# wakes it early -- so raising it trades idle CPU for latency on
|
|
93
|
+
# whatever arrives while it is asleep, in a way Core's own wait does
|
|
94
|
+
# not have to. Giving `step()` its own cadence or signalling the loop
|
|
95
|
+
# from the queues would close that gap; neither is done here, so the
|
|
96
|
+
# gap stays and this is a divergence from what is cited above rather
|
|
97
|
+
# than a match to it.
|
|
98
|
+
#
|
|
99
|
+
# 5 ms rather than a larger figure in the 5-20 ms range the issue
|
|
100
|
+
# above named: an idle node's own CPU cost falls to about the same
|
|
101
|
+
# small fraction of its former self at 5, 10 and 20 ms alike, measured
|
|
102
|
+
# by comparing resource.getrusage across an idle run at each -- so
|
|
103
|
+
# nothing past 5 ms buys back more of it. What does move past 5 ms is
|
|
104
|
+
# the very latency the comment above says this sleep trades for that
|
|
105
|
+
# CPU, measured end to end on a real RPC round trip: it grows with the
|
|
106
|
+
# sleep, so the smallest figure that already captures the CPU win is
|
|
107
|
+
# the one that pays least for it, and it still leaves comfortable
|
|
108
|
+
# headroom under `tests/__init__.py`'s own 25 ms poll in `wait_until`
|
|
109
|
+
# and `wait_until_listening`, so nothing in the suite can resolve it.
|
|
110
|
+
IDLE_SLEEP_SECONDS = 0.005
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _default_worker_count() -> int:
|
|
114
|
+
"""How many workers `Node.worker_pool` spawns.
|
|
115
|
+
|
|
116
|
+
Eight outside of a test run, unconditionally. Under `pytest-xdist`,
|
|
117
|
+
`PYTEST_XDIST_WORKER_COUNT` is the number of worker processes the
|
|
118
|
+
run was split across (`xdist/remote.py` sets it in the worker's own
|
|
119
|
+
environment before any test module is imported), and every one of
|
|
120
|
+
them builds its own `Node`s and, through them, its own pools: eight
|
|
121
|
+
workers each, on top of `-n auto`'s one xdist worker per core, is
|
|
122
|
+
what starves a `wait_until` on a machine that has the cores for the
|
|
123
|
+
xdist workers alone and not for eight more of `Node.worker_pool`'s
|
|
124
|
+
own per xdist worker on top of that (btclib-org/btclib-node#46).
|
|
125
|
+
Dividing the machine's own core count across the xdist workers
|
|
126
|
+
instead keeps the total the run spawns near that core count,
|
|
127
|
+
whatever `-n` is set to, and leaves a node running outside of pytest
|
|
128
|
+
at the flat eight.
|
|
129
|
+
|
|
130
|
+
The reasoning is the same whether `_pool_factory` below hands
|
|
131
|
+
`worker_pool` a process pool or a thread pool: an xdist worker is
|
|
132
|
+
always an OS process, and what starves it is the machine's core
|
|
133
|
+
count being oversubscribed by whatever `Node.worker_pool` spawns
|
|
134
|
+
under it, a real OS thread competing for a core exactly as a real OS
|
|
135
|
+
process does once the interpreter that spawns it is free-threaded
|
|
136
|
+
(btclib-org/btclib-node#388).
|
|
137
|
+
"""
|
|
138
|
+
xdist_workers = os.environ.get("PYTEST_XDIST_WORKER_COUNT")
|
|
139
|
+
if xdist_workers is None:
|
|
140
|
+
return 8
|
|
141
|
+
return max(1, (os.cpu_count() or 8) // int(xdist_workers))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# `Node.worker_pool`'s own size, named so `warm_worker_pool` can compute
|
|
145
|
+
# a warm-up call count from it without a second literal to drift from.
|
|
146
|
+
_WORKER_COUNT = _default_worker_count()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _pool_factory(*, gil_enabled: bool) -> type[Pool]:
|
|
150
|
+
"""Return the pool type `Node.worker_pool` builds, chosen by `gil_enabled`.
|
|
151
|
+
|
|
152
|
+
`Pool` under a GIL build, `ThreadPool` under a free-threaded one --
|
|
153
|
+
`sys._is_gil_enabled()` is `worker_pool`'s own caller for
|
|
154
|
+
`gil_enabled`, kept out of this function so that both arms are
|
|
155
|
+
reachable, and asserted, on a single interpreter (issue #388): a
|
|
156
|
+
`ThreadPool` constructs on a GIL build as readily as a `Pool` does,
|
|
157
|
+
so a parametrized test exercises both without a second interpreter.
|
|
158
|
+
|
|
159
|
+
Two conditions license a `ThreadPool` at all, and both were
|
|
160
|
+
established by reading and by running rather than by trust of a
|
|
161
|
+
process-era comment (issue #388): `btclib.script.engine` never
|
|
162
|
+
writes to the `Tx`, `TxIn` or `PrecomputedTxData` a task is handed
|
|
163
|
+
-- confirmed by a concurrent run over a real, mixed-flavour,
|
|
164
|
+
signed transaction with `Tx.__setattr__`/`TxIn.__setattr__`
|
|
165
|
+
instrumented to catch one, on both interpreters -- and
|
|
166
|
+
`btclib-secp256k1` verifies through one shared, read-only
|
|
167
|
+
libsecp256k1 context that its own "Thread safety" section documents
|
|
168
|
+
as safe for concurrent calls. Under a GIL build the choice does not
|
|
169
|
+
matter for correctness and matters for speed: the libsecp256k1 cffi
|
|
170
|
+
call does not release the GIL, so a `ThreadPool` there is threads
|
|
171
|
+
taking turns behind a process pool's own real parallelism, which is
|
|
172
|
+
why the GIL build keeps `Pool`.
|
|
173
|
+
|
|
174
|
+
Core's own `CCheckQueue` always shares one
|
|
175
|
+
`PrecomputedTransactionData` by pointer across its worker threads
|
|
176
|
+
(`validation.h`'s `CScriptCheck::txdata`,
|
|
177
|
+
at bitcoin/bitcoin@794a753958); a `ThreadPool` is this tree's way of
|
|
178
|
+
doing the same once the interpreter running it makes a real thread
|
|
179
|
+
as parallel as one of Core's.
|
|
180
|
+
"""
|
|
181
|
+
return Pool if gil_enabled else ThreadPool
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Node(threading.Thread):
|
|
185
|
+
"""A bitcoin full node, and the thread that runs its main loop.
|
|
186
|
+
|
|
187
|
+
`config` (or a default `Config` when none is given) is what says
|
|
188
|
+
which chain, which data directory and which ports; `__init__` opens
|
|
189
|
+
every database under that directory and wires the p2p and RPC
|
|
190
|
+
managers to this node before `start()` ever runs `run`'s loop.
|
|
191
|
+
|
|
192
|
+
Building one touches no process-wide state: `install_signal_handlers`
|
|
193
|
+
below is the separate, explicit call a caller makes for that, and
|
|
194
|
+
this object never makes it on its own behalf (issue #436).
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(self, config: Config | None = None) -> None:
|
|
198
|
+
"""Open every database `config` names, and wire the two managers up."""
|
|
199
|
+
super().__init__()
|
|
200
|
+
|
|
201
|
+
if config is None:
|
|
202
|
+
config = Config()
|
|
203
|
+
|
|
204
|
+
self.config = config
|
|
205
|
+
self.chain = config.chain
|
|
206
|
+
self.data_dir = config.data_dir
|
|
207
|
+
self.data_dir.mkdir(exist_ok=True, parents=True)
|
|
208
|
+
|
|
209
|
+
self.terminate_flag = threading.Event()
|
|
210
|
+
log_path = self.data_dir / config.log_path if config.log_path else None
|
|
211
|
+
self.logger = Logger(log_path, debug=config.debug)
|
|
212
|
+
|
|
213
|
+
self.chainstate = Chainstate(self.data_dir, self.chain, self.logger)
|
|
214
|
+
self.block_db = BlockDB(self.data_dir, self.logger)
|
|
215
|
+
# the two halves of a filter live in different databases -- the
|
|
216
|
+
# block and its reverse patch in one, the index in the other --
|
|
217
|
+
# so catching up is here, where both are built, and before
|
|
218
|
+
# anything is listening: the version message this node sends
|
|
219
|
+
# says it serves filters for the whole chain
|
|
220
|
+
self.chainstate.filter_index.catch_up(
|
|
221
|
+
self.chainstate.block_index.active_chain, self.block_db
|
|
222
|
+
)
|
|
223
|
+
self.mempool = Mempool(self.logger)
|
|
224
|
+
|
|
225
|
+
# A `getcfilters` answer `p2p.callbacks.get_cfilters` could not
|
|
226
|
+
# finish scheduling under its own pacing bound, keyed by
|
|
227
|
+
# connection id: the connection itself and the heights still
|
|
228
|
+
# owed. `p2p.callbacks.advance_cfilters` and
|
|
229
|
+
# `p2p.main.resume_cfilters` are the only two that read or write
|
|
230
|
+
# this, and both run on this thread -- `run`'s own loop below,
|
|
231
|
+
# under `handle_p2p` or under `resume_cfilters` directly -- so
|
|
232
|
+
# nothing here needs a lock. btclib-org/btclib-node#442
|
|
233
|
+
self.pending_cfilters: dict[int, tuple[Connection, deque[int]]] = {}
|
|
234
|
+
|
|
235
|
+
# The same shape as `pending_cfilters` above, for a `getdata`
|
|
236
|
+
# `p2p.callbacks.getdata` could not finish serving: the
|
|
237
|
+
# connection and the items still owed, read and written only by
|
|
238
|
+
# `p2p.callbacks.advance_getdata` and `p2p.main.resume_getdata`,
|
|
239
|
+
# both on this thread. btclib-org/btclib-node#470
|
|
240
|
+
self.pending_getdata: dict[int, tuple[Connection, deque[Inventory]]] = {}
|
|
241
|
+
|
|
242
|
+
# Built on first use, by the property below: the pool is
|
|
243
|
+
# interpreters under a GIL build (spawned rather than forked
|
|
244
|
+
# wherever that is the platform's default) or threads under a
|
|
245
|
+
# free-threaded one (issue #388), and a node that never
|
|
246
|
+
# validates a script should not pay for either. Which is most
|
|
247
|
+
# of them -- a node serving headers, a node under test, a node
|
|
248
|
+
# that has nothing to connect -- and each one that does pay
|
|
249
|
+
# competes for the cores with the nodes that are actually
|
|
250
|
+
# validating.
|
|
251
|
+
self._worker_pool: Pool | None = None
|
|
252
|
+
self._worker_pool_lock = threading.Lock()
|
|
253
|
+
# Set by `warm_worker_pool` and joined before `run`'s own
|
|
254
|
+
# teardown reads `_worker_pool` below: without that join, a
|
|
255
|
+
# warm-up still building the pool when the loop stops would
|
|
256
|
+
# race the `is not None` check there, and the pool it goes on
|
|
257
|
+
# to build would never be handed to `terminate`.
|
|
258
|
+
self._worker_pool_warmup: threading.Thread | None = None
|
|
259
|
+
|
|
260
|
+
self.status = NodeStatus.Starting
|
|
261
|
+
|
|
262
|
+
self.download_manager = DownloadManager(self, self.logger)
|
|
263
|
+
|
|
264
|
+
self.p2p_port: int | None
|
|
265
|
+
if config.p2p_port:
|
|
266
|
+
self.p2p_port = config.p2p_port
|
|
267
|
+
else:
|
|
268
|
+
self.p2p_port = None
|
|
269
|
+
peer_db = PeerDB(self.chain, self.data_dir)
|
|
270
|
+
self.p2p_manager = P2pManager(self, self.p2p_port, peer_db)
|
|
271
|
+
|
|
272
|
+
self.rpc_port: int | None
|
|
273
|
+
if config.rpc_port:
|
|
274
|
+
self.rpc_port = config.rpc_port
|
|
275
|
+
else:
|
|
276
|
+
self.rpc_port = None
|
|
277
|
+
self.rpc_manager = RpcManager(self, self.rpc_port)
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def worker_pool(self) -> Pool:
|
|
281
|
+
"""The pool `interpreter.py` validates a script in, built on first use.
|
|
282
|
+
|
|
283
|
+
`_pool_factory` picks the type against `sys._is_gil_enabled()`
|
|
284
|
+
read here, once, rather than inside that function: a `Pool`
|
|
285
|
+
under a GIL build, a `ThreadPool` under a free-threaded one
|
|
286
|
+
(issue #388). Under the lock, so that two callers building it at
|
|
287
|
+
once get one pool between them: the second would otherwise leave
|
|
288
|
+
a pool with nothing holding it and nothing to terminate it.
|
|
289
|
+
"""
|
|
290
|
+
with self._worker_pool_lock:
|
|
291
|
+
if self._worker_pool is None:
|
|
292
|
+
# sys._is_gil_enabled is the interpreter's own name for
|
|
293
|
+
# the question; CPython gives it no public spelling
|
|
294
|
+
self._worker_pool = _pool_factory(
|
|
295
|
+
gil_enabled=sys._is_gil_enabled() # noqa: SLF001
|
|
296
|
+
)(processes=_WORKER_COUNT)
|
|
297
|
+
return self._worker_pool
|
|
298
|
+
|
|
299
|
+
def _close_worker_pool(self) -> None:
|
|
300
|
+
# read, not asked for: the property would build the pool this
|
|
301
|
+
# exists to take down. `terminate()` alone left `_worker_pool`
|
|
302
|
+
# referencing a pool whose worker processes and handler threads
|
|
303
|
+
# it had not waited for; `join()` waits for them here, and
|
|
304
|
+
# dropping the reference is what stops whatever collects the
|
|
305
|
+
# `Node` afterwards from finding one still to take down.
|
|
306
|
+
if self._worker_pool is not None:
|
|
307
|
+
self._worker_pool.terminate()
|
|
308
|
+
self._worker_pool.join()
|
|
309
|
+
self._worker_pool = None
|
|
310
|
+
|
|
311
|
+
def __del__(self) -> None:
|
|
312
|
+
"""Terminate the worker pool if `run` never got to, as a backstop.
|
|
313
|
+
|
|
314
|
+
For a `Node` built and used without ever being `start()`ed --
|
|
315
|
+
`tests/unit/main_test.py` calls `update_chain` against one
|
|
316
|
+
directly, on the thread that built it, to reach a block's own
|
|
317
|
+
validation without a loop around it, and that builds a real
|
|
318
|
+
`worker_pool` that `run`'s own teardown below never runs to take
|
|
319
|
+
back down. Without this, that pool is still in `Pool.RUN` state
|
|
320
|
+
whenever something finally collects it, which is what made
|
|
321
|
+
`Pool.__del__` warn and reach for a queue of its own on an xdist
|
|
322
|
+
worker's stderr, reported against btclib-org/btclib-node#195.
|
|
323
|
+
`getattr` rather than the attribute itself: `__init__` can raise
|
|
324
|
+
before `_worker_pool` is set, and a constructor's exception
|
|
325
|
+
should not come back paired with a second one out of here.
|
|
326
|
+
"""
|
|
327
|
+
if getattr(self, "_worker_pool", None) is not None:
|
|
328
|
+
self._close_worker_pool()
|
|
329
|
+
|
|
330
|
+
def warm_worker_pool(self) -> None:
|
|
331
|
+
"""Build the worker pool now, on a thread of its own, and warm it.
|
|
332
|
+
|
|
333
|
+
`check_transactions`' own first call used to be what built and
|
|
334
|
+
warmed `worker_pool`, on whatever thread called it -- `run`'s
|
|
335
|
+
own loop below, the same one that drains
|
|
336
|
+
`p2p_manager.handshake_messages` and promotes a connection once
|
|
337
|
+
its `verack` arrives. Under `_pool_factory`'s process arm, each
|
|
338
|
+
of the pool's own processes pays its own import of
|
|
339
|
+
`btclib_node.interpreter` (and, through it,
|
|
340
|
+
`btclib.script.engine`) the first time it is dispatched a task,
|
|
341
|
+
and while that first dispatch is running, the loop below cannot
|
|
342
|
+
drain that queue: a peer whose `verack` the kernel already
|
|
343
|
+
delivered sits unpromoted until the call returns
|
|
344
|
+
(btclib-org/btclib-node#262). Under the thread arm every worker
|
|
345
|
+
already shares the one import this process paid when `Node`
|
|
346
|
+
itself was imported, so the same dispatch below costs this
|
|
347
|
+
module nothing there -- and is still made, both arms being one
|
|
348
|
+
call site and the warm-up being harmless where it is not needed.
|
|
349
|
+
|
|
350
|
+
`download_manager.block_download` is the only caller, right
|
|
351
|
+
before it sends the first real `GetData` for a block this node
|
|
352
|
+
does not have -- the earliest point a script is actually going
|
|
353
|
+
to be validated, with a peer's round trip ahead of it as extra
|
|
354
|
+
runway, rather than the moment header sync merely completes.
|
|
355
|
+
Reaching `HeaderSynced` is not enough on its own: the comment on
|
|
356
|
+
`_worker_pool` above is that a node which never validates a
|
|
357
|
+
script should not pay for the pool, and a node whose headers are
|
|
358
|
+
synced but which never has a block to fetch -- a header-only
|
|
359
|
+
peer under test, a peer whose counterpart stops serving blocks
|
|
360
|
+
-- is exactly that. The guard below makes a second call a no-op,
|
|
361
|
+
since `block_download` runs on every pass of the loop below and
|
|
362
|
+
would otherwise ask for a second thread once the first has
|
|
363
|
+
already built the pool.
|
|
364
|
+
"""
|
|
365
|
+
if self._worker_pool_warmup is not None:
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
def build_and_warm() -> None:
|
|
369
|
+
self.worker_pool.starmap(warm, [()] * (_WORKER_COUNT * 4))
|
|
370
|
+
|
|
371
|
+
self._worker_pool_warmup = threading.Thread(target=build_and_warm, daemon=True)
|
|
372
|
+
self._worker_pool_warmup.start()
|
|
373
|
+
|
|
374
|
+
def _drain_message_queues(self) -> bool:
|
|
375
|
+
"""Handle whatever is waiting, and answer whether nothing was.
|
|
376
|
+
|
|
377
|
+
One message must not end the node. `handle_p2p` and
|
|
378
|
+
`handle_p2p_handshake` already answer a bad message by dropping
|
|
379
|
+
the peer, but what reaches here is whatever they did not expect
|
|
380
|
+
-- and leaving `run`'s own loop by exception skips every close
|
|
381
|
+
below it, so the databases would stay open.
|
|
382
|
+
|
|
383
|
+
`resume_cfilters` and `resume_getdata` are last and unconditional,
|
|
384
|
+
not one more queue to size a share from: nothing is queued to
|
|
385
|
+
trigger either, a paused `getcfilters` or `getdata` answer being
|
|
386
|
+
owed regardless of what else this pass finds waiting.
|
|
387
|
+
|
|
388
|
+
A connection paused on `MAX_QUEUED_RECV_BYTES`
|
|
389
|
+
(`p2p/connection.py`) waits here and nowhere of its own: what
|
|
390
|
+
resumes it is `handle_p2p` popping enough of that connection's
|
|
391
|
+
own items off `p2p_manager.messages`, and that queue is shared by
|
|
392
|
+
every connection. The items another peer has ahead of it grow
|
|
393
|
+
with the number of peers currently busy, while the share popped
|
|
394
|
+
per pass grows only with the log of the whole backlog, so the
|
|
395
|
+
wait is a function of how many peers are busy rather than a
|
|
396
|
+
constant -- where Core's own `ThreadMessageHandler`
|
|
397
|
+
(`net.cpp:3216-3238`, at bitcoin/bitcoin@b91d983f66) calls
|
|
398
|
+
`ProcessMessages` once per peer every round and so bounds it by a
|
|
399
|
+
constant. The wait is still bounded rather than open-ended:
|
|
400
|
+
`parse_messages` appends in arrival order and each connection's
|
|
401
|
+
own contribution is capped near that same bound rather than by
|
|
402
|
+
it -- the weighing happens once per parse pass, after it, so a
|
|
403
|
+
connection already at the bound still adds whatever that pass
|
|
404
|
+
completed, up to `MAX_PROTOCOL_MESSAGE_LENGTH` for the single
|
|
405
|
+
message that crossed it -- so what can sit ahead of a paused
|
|
406
|
+
connection is the other connections' caps and whatever
|
|
407
|
+
`ping`/`pong` arrives after it.
|
|
408
|
+
`tests/unit/init_test.py` measures the wait in passes of this
|
|
409
|
+
loop against the number of busy connections.
|
|
410
|
+
btclib-org/btclib-node#490
|
|
411
|
+
"""
|
|
412
|
+
wait = True
|
|
413
|
+
try:
|
|
414
|
+
while len(self.p2p_manager.handshake_messages):
|
|
415
|
+
handle_p2p_handshake(self)
|
|
416
|
+
wait = False
|
|
417
|
+
for _ in range(int(log2(len(self.rpc_manager.messages) + 1))):
|
|
418
|
+
handle_rpc(self)
|
|
419
|
+
wait = False
|
|
420
|
+
for _ in range(int(log2(len(self.p2p_manager.messages) + 1))):
|
|
421
|
+
handle_p2p(self)
|
|
422
|
+
wait = False
|
|
423
|
+
if resume_cfilters(self):
|
|
424
|
+
wait = False
|
|
425
|
+
if resume_getdata(self):
|
|
426
|
+
wait = False
|
|
427
|
+
except Exception:
|
|
428
|
+
self.logger.exception("Exception occurred handling a message")
|
|
429
|
+
return wait
|
|
430
|
+
|
|
431
|
+
def _step_chain(self) -> bool:
|
|
432
|
+
"""Advance the chain one step, and answer whether `run` should stop."""
|
|
433
|
+
try:
|
|
434
|
+
self.download_manager.step()
|
|
435
|
+
update_chain(self)
|
|
436
|
+
except Exception:
|
|
437
|
+
self.logger.exception("Exception occurred")
|
|
438
|
+
return True
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
@override
|
|
442
|
+
def run(self) -> None:
|
|
443
|
+
self.logger.info("Starting main loop")
|
|
444
|
+
|
|
445
|
+
# Set before either manager starts, on this thread rather than
|
|
446
|
+
# a manager's own: `listening` is set on a manager's thread, so
|
|
447
|
+
# a caller whose wait ends there learns nothing about whether
|
|
448
|
+
# this assignment has run yet, and a write racing it here can
|
|
449
|
+
# put `status` back below `HeaderSynced` for good (#398).
|
|
450
|
+
self.status = NodeStatus.SyncingHeaders
|
|
451
|
+
if self.p2p_port:
|
|
452
|
+
self.p2p_manager.start()
|
|
453
|
+
if self.rpc_port:
|
|
454
|
+
self.rpc_manager.start()
|
|
455
|
+
while not self.terminate_flag.is_set():
|
|
456
|
+
if self._drain_message_queues():
|
|
457
|
+
time.sleep(IDLE_SLEEP_SECONDS)
|
|
458
|
+
if self._step_chain():
|
|
459
|
+
break
|
|
460
|
+
self.p2p_manager.stop()
|
|
461
|
+
self.rpc_manager.stop()
|
|
462
|
+
|
|
463
|
+
self.p2p_manager.peer_db.close()
|
|
464
|
+
self.chainstate.close()
|
|
465
|
+
self.block_db.close()
|
|
466
|
+
|
|
467
|
+
# joined before the read below, not asked for: the same race
|
|
468
|
+
# the attribute's own comment above names
|
|
469
|
+
if self._worker_pool_warmup is not None:
|
|
470
|
+
self._worker_pool_warmup.join()
|
|
471
|
+
self._close_worker_pool()
|
|
472
|
+
|
|
473
|
+
self.logger.info("Stopping node")
|
|
474
|
+
self.logger.close()
|
|
475
|
+
|
|
476
|
+
def stop(self) -> None:
|
|
477
|
+
"""Ask the main loop to stop, and wait up to `STOP_TIMEOUT` for it.
|
|
478
|
+
|
|
479
|
+
Raises if the loop has not come back by then, the node having
|
|
480
|
+
no way to be sure of its chainstate or its databases while a
|
|
481
|
+
thread is still inside them.
|
|
482
|
+
|
|
483
|
+
Signalling alone lets the caller go on while the node is still
|
|
484
|
+
there. A test that returns then is torn down around a thread
|
|
485
|
+
that goes on logging into a harness that has moved on, which is
|
|
486
|
+
where `ValueError: I/O operation on closed file` came from; and
|
|
487
|
+
a loop that cannot come back at all holds the interpreter open
|
|
488
|
+
after the last test, this being a non-daemon thread. No
|
|
489
|
+
per-test limit reaches that second one -- the test it belongs
|
|
490
|
+
to has already passed -- so waiting here is what puts the wait
|
|
491
|
+
inside the test, where a limit can name it
|
|
492
|
+
(btclib-org/btclib-node#98).
|
|
493
|
+
|
|
494
|
+
The bound is the point rather than a precaution:
|
|
495
|
+
`pytest-timeout` arms one timer per test, so a limit already
|
|
496
|
+
spent in the call phase is not there for the teardown, and an
|
|
497
|
+
unbounded wait in a teardown is a run that stops instead of
|
|
498
|
+
failing (btclib-org/btclib-node#115).
|
|
499
|
+
|
|
500
|
+
The node's own thread is the one caller that cannot wait, the
|
|
501
|
+
`stop` RPC being handled inside the loop it stops. It gets the
|
|
502
|
+
signal alone, and reaches the end of `run` by itself. A signal
|
|
503
|
+
handler is the other caller worth naming: this raising there
|
|
504
|
+
makes an operator's interrupt loud, and it does not make the
|
|
505
|
+
process able to exit, the wedged thread being non-daemon.
|
|
506
|
+
"""
|
|
507
|
+
self.terminate_flag.set()
|
|
508
|
+
if self.is_alive() and threading.current_thread() is not self:
|
|
509
|
+
self.join(timeout=STOP_TIMEOUT)
|
|
510
|
+
if self.is_alive():
|
|
511
|
+
# named by its data directory, which is what tells one
|
|
512
|
+
# node from another where several are running
|
|
513
|
+
err_msg = f"the node at {self.data_dir} did not stop: its "
|
|
514
|
+
err_msg += "thread is still running after the flag was set. "
|
|
515
|
+
err_msg += "Nothing after this can trust the chainstate or "
|
|
516
|
+
err_msg += "the databases"
|
|
517
|
+
raise NodeShutdownTimeoutError(err_msg)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def install_signal_handlers(node: Node) -> None:
|
|
521
|
+
"""Stop `node` on SIGINT, SIGTERM and SIGTSTP, process-wide.
|
|
522
|
+
|
|
523
|
+
`signal.signal` keeps one handler per signal per process, replacing
|
|
524
|
+
whatever was there before, so this is for the one caller in a
|
|
525
|
+
process that wants an operator's interrupt to reach a node at all --
|
|
526
|
+
`scripts/chains/`, and whatever a future CLI ends up being. Calling
|
|
527
|
+
it a second time, for a second node, replaces the first node's
|
|
528
|
+
handler rather than adding to it: that is the same `signal.signal`
|
|
529
|
+
the first call made, not a defect this function introduces.
|
|
530
|
+
|
|
531
|
+
Kept out of `Node.__init__` for two reasons (issue #436). A second
|
|
532
|
+
`Node` built in one process used to silently disown the first,
|
|
533
|
+
every call installing a fresh handler bound to the newer node with
|
|
534
|
+
nothing to say the first one's databases were still open behind it
|
|
535
|
+
-- every functional p2p test builds two nodes, so the first node's
|
|
536
|
+
handlers survived only for the length of the second one's
|
|
537
|
+
constructor call. And `signal.signal` raises outside the main
|
|
538
|
+
thread of the main interpreter, so a `Node` could not be built at
|
|
539
|
+
all from a worker thread, whether or not that caller ever wanted a
|
|
540
|
+
process-wide interrupt.
|
|
541
|
+
|
|
542
|
+
SIG_DFL/SIG_IGN are `signal.signal`'s own other two handler
|
|
543
|
+
arguments, POSIX's only alternatives to a callable one; every
|
|
544
|
+
handler is called with `(signum, frame)`, unread here since the
|
|
545
|
+
three signals below share one handler and `stop` takes neither.
|
|
546
|
+
SIGTSTP is for hibernation and does not exist on Windows
|
|
547
|
+
(btclib-org/btclib-node#429).
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
def stop_handler(_signum: int, _frame: FrameType | None) -> None:
|
|
551
|
+
node.stop()
|
|
552
|
+
|
|
553
|
+
signal.signal(signal.SIGINT, stop_handler)
|
|
554
|
+
signal.signal(signal.SIGTERM, stop_handler)
|
|
555
|
+
signal.signal(signal.SIGTSTP, stop_handler)
|