btclib_node.rpc package¶
Submodules¶
btclib_node.rpc.callbacks module¶
One handler per JSON-RPC method, and callbacks, the table dispatching them.
Every handler shares the signature (node, conn, params) that rpc.main.handle_rpc calls each one with, whether or not its own body reads every argument – the same shared-signature reasoning p2p.callbacks carries for its own two tables. README.md’s own limitation applies to every entry here: this table is served over a listener that authenticates nothing.
- btclib_node.rpc.callbacks.get_best_block_hash(node: Node, conn: RpcConnection, _: list[Any]) bytes[source]¶
Answer getbestblockhash with the active chain’s own tip.
- btclib_node.rpc.callbacks.get_block_count(node: Node, conn: RpcConnection, _: list[Any]) int[source]¶
Answer getblockcount with the active chain’s own height.
- btclib_node.rpc.callbacks.get_block_hash(node: Node, conn: RpcConnection, params: list[Any]) bytes[source]¶
Answer getblockhash, Core’s own checks on height in Core’s order.
A missing, wrongly typed, non-integral or out-of-range height is each refused the way RPCMethod::HandleRequest and src/rpc/blockchain.cpp:585-601 refuse it, cited beside each check below; a height in range answers active_chain[height].
- btclib_node.rpc.callbacks.get_block_header(node: Node, conn: RpcConnection, params: list[Any]) dict[str, Any] | str[source]¶
Answer getblockheader for params[0], verbose by Core’s own default.
Not verbose, answers the same eighty bytes a peer is sent on the wire, hex-encoded; verbose, answers the object blockheaderToJSON does, height and confirmations included for a header off the active chain as much as for one on it – each field’s own Core citation is beside where it is built, below.
nTx is the one member blockheaderToJSON answers that this does not (src/rpc/blockchain.cpp:185, at bitcoin/bitcoin@ca7162cde5): Core reads it off CBlockIndex::nTx, a count kept beside the header once the block is received; BlockInfo (chainstate/block_index.py) carries no such count, only header, index, status and downloaded, so answering it here would mean parsing the whole block body off block_db for every call – a header lookup paying a block’s own cost, and one that still has nothing to answer for a header whose block was never downloaded. Left absent rather than answered at that price.
- btclib_node.rpc.callbacks.get_blockchain_info(node: Node, conn: RpcConnection, _: list[Any]) dict[str, Any][source]¶
Answer getblockchaininfo with Core’s own members this node can answer.
chain: BitcoinCoreFetcher.assert_network (btclib) and BitcoinCoreRpcClient.assert_chain (bitcoin_core_rpc) call this once before their first fetch, by default, and read chain alone – proven by asking a real client of a real node here for get_best_block_id before this callback existed: the very first call failed -32601 Method not found on getblockchaininfo, not on the method it asked for. That is why chain could not be left out, not a reason the rest stayed absent.
blocks is active_chain’s own last index, matching get_block_count above: Core’s own “the height of the most-work fully-validated chain” (src/rpc/blockchain.cpp:1427, at bitcoin/bitcoin@ca7162cde5). headers is header_index’s own last index the same way – header_index is this node’s own best known header chain, tracked separately from active_chain (BlockIndex’s own class docstring) the way Core’s m_best_header is tracked separately from ActiveChain()’s own tip, and answered the same way Core answers it: chainman.m_best_header->nHeight (src/rpc/ blockchain.cpp:1428, at bitcoin/bitcoin@ca7162cde5). bestblockhash is active_chain’s own tip, matching get_best_block_hash above (src/rpc/blockchain.cpp:1429, at bitcoin/bitcoin@ca7162cde5) – both already the display byte order Core’s own GetHex() answers, BlockHeader.hash (btclib) being the reversed hash rather than the wire’s own, confirmed against a real bitcoind’s identical expression at tests/integration/bitcoind_test.py:66.
bits is the tip header’s own compact target, header.bits, hex (Core’s strprintf(“%08x”, tip.nBits), src/rpc/blockchain.cpp:1430, at bitcoin/bitcoin@ca7162cde5). target is header.target (btclib), 32 bytes already in the same big-endian order Core’s own GetTarget(…).GetHex() answers (src/rpc/blockchain.cpp:1431, same commit) – target_from_bits (btclib.block.proof_of_work) is Core’s SetCompact, and arith_uint256::GetHex writes each 32-bit limb little-endian into a base_blob and then reverses that whole blob (src/arith_uint256.cpp:141, src/uint256.cpp:11, same commit), which is a plain big-endian print of the magnitude and not the reversal a hash’s own GetHex answers. difficulty is header.target’s ratio against the genesis target, header.difficulty (btclib) – the same ratio Core’s own GetDifficulty computes by repeated *=//= 256.0 from the compact exponent (src/rpc/blockchain.cpp:106, same commit), verified bit for bit against that literal loop on regtest’s own genesis bits 0x207fffff in this callback’s own unit test.
time is the tip header’s own timestamp, contextual.block_time (Core’s CBlockHeader::GetBlockTime, src/rpc/blockchain.cpp:1433, same commit). mediantime is contextual.median_time_past of the tip, over main.parent_lookup’s own walk – the same call main.verify_mempool_acceptance already makes of the tip, for Core’s own CBlockIndex::GetMedianTimePast (src/rpc/blockchain.cpp :1434, same commit). chainwork is block_index.chainwork’s own entry for the tip, hex and zero-padded to 64 digits the way Core’s nChainWork.GetHex() prints a plain magnitude (src/rpc/ blockchain.cpp:1450, same commit) – get_block_header above answers its own chainwork the same way now, closing what used to be a divergence from Core between the two (btclib-org/btclib-node#658).
initialblockdownload is node.is_initial_block_download, main.update_ibd_status’s own latch, matching Core’s own IsInitialBlockDownload (src/rpc/blockchain.cpp:1436, at bitcoin/bitcoin@ca7162cde5) field for field: chain work against Chain.minimum_chain_work and tip age against MAX_TIP_AGE, not merely whether this node has run out of candidates to try. size_on_disk is block_db.BlockDB.current_usage, Core’s own CalculateCurrentUsage (src/rpc/blockchain.cpp:1451, same commit). pruned is Config.pruned (src/rpc/blockchain.cpp:1452, same commit); pruneheight, present only where pruned is true, is the first height block_db.BlockDB.prune_up_to has not deleted – pruned_up_to + 1, Core’s own “the first block unpruned, all previous blocks were pruned” (src/rpc/blockchain.cpp:1455, same commit, prune_height.value() + 1). automatic_pruning, present alongside it, is whether Config.prune_target_mib is set – Core’s own GetPruneTarget() != PRUNE_TARGET_MANUAL (src/rpc/blockchain.cpp:1457, same commit); prune_target_size, present only where that is true, is prune_target_mib in bytes, Core’s own unit for the member of the same name.
Absent, each for its own reason rather than by oversight: verificationprogress, Core’s own GuessVerificationProgress (src/validation.cpp:5519, at bitcoin/bitcoin@ca7162cde5) extrapolating from ChainTxData, an assumed transaction rate for the chain as a whole, against each block’s own accumulated transaction count (CBlockIndex::m_chain_tx_count) – chains.py carries neither the per-chain assumption nor a per-block count, so answering this member under Core’s own name would answer a number carrying none of Core’s meaning behind it, rather than a truthful one; warnings, this node raising none of its own; signet_challenge, SigNet here carrying no configurable challenge (chains.py’s own genesis is the one public signet); backgroundvalidation, present on Core’s own side only behind an assumeutxo snapshot this node has no counterpart to.
- btclib_node.rpc.callbacks.get_connection_count(node: Node, conn: RpcConnection, _: list[Any]) int[source]¶
Answer getconnectioncount, a pending connection counted too.
Core’s own getconnectioncount counts every entry of m_nodes (CConnman::GetNodeCount), which holds a socket from the moment it is accepted or dialled – before its handshake, not only after.
- btclib_node.rpc.callbacks.get_mempool_info(node: Node, conn: RpcConnection, _: list[Any]) dict[str, Any][source]¶
Answer getmempoolinfo with the fields this tree backs for real.
The comment below argues, field by field, why Core’s own several others are left out rather than answered with a placeholder, and why mempoolminfee alone among them is BTC/kvB rather than this tree’s own sat/kvB.
- btclib_node.rpc.callbacks.get_peer_info(node: Node, conn: RpcConnection, _: list[Any]) list[dict[str, Any]][source]¶
Answer getpeerinfo, one entry per handshake-complete peer.
A pending connection – accepted or dialled but short of verack – is left out: it carries no version_message yet for the fields below to read. Each field matches one getpeerinfo answers with its own CNode::CopyStats (src/net.cpp, at bitcoin/bitcoin@58a7869f86), cited beside where it is built.
- btclib_node.rpc.callbacks.get_raw_mempool(node: Node, conn: RpcConnection, params: list[Any]) dict[str, Any] | list[str][source]¶
Answer getrawmempool, Core’s own three shapes by params.
verbose alone answers one object per mempool transaction; neither flag answers a plain array of txids; mempool_sequence alone adds node.mempool.sequence beside that array. The two together are refused outright, matching MempoolToJSON’s own combination check.
- btclib_node.rpc.callbacks.get_raw_transaction(node: Node, conn: RpcConnection, params: list[Any]) dict[str, Any] | str[source]¶
getrawtransaction, for a mempool transaction or a named block’s.
No -txindex equivalent: this node keeps no lookup from every txid it has ever confirmed to the block that holds it, so a transaction is answered for exactly the two cases Core itself falls back to without one – the mempool by itself (src/rpc/rawtransaction.cpp:313-314, !g_txindex), and a block named explicitly, searched rather than indexed. Both are read-only lookups against block_index and block_db, which already hold every validated block for reasons of their own; this adds no store.
btclib’s own BitcoinCoreFetcher.get_tx calls this with a txid alone, verbosity 0 being its _call’s implicit default – the shape it always gets, unconditionally, below.
- btclib_node.rpc.callbacks.get_tx_out_set_info(node: Node, conn: RpcConnection, params: list[Any]) dict[str, Any][source]¶
Answer gettxoutsetinfo from UtxoIndex’s own running CoinStats.
Core’s own default path recomputes every field from a live scan of the coins database (ComputeUTXOStats, kernel/coinstats.cpp) on every call, unless -coinstatsindex is running, in which case the incrementally-maintained CoinStatsIndex answers instead (index/coinstatsindex.cpp) – chainstate/muhash.py’s own module docstring is where CoinStats is argued as this tree’s equivalent of that second path, the only one it implements. height, bestblock, txouts, bogosize, total_amount and (for hash_type: “muhash”) muhash are Core’s own field names and units, total_amount in BTC through _btc_amount the way get_mempool_info’s own mempoolminfee already is; muhash itself is the raw digest bytes reversed before this returns, matching uint256::GetHex()’s own convention rather than this class’s digest() (chainstate/muhash.py’s own comment beside is_bip30_unspendable is where that reversal is confirmed against the well-known genesis hash rather than assumed).
hash_type: “hash_serialized_3” – Core’s own default, the legacy double-SHA256 scan – is refused with ParseHashType’s own error text (RPC_INVALID_PARAMETER, ‘%s’ is not a valid hash_type), reused here for a value Core itself accepts but this tree has no accumulator for: this node answers only from CoinStats, never from a live scan, so there is no second computation to answer that hash type with. hash_type: “none” answers every field but muhash itself, the way Core’s own CoinStatsHashType::NONE does.
hash_or_height is refused the way an ordinary bitcoind, run without -coinstatsindex, already refuses it – !g_coin_stats_index (src/rpc/blockchain.cpp:1091-1092) is Core’s own gate, and this tree has no such index either: CoinStats only ever holds the current best block’s own commitment, nothing keyed by an earlier height. use_index is read and type-checked the way Core’s own RPCArg::Type::BOOL argument is, but changes nothing here: there is no non-indexed path for it to switch this tree onto, CoinStats being the only one there is.
transactions and disk_size are left out of every answer, the way Core’s own indexed answer already leaves them out (src/rpc/blockchain.cpp:1131-1134, if (!stats.index_used) {…}): both are an O(n) count over the whole set, which an incrementally maintained accumulator exists specifically to avoid paying on every call. total_unspendable_amount and block_info, CoinStatsIndex’s own two fields this tree could in principle also answer, are left out for a different reason: they need bookkeeping (the subsidy schedule, the BIP30/genesis/unclaimed-reward split) this branch does not add, and issue #639’s own “Not in scope” does not ask for them.
- btclib_node.rpc.callbacks.ping(node: Node, conn: RpcConnection, _: list[Any]) None[source]¶
Answer ping by sending every peer a fresh one, via ping_all.
Called on Node’s own thread, handle_rpc’s the same as every handler here; ping_all is defined on P2pManager but reaches this one call site as a plain method call, not a coroutine scheduled on that manager’s own loop.
- btclib_node.rpc.callbacks.prune_blockchain(node: Node, conn: RpcConnection, params: list[Any]) int[source]¶
Answer pruneblockchain: manually delete up to height, or a timestamp.
Core’s own pruneblockchain (rpc/blockchain.cpp:918-975, at bitcoin/bitcoin@ca7162cde5). Requires Config.pruned, matching IsPruneMode()’s own refusal (rpc/blockchain.cpp:936-938) – manual pruning (Config.prune_target_mib unset) and automatic pruning (set) both answer this RPC the same way, Core drawing no such distinction for it either; main._prune_chain is the one place the two differ.
height above _PRUNE_TIMESTAMP_TO_HEIGHT_THRESHOLD is read as a block time instead, by _height_from_timestamp above.
Refused the way Core refuses it, in Core’s own order and wording: a missing or wrongly typed height (RPCMethod::HandleRequest’s own generic argument check, same as get_block_hash above), a negative one (rpc/blockchain.cpp:945-947), a chain shorter than chain.prune_after_height (rpc/blockchain.cpp:962-963, Core’s own per-chain nPruneAfterHeight – 100000 on mainnet, 1000 elsewhere, chains.py’s own leaves carrying the line each comes from), and a height past the tip (rpc/blockchain.cpp:964-965). A height within MIN_BLOCKS_TO_KEEP of the tip is not refused, only clamped down to it (rpc/blockchain.cpp:966-969), and pruning still runs – that clamp’s own floor is MIN_BLOCKS_TO_KEEP, not prune_after_height, matching Core drawing the two apart too.
Answers block_db.BlockDB.pruned_up_to, Core’s own “height of the last block pruned” (rpc/blockchain.cpp:927-928) – this store already tracks exactly that height, so there is no index scan to answer it with the way Core’s own GetPruneHeight runs one.
- btclib_node.rpc.callbacks.send_raw_transaction(node: Node, conn: RpcConnection, params: list[Any]) str[source]¶
Answer sendrawtransaction: verify, add to the mempool, announce.
A transaction that fails to decode, that verify_mempool_acceptance refuses, or that Mempool.add_tx evicts right back out under its own size limit is each refused with the reject reason and code cited beside its own raise, below; one kept is broadcast to peers and its txid answered.
- btclib_node.rpc.callbacks.service_names(services: int) list[str][source]¶
Return the service bits the way Core’s getpeerinfo names them.
serviceFlagsToStr, which is a walk over the set bits from the least significant up rather than over the names: a bit a member names contributes that name without the
NODE_prefix Core’s own enum carries, and a bit none names contributes “UNKNOWN[2^n]” rather than nothing. Core reserves a range of bits for temporary experiments and sends everything else through the BIP process, so a bit nobody here has heard of is a service and not an error – and dropping it would report a peer as offering less than it said it does.
- btclib_node.rpc.callbacks.stop(node: Node, conn: RpcConnection, _: list[Any]) str[source]¶
Answer stop; handle_rpc waits for this reply before stopping.
- btclib_node.rpc.callbacks.test_mempool_accept(node: Node, conn: RpcConnection, params: list[Any]) list[dict[str, Any]][source]¶
Answer testmempoolaccept, one verdict per raw tx in params[0].
Runs verify_mempool_acceptance without calling Mempool.add_tx, so a transaction it verifies is reported allowed without being added – the same reject reasons send_raw_transaction raises are reported here per entry instead, neither ending the whole batch. A fault that is neither of those two propagates and does end it, matching Core’s own testmempoolaccept, which has no per-tx catch-all either (btclib-org/btclib-node#668).
btclib_node.rpc.connection module¶
RpcConnection, one accepted HTTP socket carrying one or more requests.
Parses the header section off the wire, bounded by MAX_HEADER_BYTES and MAX_BODY_BYTES since the listener this serves is bound to every interface, and decodes the JSON-RPC batch rpc.manager.RpcManager.messages queues for rpc.main.handle_rpc. RawJSON is a JSON number written back out exactly as given, the way Core’s own UniValue writes one built from a string rather than from a float.
Matching Core’s own per-version keep-alive default (_wants_keep_alive, src/httpserver.cpp:557-575, at bitcoin/bitcoin@ca7162cde5), async_send keeps the socket open across replies where the request it is answering asked to, reading the next request off the same connection rather than requiring a fresh accept per call (issue #640).
- class btclib_node.rpc.connection.JSONEncoder(mark: str = '', **kwargs: Any)[source]¶
Bases:
JSONEncoderEncode bytes as hex and unwrap a RawJSON under a caller’s mark.
default below is json.dumps’s own hook for a type it has no built-in encoding for; it is what RpcConnection.async_send supplies cls= and mark= to, so that a RawJSON value comes out marked rather than quoted, for async_send to unquote once encoding is done – json itself has no hook for writing a literal unquoted.
- default(obj: object) Any[source]¶
Implement this method in a subclass such that it returns a serializable object for
o, or calls the base implementation (to raise aTypeError).For example, to support arbitrary iterators, you could implement default like this:
def default(self, o): try: iterable = iter(o) except TypeError: pass else: return list(iterable) # Let the base class default method raise the TypeError return super().default(o)
- class btclib_node.rpc.connection.RawJSON(text: str)[source]¶
Bases:
objectA JSON number written to the wire exactly as given, not from a float.
Core’s own UniValue(UniValue::VNUM, “<string>”) does the same: the value is built from a string and written out verbatim, whatever that string was, rather than round-tripped through a floating-point type on the way out. ValueFromAmount (src/core_io.cpp:283-293, at bitcoin/bitcoin@58a7869f86) is the caller this exists for – rpc.callbacks.get_mempool_info’s own mempoolminfee, an exact eight-decimal BTC amount a Python float cannot always carry: repr fixes no decimal places and emits exponent notation (1e-06) at a magnitude ordinary for a feerate, which Core’s own %d.%08d format never does.
json.JSONEncoder.default cannot return this directly – its return value is re-encoded through the same machinery rather than written as-is, and Python’s json has no hook for a raw literal. JSONEncoder.default below returns a marked placeholder instead, and RpcConnection.async_send substitutes it, quotes and all, for text once encoding has already run.
The mark is not a fixed word: a fixed one is not actually safe – a plain string value that happens to contain it once, unpaired (an error message echoing back a client’s own malformed method name, say), lets a regex substitution’s own non-greedy match run past that string’s closing quote and merge it with an unrelated placeholder later in the same response, corrupting both. RpcConnection.async_send passes a fresh random token instead, one per call, so a legitimate value colliding with it is not a realistic risk the way colliding with a guessable word is.
- class btclib_node.rpc.connection.RequestHead(request_line: bytes, separator: bytes, fields: bytes, length: int, keep_alive: bool, consumed: int)[source]¶
Bases:
objectOne request’s own header section and the framing decision from it.
request_line and fields are kept as the raw bytes parse_request_head split them from, not reduced to HTTPMessage’s own parsed object, so serialize reproduces the exact octets parsed – the same round-trip tests/fuzz_corpus_test.py already holds p2p.connection.frame_message_bytes to. length and keep_alive are the two decisions RpcConnection.run draws from this section before it knows how many more bytes to read.
separator is the exact bytes head.partition(b”rn”) returned between request_line and fields inside parse_request_head – b”rn” for a request carrying at least one header field, b”” for one carrying none (request_line itself is then the whole of head, and there is nothing for a separator to separate). Stored rather than assumed, since assuming b”rn” unconditionally is exactly what made serialize fabricate two octets that were never in the input for a zero-field request (issue #516 review round 1). consumed is len(serialize()), computed once in parse_request_head from data directly rather than re-derived from serialize() at every call site: run trims self.buffer by this, not by re-parsing its own output.
- class btclib_node.rpc.connection.RpcConnection(loop: AbstractEventLoop, client: socket.socket, manager: RpcManager, connection_id: int, request_timeout: float = 30.0)[source]¶
Bases:
objectOne accepted RPC socket, from the header read through the reply.
RpcManager.server builds one per accepted client, on this manager’s own thread; run below is scheduled on the same loop and reads the request off client, queuing it onto manager.messages for rpc.main.handle_rpc on Node’s own thread to answer, through send or send_and_wait.
- async async_send(response: list[dict[str, Any]]) None[source]¶
Write response back as one JSON-RPC HTTP reply.
Wraps any RawJSON value in a fresh per-call mark before encoding, substitutes it back out unquoted once encoding is done, and frames the result behind a Content-Length header. self.keep_alive – _wants_keep_alive’s own answer, set by run above off the request this is answering – decides what happens once the reply is on the wire: client closes, or this reads another request off the same socket, matching Core’s own per-version keep-alive default either way (issue #640). self.is_batch, set by run off the same request, decides whether response stays an array here: unwrapping it purely from len(response) == 1 used to answer a one-member batch with the same bare object a lone request gets, with nothing left in response by then to tell the two apart (issue #653).
- close() None[source]¶
Close client.
Cancelling whatever this connection is doing – reading a request, writing a reply – is RpcManager.stop’s own job: its asyncio.all_tasks(self.loop) sweep already reaches whichever task is actually live for this connection, cancelled and driven to completion before this is ever called, so this method does not need a handle of its own to end one – an earlier version kept one anyway (self.task), set once at accept and never again, so past a connection’s first request it named a long-finished Future and cancelled nothing (issue #714). Core’s own per-connection object, HTTPRemoteClient, carries no such handle either: HTTPServer::ClearConnectedClients (src/httpserver.cpp:1160-1167, at bitcoin/bitcoin@ca7162cde5), its own shutdown-time sweep, drops whatever is left in m_connected the same unconditional way, once its own socket-handling thread has already been joined, rather than reaching into a live worker to end it.
- async run() None[source]¶
Read one request off client and queue it for handle_rpc.
Reads the header section up to HEADER_TERMINATOR, then the body up to its own Content-Length, both bounded against an unterminated or overstated one and, together, against taking longer than self.request_timeout – REQUEST_TIMEOUT’s own docstring is where that bound is argued against Core’s. A body that is not valid JSON is answered PARSE_ERROR directly, on the spot, and a body that is gets appended to manager.messages for rpc.main.handle_rpc to answer instead, through send. Any failure – asyncio.timeout raises the standard library’s own TimeoutError once expired, caught below like any other – closes client rather than raising, since nothing reads the Future this task runs under.
Called again, by async_send below, for every request after the first one a kept-alive connection carries – self.buffer is trimmed to what is left after this request’s own body before that request is queued, so a second call starts clean rather than re-reading bytes this one already consumed.
- send(response: list[dict[str, Any]]) None[source]¶
Schedule async_send on loop, from handle_rpc’s own thread.
- send_and_wait(response: list[dict[str, Any]]) None[source]¶
Like send, but block up to 2 seconds for the write to finish.
handle_rpc’s own stop request is the only caller: the client has to see its own reply before node.stop() starts tearing loop down under it. Forces a close of its own regardless of what the request asked for, whatever run last set self.keep_alive to – RpcManager.stop, called right after this returns, tears the whole loop down, so there is no next request this connection could still answer.
- btclib_node.rpc.connection.parse_request_head(data: bytes) RequestHead[source]¶
Parse one request’s header section off the front of data.
The framing half of what RpcConnection.run used to do inline, pulled out so fuzz/fuzz_rpc_head.py can drive it over raw octets the way Core’s own http_request.cpp fuzz target drives HTTPRequest::LoadControlData/LoadHeaders over a raw http_buffer (at bitcoin/bitcoin@ca7162cde5) – scoped the same way that target is: request-line and header-field framing and the Content-Length/keep-alive decisions drawn from them, never the JSON-RPC body those bytes go on to carry, which is stdlib json’s own business (run below) and not this node’s.
Raises IncompleteRequestHeadError where data does not yet hold HEADER_TERMINATOR – run’s own call site never hits this, since it only calls here once _recv_until has already confirmed the terminator is present, but a fuzzed byte string has no such guarantee. Raises MalformedRequestHeadError for a Content-Length that is not a bare non-negative integer within [0, MAX_BODY_BYTES], or a header section http.client itself refuses (an unterminated or oversized header line).
btclib_node.rpc.errors module¶
How a callback refuses a request rather than failing on it.
- exception btclib_node.rpc.errors.RpcError(code: RpcErrorCode, message: str)[source]¶
Bases:
ExceptionA request this node refuses, named by the answer it is owed.
handle_rpc turns it into the error object of JSON-RPC 2.0’s section 5.1, so raising it is how a callback says which of the two was wrong, the request or the node.
- class btclib_node.rpc.errors.RpcErrorCode(*values)[source]¶
Bases:
IntEnumThe codes of Bitcoin Core’s RPCErrorCode, src/rpc/protocol.h.
A client reads the code before it reads the message, so a refusal this node makes carries the number Core gives the same refusal. INTERNAL_ERROR is what Core’s own header reserves for a genuine fault of the server, which is why nothing here answers a bad request with it.
- btclib_node.rpc.errors.bool_param(params: list[Any], position: int, *, name: str, default: bool) bool[source]¶
Read a declared RPCArg::Type::BOOL parameter, Core’s own way.
Omitted or explicit null both stand for the argument’s own declared default. Anything else is read, and refused with RPC_TYPE_ERROR where it is not an actual JSON bool – the same check RPCMethod::HandleRequest makes for every declared argument before the handler body runs at all (src/rpc/util.cpp:653-661), applied here to the one JSON type this helper’s every caller declares. position is the zero-based index into params, the way every caller here already addresses it; type_error wants Core’s own one-based count, so it is passed position + 1.
- btclib_node.rpc.errors.error_msg(code: RpcErrorCode, message: str, request_id: object = None) dict[str, Any][source]¶
Build the error response of JSON-RPC 2.0’s section 5, code and message.
The specification requires the answer to carry the id of the request it answers, and reserves null for a request whose id could not be read out of it – which is what its own example for an invalid request object shows. So a caller passes the id wherever is_valid_rpc has already found one, and leaves it out where the request – or, for PARSE_ERROR, the body before it was even a request – is what was wrong. Nothing here reads request_id beyond embedding it in the response unchanged, so object is as much as the signature needs – the specification lets a request’s id be any JSON scalar, and this node does not itself validate the field before echoing it back.
- btclib_node.rpc.errors.type_error(position: int, name: str, value: object, expected: str) RpcError[source]¶
Refuse a declared argument’s own JSON type, Core’s own wrapped shape.
RPCMethod::HandleRequest’s own type check (src/rpc/util.cpp :652-661, read at bitcoin/bitcoin@b91d983f66) collects every mismatched argument into one UniValue object, keyed strprintf(“Position %s (%s)”, i + 1, arg.m_names), and wraps it in strprintf(“Wrong type passed:n%s”, arg_mismatch.write(4)) – UniValue::write’s own four-space indent and lack of a trailing newline after the closing brace (src/univalue/lib/univalue_write.cpp), reproduced literally below rather than through a JSON encoder, because every caller here checks exactly one declared argument and raises before a second could ever join it in the same object – there is never a second key to encode. Measured against a real bitcoind (v31.1.0, -regtest) answering a raw testmempoolaccept, getblockheader, getblockhash, getrawtransaction and sendrawtransaction call each with one argument of the wrong JSON type.
position is the argument’s own one-based position among the method’s declared arguments, the way Core counts it (i + 1), and name is Core’s own declared name for it – arg.m_names itself, the raw field the key above is built from, not GetFirstName()’s |-trimmed form (m_names.substr(0, m_names.find(‘|’)), src/rpc/util.cpp:917-920), which only RPCArg::ToString reads, for the usage string, and which HandleRequest’s own type check never calls. The two coincide for every argument checked here except getrawtransaction’s own second one, declared “verbosity|verbose” – get_raw_transaction’s own verbose is neither the raw m_names this key is built from nor GetFirstName()’s trimmed form, for the reason _parse_txid’s own usage-string comment argues.
btclib_node.rpc.main module¶
handle_rpc, called once per pass of Node’s loop.
Pops one request off RpcManager.messages, validates its JSON-RPC shape with is_valid_rpc, and dispatches it through rpc.callbacks.callbacks by method name, answering an unknown method or a malformed request with an RpcError rather than raising past the loop.
- btclib_node.rpc.main.get_connection(manager: RpcManager, connection_id: int) RpcConnection | None[source]¶
Look up connection_id in manager.connections, or None.
- btclib_node.rpc.main.handle_rpc(node: Node) None[source]¶
Pop one request batch off node.rpc_manager.messages and answer it.
Validates each request in the batch with is_valid_rpc, dispatches a valid one by method name through rpc.callbacks.callbacks, and answers an unknown method, an invalid request or a raising callback with a JSON-RPC error rather than raising past Node’s own loop – except a stop request, whose own reply is waited on before node.stop() runs, so the client sees it before the loop it arrived on is torn down.
conn_id is left in manager.connections – RpcConnection.async_send is what removes it, on the branch that actually closes conn, once conn is done answering rather than the instant this function has merely scheduled that answer. This function used to pop it here, unconditionally, on the theory that every reply eventually closes; once a reply could keep the connection open instead (issue #640), that pop raced async_send’s own re-entry into RpcConnection.run for the next request on the same kept-alive connection, on RpcManager’s own thread – conn.send below only schedules async_send, it does not wait for it, so nothing orders this function’s own next line against how far across that coroutine the other thread has already run by the time it executes. Where async_send won the race – wrote the reply, re-armed conn and read the next request whole, all inside one burst neither sock_sendall nor an already-buffered sock_recv had to suspend for – this function’s pop then removed the entry async_send had just put back for that next request’s own benefit, not the stale one it was meant to remove, and the request already queued behind it was answered by nobody: rpc.main.get_connection found no connection for it and handle_rpc silently returned, which is what a client pooling one connection across many calls (tests/functional/rpc/connections_test.py’s test_many_unpaced_calls_over_one_session_transport_do_not_reset) saw as one call in a few hundred stalling for its own full timeout with nothing logged on either side (issue #688).
btclib_node.rpc.manager module¶
RpcManager, the thread listening for JSON-RPC connections.
Runs its own asyncio loop, accepting a RpcConnection per accepted socket – one request or several, connection.RpcConnection’s own docstring has the keep-alive that decides which – and queuing what each one parses onto messages for Node’s own thread to read in rpc.main.handle_rpc. listening is set once run has actually bound the socket, which is what a caller waits on rather than is_alive() alone – that flag is true before anything is bound.
- class btclib_node.rpc.manager.RpcManager(node: Node, port: int | None)[source]¶
Bases:
ThreadThe thread listening for JSON-RPC connections.
The module docstring above is where its own loop and the boundary with Node’s thread are argued; connections and messages are this class’s own state for that, and _accept_queue is server’s own, kept here only so a test can reach it directly.
- create_connection(loop: AbstractEventLoop, client: socket) RpcConnection[source]¶
Wrap client in a RpcConnection and register it under a new id.
- 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.
- async server(loop: AbstractEventLoop, server_socket: socket) None[source]¶
Accept connections off server_socket until cancelled by stop.
Awaits _accept_queue for what _accept_loop, a task of its own, fills, one RpcConnection and one conn.run task per socket – rather than a bare await loop.sock_accept(server_socket) right here, which does not have the property the comment below argues for.
- stop() None[source]¶
Stop this manager’s loop, join its thread, and close every socket.
Guarded on is_alive for the node that never started this thread at all; the long comments below argue why the handle this schedules is cancelled unconditionally afterward, why the pending-task sweep runs as its own pass rather than folded into one combined loop, and why closing _server_socket here does not race server’s own with server_socket:.
Module contents¶
The JSON-RPC surface this node serves.
Connections, the RPC manager, the errors JSON-RPC 2.0 defines and the method handlers Node’s loop calls. manager.RpcManager is the thread; connection.RpcConnection is one accepted socket, carrying one request or several; callbacks.callbacks is the method-name table main.handle_rpc dispatches through; errors.RpcError is what a handler raises to answer with a JSON-RPC error object instead of a result.