Repository navigation
protocol 1.7: revert adding chaintip to responses - #17
Merged
ecdsa merged 1 commit intoJul 1, 2026
Merged
Conversation
partially reverts 9c32658 I just realised the implementation in ElectrumX (server) was not including the chaintip in the notifications for "blockchain.outpoint.subscribe". (I missed this previously as the integration tests between Electrum Wallet and ElectrumX were passing: the client saw the missing field, logged an error and disconnected from the server, but then the client reconnected, re-subscribed to the outpoints, and got responses from the server with the chaintip included - as only the notifications were missing the chaintip, the responses were not.) Turns out with ElectrumX's architecture and how it is using bitcoind, it is not simple at all to also include the chaintip in the notifications... "blockchain.outpoint.subscribe" is implemented using bitcoin core's txospenderindex. ElectrumX calls "getrawtransaction" to find the funder tx, and then it calls "gettxspendingprevout" to find the spender tx, and for both responses bitcoind gives the containing blockhash for the tx - but not the chaintip. After that, ElectrumX uses its own mempool to enrich the "blockchain.outpoint.subscribe" response with the mempool state of funder tx and spender tx. So the server needs to make sure bitcoind's chaintip does not move and its own mempool stays consistent during all this. This was doable when serving the initial request: ElectrumX checked the chaintip before doing all the calculations and then checked the chaintip again, and if it changed, it tried again, which was already hackish. In the worst case, the server would fall back to sending a json-rpc-error to the client. but this is not practical at all while sending out notifications... I am assuming that other server implementations would eventually run into a similar issue. Also, note that when serving the history of an spk or the status of an outpoint, if all the txs touching it are somewhat-deeply mined, it is almost of no interest if the chaintip moves. Even a small reorg is not interesting if it does not affect the relevant txs as they are deeper. So, now I think the right approach is that instead of adding the chaintip to responses, we could add the blockhash corresponding to the last relevant tx. - e.g. for spk.get_history, the last tx touching the spk, - or for outpoint.get_status, the spender if it exists, or the funder if that exists, or none - also note that if a server implements this using bitcoind's txospenderindex, "getrawtransaction" and "gettxspendingprevout" already provide the relevant blockhash However, I think this needs more discussion, so the simplest approach is to just revert this for now, and see how to include something along these lines in the next version.
SomberNight
added a commit
to spesmilo/electrum
that referenced
this pull request
Jul 1, 2026
This was motivated by wanting to include the chaintip in responses but that part was reverted and postponed to later. ref spesmilo/electrum-protocol#17
SomberNight
added a commit
to SomberNight/electrumx
that referenced
this pull request
Jul 1, 2026
SomberNight
added a commit
to spesmilo/electrum
that referenced
this pull request
Jul 1, 2026
This was motivated by wanting to include the chaintip in responses but that part was reverted and postponed to later. ref spesmilo/electrum-protocol#17
NiKrause
added a commit
to Doichain/electrumx
that referenced
this pull request
Sep 16, 2026
…CI, replaces #11) (#12) * cli: aggressively log version and "spesmilo" at startup to help distinguish logs from kyuupichan/electrumx ref spesmilo/electrumx#306 * fix: handle correctly the session limiting mechanism This prevents the DoS attack vector caused by hitting the max number of sessions. Prior to this change if the max number of sessions was reached, the server would stop listening for new connections until ALL existing connections were closed. Now, the listening port is closed, the existing connections remain live and when the lower threshold is reached, the listening socket is reopened. * add mempool.get_info, update relayfee comments Adds the `mempool.get_info` rpc to fetch mempoolminfee, minrelaytxfee and incrementalrelayfee from the daemons getmempoolinfo rpc for protocol version 1.6. Updates the relayfee comments as the fee returned by `blockchain.relayfee` doesn't guarantee to get into the mempool as previously stated in the docstrings. * banner: fix daemon version parsing The parsing of the version number returned from 'getnetworkinfo' was not adapted to the changed bitcoin core version number scheme since core went from beta to stable. The banner incorrectly showed 0.29.0 instead of 29.0.0. * daemon: check daemon version on startup Checks the daemon version on startup against a minimum allowed daemon version hardcoded in the corresponding `Coin`. This allows ensure that the connected daemon is running a version that supports all the rpc calls electrumx requires. * v1.6: guard new methods behind 1.6 ptuple Guards the new `blockchain.transaction.broadcast_package` and `mempool.get_info` methods of protocol 1.6 behind this versions ptuple in `set_request_handlers` so they are only exposed in protocol versions >= 1.6. Also stops exposing the `blockchain.relayfee` endpoint for version >= 1.6 as it got removed in 1.6. * mempool.py: add a few type hints * session: broadcast_package: followup #288 Removes the incorrect and unnecessary txid calculation in `ElectrumX.package_broadcast()`, the daemon will complain anyways if the transactions are invalid. * session: estimatefee: normalize mode arg Make mode uppercase if passed as neither the protocol nor core do specify any requirement here. * session.py: sanitize inputs for tx broadcast RPCs * session: fix ReplyAndDisconnect usage for DROP_CLIENT_UNKNOWN was just sending back a response with value 1, instead of a jsonrpc error ref spesmilo/electrumx@860ff53 * session: fix ReplyAndDisconnect. was broken for PaddedRSTransport * add warmup budget to PaddedRSTransport Adds a 1024 (unpadded) byte sending budget to the PaddedRSTransport below which messages are instantly flushed down the socket before the transport will beginn waiting for the buffer to reach MIN_PACKET_SIZE (1024). This allows to get the first couple of messages sent quickly when a wallet connects to improve its ux. * protocol change: server.version must be the first message on the wire related: spesmilo/electrumx#8 * session: wait for protocol ver negotiation to finish before other reqs We already enforce that server.version must be the first received message in a session, but we also need to ensure that the server finishes processing that message and sets up the correct protocol version before starting to process further messages. * docs: update to reflect removal of DROP_CLIENT_UNKNOWN envvar * session: 'server.version' must tolerate and ignore extra unknown args to leave room for future extensions * protocol change: define order of mempool txs in status of scripthash Note that this is a soft fork: the server can apply it even for past protocol versions. Previously, with the order being undefined, if an address had multiple mempool transactions touching it, switching between different servers could result in a change in address status simply as a result of these servers ordering mempool txs differently. This would result in the client re-requesting the whole history of the address. ----- D/i | interface.[electrum.blockstream.info:60002] | <-- ('blockchain.scripthash.subscribe', ['660b44502503064f9d5feee48726287c0973e25bc531b4b8a072f57f143d5cd0']) {} (id: 12) D/i | interface.[electrum.blockstream.info:60002] | --> 9da27f9df91e3f860212f65b736fa20a539ba6e3d509f6370367ee7f10a4d5b0 (id: 12) D/i | interface.[electrum.blockstream.info:60002] | <-- ('blockchain.scripthash.get_history', ['660b44502503064f9d5feee48726287c0973e25bc531b4b8a072f57f143d5cd0']) {} (id: 13) D/i | interface.[electrum.blockstream.info:60002] | --> [ {'fee': 200, 'height': 0, 'tx_hash': '3ee6d6e26291ce360127fe039b816470fce6eeea19b5c9d10829a1e4efc2d0c7'}, {'fee': 239, 'height': 0, 'tx_hash': '9e050f09b676b9b0ee26aa02ccee623fae585a85d6a5e24ecedd6f8d6d2d3b1d'}, {'fee': 178, 'height': 0, 'tx_hash': 'fb80adbf8274190418cb3fb0385d82fe9d47a844d9913684fa5fb3d48094b35a'}, {'fee': 200, 'height': 0, 'tx_hash': '713933c50b7c43f606dad5749ea46e3bc6622657e9b13ace9d639697da266e8b'} ] (id: 13) D/i | interface.[testnet.hsmiths.com:53012] | <-- ('blockchain.scripthash.subscribe', ['660b44502503064f9d5feee48726287c0973e25bc531b4b8a072f57f143d5cd0']) {} (id: 12) D/i | interface.[testnet.hsmiths.com:53012] | --> f7ef7237d2d62a3280acae05616200b96ad9dd85fd0473c29152a4a41e05686c (id: 12) D/i | interface.[testnet.hsmiths.com:53012] | <-- ('blockchain.scripthash.get_history', ['660b44502503064f9d5feee48726287c0973e25bc531b4b8a072f57f143d5cd0']) {} (id: 13) D/i | interface.[testnet.hsmiths.com:53012] | --> [ {'tx_hash': '9e050f09b676b9b0ee26aa02ccee623fae585a85d6a5e24ecedd6f8d6d2d3b1d', 'height': 0, 'fee': 239}, {'tx_hash': 'fb80adbf8274190418cb3fb0385d82fe9d47a844d9913684fa5fb3d48094b35a', 'height': 0, 'fee': 178}, {'tx_hash': '3ee6d6e26291ce360127fe039b816470fce6eeea19b5c9d10829a1e4efc2d0c7', 'height': 0, 'fee': 200}, {'tx_hash': '713933c50b7c43f606dad5749ea46e3bc6622657e9b13ace9d639697da266e8b', 'height': 0, 'fee': 200} ] (id: 13) * [AuxPow] Add Support for individual block headers instead of a combined hex string Array headers: fix type error Array headers: move variable initialization * Array headers: Refactor AuxPoW * Array headers: Refactor AuxPoW truncation * bump PROTOCOL_MAX to 1.6 ref spesmilo/electrum-protocol#6 * try to raise `ulimit -n` open file limit automatically at startup * prepare release 1.19.0 * lib/tx.py: fix wtxid calculation * lib/tx.py: fix vsize calculation * tests: add more test txs for Bitcoin * coins: add additional default server Adds additional default server for peer discovery bootstrap operated by known contributor. * chore(dockerfile): migrate from RocksDB to LevelDB, update to Python 3.14/Trixie * coins: rm veruscoin, fix CI The verushash dependency of this Veruscoin broke the CI. * refactor(dockerfile): remove rapidjson and leveldb extras, drop libsnappy-dev * don't get stuck during shutdown waiting for SessionManager to stop during shutdown, a busy server could get stuck waiting forever on this log line: ``` INFO:SessionManager:waiting for all server's resources to close ``` This happens if a new client connects *just before* we stop listening for new connections during shutdown. As such a client would not yet get put into the sessions dict of active sessions, and hence we would not close that session, and then we would wait forever on `server.wait_closed()`. note: - the `timeout_after(3)` change should be sufficient to fix the bug described - the `_start_main_loop` change is to fix the logged `Task exception was never retrieved` errors see log: ``` 2026-03-18T17:46:31.449692906Z INFO:ElectrumX:[36] SSL xx.xx.xx.xx:48303, 33 total 2026-03-18T17:46:42.532045642Z WARNING:Controller:received SIGTERM signal, initiating shutdown 2026-03-18T17:46:42.532074094Z INFO:Controller:shutting down 2026-03-18T17:46:42.533451153Z INFO:Prefetcher:cancelled; prefetcher stopping 2026-03-18T17:46:42.534319153Z INFO:BlockProcessor:flushing to DB for a clean shutdown... 2026-03-18T17:46:42.804120065Z INFO:ElectrumX:[8] disconnected. Sent 1,969,736 bytes in 12 messages 2026-03-18T17:46:43.081894786Z INFO:ElectrumX:[37] SSL xx.xx.xx.xx:37196, 7 total 2026-03-18T17:46:43.114221817Z INFO:ElectrumX:[38] SSL xx.xx.xx.xx:37826, 8 total 2026-03-18T17:46:43.136865456Z INFO:ElectrumX:[39] SSL xx.xx.xx.xx:65376, 9 total 2026-03-18T17:46:43.190371349Z INFO:ElectrumX:[40] SSL xx.xx.xx.xx:33240, 10 total 2026-03-18T17:46:43.190926440Z INFO:ElectrumX:[41] SSL xx.xx.xx.xx:48276, 11 total 2026-03-18T17:46:43.331333593Z INFO:ElectrumX:[42] SSL xx.xx.xx.xx:48422, 12 total 2026-03-18T17:46:43.385320324Z INFO:ElectrumX:[43] SSL xx.xx.xx.xx:48290, 13 total 2026-03-18T17:46:43.544174665Z INFO:ElectrumX:[44] SSL xx.xx.xx.xx:57090, 7 total 2026-03-18T17:46:43.660732815Z INFO:SessionManager:stop listening on servers, so no new sessions can be created 2026-03-18T17:46:43.660772535Z INFO:SessionManager:closing down server for rpc://127.0.0.1:8000 2026-03-18T17:46:43.660779193Z INFO:SessionManager:closing down server for tcp://all_interfaces:51001 2026-03-18T17:46:43.660784024Z INFO:SessionManager:closing down server for ssl://all_interfaces:51002 2026-03-18T17:46:43.660788621Z INFO:SessionManager:closing 1 active sessions 2026-03-18T17:46:43.703167858Z INFO:ElectrumX:[45] SSL xx.xx.xx.xx:59458, 1 2026-03-18T17:46:43.704859567Z Task exception was never retrieved 2026-03-18T17:46:43.704919163Z future: <Task finished name='Task-1441' coro=<TaskGroup.spawn() done, defined at /usr/local/lib/python3.12/site-packages/aiorpcx/curio.py:205> exception=RuntimeError('task group terminated')> 2026-03-18T17:46:43.704928112Z Traceback (most recent call last): 2026-03-18T17:46:43.704932863Z File "/usr/local/lib/python3.12/site-packages/aiorpcx/curio.py", line 211, in spawn 2026-03-18T17:46:43.704938243Z self._add_task(task) 2026-03-18T17:46:43.704942688Z File "/usr/local/lib/python3.12/site-packages/aiorpcx/curio.py", line 157, in _add_task 2026-03-18T17:46:43.704948140Z raise RuntimeError('task group terminated') 2026-03-18T17:46:43.704953072Z RuntimeError: task group terminated 2026-03-18T17:46:43.705579195Z INFO:ElectrumX:[46] SSL xx.xx.xx.xx:53554, 2 total 2026-03-18T17:46:43.706381696Z Task exception was never retrieved 2026-03-18T17:46:43.706410424Z future: <Task finished name='Task-1449' coro=<TaskGroup.spawn() done, defined at /usr/local/lib/python3.12/site-packages/aiorpcx/curio.py:205> exception=RuntimeError('task group terminated')> 2026-03-18T17:46:43.706419012Z Traceback (most recent call last): 2026-03-18T17:46:43.706423835Z File "/usr/local/lib/python3.12/site-packages/aiorpcx/curio.py", line 211, in spawn 2026-03-18T17:46:43.706429540Z self._add_task(task) 2026-03-18T17:46:43.706434988Z File "/usr/local/lib/python3.12/site-packages/aiorpcx/curio.py", line 157, in _add_task 2026-03-18T17:46:43.706440192Z raise RuntimeError('task group terminated') 2026-03-18T17:46:43.706444770Z RuntimeError: task group terminated 2026-03-18T17:46:43.881900608Z INFO:ElectrumX:[47] SSL xx.xx.xx.xx:33056, 3 total 2026-03-18T17:46:43.882805255Z Task exception was never retrieved 2026-03-18T17:46:43.882834359Z future: <Task finished name='Task-1467' coro=<TaskGroup.spawn() done, defined at /usr/local/lib/python3.12/site-packages/aiorpcx/curio.py:205> exception=RuntimeError('task group terminated')> 2026-03-18T17:46:43.882868552Z Traceback (most recent call last): 2026-03-18T17:46:43.882873974Z File "/usr/local/lib/python3.12/site-packages/aiorpcx/curio.py", line 211, in spawn 2026-03-18T17:46:43.882879619Z self._add_task(task) 2026-03-18T17:46:43.882884083Z File "/usr/local/lib/python3.12/site-packages/aiorpcx/curio.py", line 157, in _add_task 2026-03-18T17:46:43.882889028Z raise RuntimeError('task group terminated') 2026-03-18T17:46:43.882893514Z RuntimeError: task group terminated 2026-03-18T17:46:43.979113844Z INFO:ElectrumX:[48] SSL xx.xx.xx.xx:53972, 4 total 2026-03-18T17:46:43.980168590Z Task exception was never retrieved 2026-03-18T17:46:43.980197406Z future: <Task finished name='Task-1474' coro=<TaskGroup.spawn() done, defined at /usr/local/lib/python3.12/site-packages/aiorpcx/curio.py:205> exception=RuntimeError('task group terminated')> 2026-03-18T17:46:43.980208788Z Traceback (most recent call last): 2026-03-18T17:46:43.980214166Z File "/usr/local/lib/python3.12/site-packages/aiorpcx/curio.py", line 211, in spawn 2026-03-18T17:46:43.980219966Z self._add_task(task) 2026-03-18T17:46:43.980224382Z File "/usr/local/lib/python3.12/site-packages/aiorpcx/curio.py", line 157, in _add_task 2026-03-18T17:46:43.980229240Z raise RuntimeError('task group terminated') 2026-03-18T17:46:43.980233970Z RuntimeError: task group terminated 2026-03-18T17:46:44.005542261Z INFO:SessionManager:waiting for all server's resources to close ``` * readthedocs: update base image * ci: move from Cirrus CI to Github Actions Moves the CI from Cirrus to Github Actions. I didn't move the Coveralls stuff as it was unused anyway. I also bumped the python versions to 3.14. * test_api: create new event loop I bumped the CI to use python3.14 which makes the test fail. The reason is the get_event_loop() call doesn't automatically create a new event loop anymore but instead raises if there is none. * DB storage: implement iterator.seek(key) in a way that works consistently between LevelDB and RocksDB. (written to be used for addr history pagination, in history db) * docs: remove coveralls links * ci: re-add comment to build_docs task * session: broadcast_package RPC: fix name of arg: tx_package->raw_txs to follow protocol spec: https://github.com/spesmilo/electrum-protocol/blob/22deba89418c59b5ae5eb5b61cee3e0b33ce3dfb/docs/protocol-methods.rst?plain=1#L561 This is important in case clients are passing the arg by name. * startup logging: log both "max send" and "max recv" sizes * ci: pycodestyle: raise the max line limit it is more annoying than useful * coins: rm WIF_BYTE field * coins: rm XPUB_VERBYTES, XPRV_VERBYTES fields * (trivial) tx.py: add demarcation line - altcoin separator * coins: add explanation how base58 decoder is used * coins: rm HEADER_VALUES, HEADER_UNPACK fields * coins: rm more unused fields * deps: rm 'attrs', use stdlib dataclasses instead * lrucache: sync with electrum client module (add type hints) * coins: refactor BitcoinMixin, so that it is not used by altcoins * coins: refactor class hierarchy to fit actual Bitcoin better * coins: allow Coin.NET to be None, to indicate abstract class follow-up prev * tests: add a block test for Bitcoin mainnet this is a small block, only around 100 KB serialized (233 kwu) * replace unmaintained rocksdb bindings with f321x's fork f321x's new rocksdb bindings are based on NightTsarina/python-rocksdb [0], plus Claude (LLM) adding support to modern Cython, cpython, and RocksDB. I skimmed the source diff up to the "v2.0.0" tag (commit [1]), and I reproduced the "rocksdb-ng==2.0.0" sdist from PyPI. [2] [0]: NightTsarina/python-rocksdb@dad2509 [1]: f321x/python-rocksdb@c1959a8 [2]: `d683f9ac2c02f4a3e8be184bd4559b2c5a2f982272f06545a6ae20e2ffe1ac11 dist/rocksdb_ng-2.0.0.tar.gz` * daemon: check "txindex" is enabled for bitcoind, error if missing * tests: storage: add testcase for exception during write_batch * coins: update TX_COUNT, TX_PER_BLOCK stats for mainnet/testnet3 * coins: fix PEERS for Bitcoin oooooof * peer discovery: allow short chain splits * coins: update PEERS for testnet3 and testnet4 * env: will rm default DB_ENGINE=leveldb default in 2.0 release - user will need to choose explicitly - but backwards compat for now * coins: add mutinynet signet Adds the mutinynet bitcoin signet as additional coin. * pyproject.toml: migrate to newer style "license" declaration ref spesmilo/electrum-ecc@70e4909 * prepare release 1.20.0 * README: be tiny bit less enthusiastic about running master before 2.0 but if you are reading this commit message, maybe you should be running git master and give feedback! :) * history db: change schema, and rm compaction * history db: change schema: prefix entries with b'H' * db: rm upgrade logic with the pending db changes, an upgrade is ~as fast as a resync from genesis * (trivial) make TXNUM_PADDING a global * db: add parameter for TXOUTIDX_LEN * db: change TXOUTIDX_LEN from 4 to 3, to save db storage size In Bitcoin consensus, a txout index is stored as a uint32_t. However, in practice, an output in a tx uses at least 10 bytes (for an OP_TRUE output), so - to exhaust a 2 byte namespace, a tx would need to have a size of at least 2 ** 16 * 10 = 655 KB, - to exhaust a 3 byte namespace, a tx would need to have a size of at least 2 ** 24 * 10 = 167 MB. * (bugfix) db: change tx_num endianness (LE->BE) to match db comparator History.get_txnums and History.backup depend on ordering of tx_nums, so we want the lexicographical order (used by leveldb comparator) to match the numerical order. This is a breaking change in both the utxo db and the history db. (no migration code is provided.) * db: add (block_hash -> block_height) in-memory map For 1 million blocks, in a python dict, I guess this might take around 100 MB of RAM. // - block_hash is 32 bytes, block_height is a varsize int // - dict size seems to increase by 93 bytes for every new item // - however there is considerable additional overhead, I guess amortized over many adds, // for the hashmap. Looks like it takes around ~145 bytes per item overall. * session: implement "blockchain.outpoint.subscribe" RPC squashed with: - "blockchain.outpoint.subscribe" RPC: add optional "spk_hint" argument - "blockchain.outpoint.subscribe" RPC: implement notifications - "blockchain.outpoint.subscribe" RPC: distinguish heights "-1" and "0" Similar to scripthash statuses, the height of an unconfirmed tx is: - `-1` if it has any unconfirmed parents, - `0` otherwise. - session: implement "blockchain.outpoint.get_status" RPC - "blockchain.outpoint.subscribe": use bitcoind 31 gettxspendingprevout * daemon: require "txospenderindex" for bitcoind, error if missing * session: add "blockchain.scriptpubkey.*" RPCs * session: add 'block_hash' field to 'transaction.get_merkle' * implement new "server.ping" semantics for protocol 1.7 * session: add "bc.tx.testmempoolaccept" RPC * session: add "mempool.recent" RPC * introduce txid_rev, txid_hum terms; add type hints all over txid_rev: txid in same endianness as prevouts are serialised in a raw tx txid_hum: txid in endianness used for human-readable display typically (these are reverses of each other) * session: prefix protocol-handler method names with "phandle_" e.g. to make it clear that these are methods that really need to do input sanitization * db: clean-break lack-of-db-upgrade * session: fixes for "server.ping", and use it to send some naive noise * session: use spec-1.7-defined RPC error code for "history too large" * bump version to 2.0b1 * session: bc.tx.get_merkle: don't include "block_hash" in resp postponing this from protocol 1.7 -- the motivation for adding this field was reliant on the "height" arg being optional, which we are also postponing. * session: "server.features": omit "hash_function" for proto 1.7 * calm pycodestyle * session: "bc.outpoint.subscribe" RPC: rename height to funder_height * session: "bc.outpoint.subscribe" RPC: make spk_hint arg mandatory We don't require it but the protocol spec does, and by checking it is at least provided, at least client impls won't accidentally forget to send it. Client impls could still send an empty string as intentional decision if they really want... * db: factor out {pack,unpack}_txoutidx * db: factor out {pack,unpack}_satoshis_val * db: factor out {pack,unpack}_block_height * db: factor out {pack,unpack}_dyn_header_offset * db: move pack/unpack serialization code into new db_util.py * db: unify everything to use big-endian encoding, not mixed Performance-wise, LE was a tiny bit faster though not consistently. Perf goes 3-4% both ways, twice in LE's favour, once in BE's favour. Still, BE is nicer as that's the natural order for leveldb comparators. ``` $ python3 -m timeit -n1 -r10 --setup "import random; from electrumx.lib.util import unpack_be_uint64, unpack_le_uint64; arr = [random.randbytes(8) for _ in range(10**7)]" "[unpack_be_uint64(x) for x in arr]" 1 loop, best of 10: 1.12 sec per loop $ python3 -m timeit -n1 -r10 --setup "import random; from electrumx.lib.util import unpack_be_uint64, unpack_le_uint64; arr = [random.randbytes(8) for _ in range(10**7)]" "[unpack_le_uint64(x) for x in arr]" 1 loop, best of 10: 1.08 sec per loop $ python3 -m timeit -n1 -r10 --setup "import random; from electrumx.lib.util import pack_be_uint64, pack_le_uint64; arr = [int.from_bytes(random.randbytes(8), byteorder='big') for _ in range(10**7)]" "[pack_be_uint64(x) for x in arr]" 1 loop, best of 10: 683 msec per loop $ python3 -m timeit -n1 -r10 --setup "import random; from electrumx.lib.util import pack_be_uint64, pack_le_uint64; arr = [int.from_bytes(random.randbytes(8), byteorder='big') for _ in range(10**7)]" "[pack_le_uint64(x) for x in arr]" 1 loop, best of 10: 705 msec per loop ``` ``` $ python3 -m timeit -n1 -r10 --setup "import random; from electrumx.lib.util import unpack_be_uint32, unpack_le_uint32; arr = [random.randbytes(4) for _ in range(10**7)]" "[unpack_be_uint32(x) for x in arr]" 1 loop, best of 10: 1.03 sec per loop $ python3 -m timeit -n1 -r10 --setup "import random; from electrumx.lib.util import unpack_be_uint32, unpack_le_uint32; arr = [random.randbytes(4) for _ in range(10**7)]" "[unpack_le_uint32(x) for x in arr]" 1 loop, best of 10: 1.03 sec per loop $ python3 -m timeit -n1 -r10 --setup "import random; from electrumx.lib.util import pack_be_uint32, pack_le_uint32; arr = [int.from_bytes(random.randbytes(4), byteorder='big') for _ in range(10**7)]" "[pack_be_uint32(x) for x in arr]" 1 loop, best of 10: 629 msec per loop $ python3 -m timeit -n1 -r10 --setup "import random; from electrumx.lib.util import pack_be_uint32, pack_le_uint32; arr = [int.from_bytes(random.randbytes(4), byteorder='big') for _ in range(10**7)]" "[pack_le_uint32(x) for x in arr]" 1 loop, best of 10: 605 msec per loop ``` * db: fix silent hist_db corruption if we crash during double-reorg It would be good to write a unit test for this... This is an old regression from when the single DB was split into utxo_db and hist_db in 91ca5fc. If there is a crash (kill -9) in db.flush_backup() after flushing the hist_db but before utxo_db, then the hist_db's tx_count (or flush_count in older code) would be left lower than the utxo_db's. Say there is a simple reorg: (1) we are on chain1, but suddenly chain2 becomes longer so our bitcoind switches to it, so e-x has to back up on chain1. (2) we crash in the middle of db.flush_backup() - just after history.backup() (3) when e-x is restarted, the BlockProcessor will just see the same chain1->chain2 reorg again and back up blocks on chain1. history.backup will be a NO-OP, but that's fine. All good. However, what if there is a double-reorg? flip-flop: chain1->chain2->chain1. That is, between (2) and (3), bitcoind switches back to chain1 as it outgrew chain2. In that case the BlockProcessor will NOT trigger a reorg again. It looks at the utxo_db when considering reorgs, so it will just advance blocks forward on chain1. But the hist_db already backed up some blocks in (2)! The address history from those blocks will forever be missing from hist_db: this is a silent db corruption. * env: rm DB_ENGINE default value * rm plyvel from base install, extend release notes * sessions: _notify_inner: rename alias to scripthash * session: mempool_txoutpoint_statuses: store dataclass instead of dict * lib/tx.py: TXOSpendStatus: rename prev_height to funder_height * lib/tx.py: make TXOSpendStatus immutable * session: fix regressed _calc_txoutpoint_status followup 8ba533c ``` ERROR:ElectrumX:[0] taskgroup died. Traceback (most recent call last): File "/home/user/wspace/electrumx/./src/electrumx/server/session.py", line 994, in main_loop async with self.taskgroup as group: ^^^^^^^^^^^^^^ File "/home/user/wspace/electrumx/.venv/lib/python3.13/site-packages/aiorpcx/curio.py", line 304, in __aexit__ await self.join() File "/home/user/wspace/electrumx/./src/electrumx/lib/util.py", line 389, in join task.result() ~~~~~~~~~~~^^ File "/home/user/wspace/electrumx/./src/electrumx/server/session.py", line 1255, in notify await self._notify_inner( ...<3 lines>... ) File "/home/user/wspace/electrumx/./src/electrumx/server/session.py", line 1341, in _notify_inner spend_status_dict = self._convert_txospendstatus_to_protocol_dict(spend_status) File "/home/user/wspace/electrumx/./src/electrumx/server/session.py", line 1496, in _convert_txospendstatus_to_protocol_dict assert spend_status.spender_height is not None ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError ``` * session: run notify in session's taskgroup instead of manager's if notify raises, disconnect session * session: better handle if bitcoind errors in _notify_inner * daemon: mention limitation of check_version and check_indexes * session: rename _spender_for_txo to reflect it ignores mempool events * session: _calc_oc_txo_status: add reorg-paranoia * session: add oc_txo_status_cache Cache the result of _calc_oc_txo_status (the non-mempool txo status), as that calculation involves two bitcoind RPC calls, and bitcoind RPCs are ~expensive. Also note _notify_inner calls _calc_oc_txo_status twice, for any given prevout. * local rpc: reorganise cache statistics in getinfo * local rpc: also show cache stats for estimatefee * session: add comment re txs_sent "double-counting" * local rpc: show cache hit rate (ratio) stat * local rpc: new command: "inspect_session" example: ``` $ ./electrumx_rpc inspect_session 4 { "client_name": "electrum/4.7.2", "conn_time": "38s", "errors": 0, "is_logged": false, "pending requests": 0, "request counts": { "blockchain.estimatefee": 6, "blockchain.headers.subscribe": 1, "blockchain.scripthash.subscribe": 66, "mempool.get_fee_histogram": 1, "mempool.get_info": 1, "server.banner": 1, "server.donation_address": 1, "server.features": 1, "server.peers.subscribe": 1, "server.ping": 1, "server.version": 1 }, "request total": 81, "subs_sh": 66, "subs_txo": 0, "txs sent": 0 } ``` * session: store longer client_name * mempool: trigger refresh after db_height changes, don't wait for poll I want to minimise the time when the following does not hold: daemon.cached_height() == block_processor.height == db.db_height == session_mgr.notified_height * session: add "chaintip" field to protocol 1.7 responses * session: add comment re get_history race * session: add more logging to retry_on_chain_sync_error * prefetcher: trigger refresh after daemon height changes, don't wait for poll similar to 34d7111 I want to minimise the time when the following does not hold: daemon.cached_height() == block_processor.height == db.db_height == session_mgr.notified_height * bp,db,hist: add type hints * session: retry_on_chain_sync_error: allow daemon_height to differ Also, comparing heights was not enough: a reorg from height h to height h can happen if it crosses a difficulty retarget boundary. * db: BlockProcessor should not update db.bhash_to_bheight directly propagate updates through flush_data, to have better consistency guarantees when BlockProcessor.tip == DB.tip * session: wrap more protocol handlers with retry_on_chain_sync_error follow-up spesmilo/electrumx#362 * session: retry_on_chain_sync_error: log time taken, add comments * hist db: clear_excess: log progress * init: log whether GIL is enabled * json: replace unmaintained ujson/rapidjson extras with orjson ujson and rapidjson were both rather unmaintained and potentially even slower than modern python stdlib json. Replace them with the maintained and performant orjson library instead. * bp: add a few asserts, small comments for upcoming refactor for perf * bp: advance_txs: two loops for tx processing: (1) outputs, (2) inputs We don't care about the order of the txs in the blocks: bitcoind should have already validated consensus rules, we just trust bitcoind. Having two loops will allow parallelising processing the txs. * bp: advance_txs: pre-allocate undo_info * bp: advance_txs: use threadpool for processing tx-inputs spend_utxo() does disk reads, so previously any single disk read would block the whole of advance_txs(). * bp: check_and_advance_blocks: use threadpool for block-parsing * bp: advance_txs: make process-tx-outputs less sequential re tx_num * bp: advance_txs: use threadpool for processing tx-outputs processing tx-outputs is fully CPU-bottlenecked, so this will only be faster if running python without the GIL * bp: advance_txs: split into two functions: inputs and outputs * bp: change undo_infos to dict towards parallelising advance_blocks * bp: advance_blocks: separate loops for txouts and txins * bp: advance_blocks: use threadpool to proc multiple blocks concurrently * bp: two code paths, based on gil_enabled :( splitting the workload across threads makes things significantly slower if there is a GIL * hist db: change "unflushed" to simple set this will be friendlier for multi-threaded writes into it, than appending to bytearrays * bp: advance_blocks: parallelise history.add_unflushed() calls * bp: add comments re methods being thread-safe * bp: LTORBlockProcessor: rm advance_txs * bp: maybe fix NameIndexBlockProcessor blindly adapt to recent changes of advance_txs * changelog: add section about performance improvements * update docs/PERFORMANCE-NOTES: rm stale mentions of json parsers follow-up fcdc956 * altcoin: verge: add peers (#365) * util: rm custom cachedproperty, became part of stdlib in py 3.8 * rm some old bsv propaganda :) * env: COIN no longer mandatory, defaults to "Bitcoin", again default was previously removed in spesmilo/electrumx@3cc5b5e, around bcash fork * controller/bp: add more shutdown logging had an exception that did not get logged anywhere... --- before: ``` DEBUG:Daemon:new height: 8887 DEBUG:MemPool:waiting for DB to sync INFO:Prefetcher:new block height 8,887 hash 6496d4c0774e2d804167c491d97855ce7988d459994e288628bae1b4aac3cb94 DEBUG:BlockProcessor:new height: 8887 INFO:DB:flush took 0.0s. Height 8,887 txs: 9,519 (+1) INFO:Prefetcher:cancelled; prefetcher stopping INFO:SessionManager:stop listening on servers, so no new sessions can be created INFO:SessionManager:closing down server for rpc://127.0.0.1:8000 INFO:SessionManager:closing down server for tcp://127.0.0.1:51001 INFO:SessionManager:closing down server for ssl://127.0.0.1:51002 INFO:SessionManager:closing down server for tcp://[::1]:51001 INFO:SessionManager:closing down server for ssl://[::1]:51002 INFO:SessionManager:closing 0 active sessions INFO:SessionManager:waiting for all server's resources to close <hang> ``` after: ``` DEBUG:Daemon:new height: 8886 INFO:Prefetcher:new block height 8,886 hash 4662fcd5308e94cbbe82754d94a59123942c4009d62e88a653d5dabca48423dd DEBUG:BlockProcessor:new height: 8886 INFO:DB:flush took 0.0s. Height 8,886 txs: 9,518 (+1) ERROR:BlockProcessor:_process_prefetched_blocks got exception: Non-thread-safe operation invoked on an event loop other than the current one INFO:BlockProcessor:_process_prefetched_blocks exiting INFO:Prefetcher:cancelled; prefetcher stopping ERROR:BlockProcessor:fetch_and_process_blocks taskgroup died. Traceback (most recent call last): File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 884, in fetch_and_process_blocks async with OldTaskGroup() as group: ~~~~~~~~~~~~^^ File "/home/user/wspace/aiorpcX/aiorpcx/curio.py", line 304, in __aexit__ await self.join() File "/home/user/wspace/electrumx/./src/electrumx/lib/util.py", line 375, in join task.result() ~~~~~~~~~~~^^ File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 833, in _process_prefetched_blocks await self.check_and_advance_blocks(blocks) File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 264, in check_and_advance_blocks await self._maybe_flush() File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 415, in _maybe_flush await self.flush(True) File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 409, in flush await self.run_in_thread_with_lock(flush) File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 242, in run_in_thread_with_lock return await asyncio.shield(run_in_thread_locked()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 241, in run_in_thread_locked return await run_in_thread(func, *args) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/user/wspace/aiorpcX/aiorpcx/curio.py", line 57, in run_in_thread return await get_event_loop().run_in_executor(None, func, *args) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/cpython-versions/3.14.6_nogil/lib/python3.14t/concurrent/futures/thread.py", line 86, in run result = ctx.run(self.task) File "/opt/cpython-versions/3.14.6_nogil/lib/python3.14t/concurrent/futures/thread.py", line 73, in run return fn(*args, **kwargs) File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 404, in flush self.db.flush_dbs( ~~~~~~~~~~~~~~~~~^ flush_data=self.flush_data(), ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ flush_utxos=flush_utxos, ^^^^^^^^^^^^^^^^^^^^^^^^ estimate_txs_remaining=self.estimate_txs_remaining, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "/home/user/wspace/electrumx/./src/electrumx/server/db.py", line 319, in flush_dbs self.db_flushed_event.set() # ~~~~~~~~~~~~~~~~~~~~~~~~~^^ File "/opt/cpython-versions/3.14.6_nogil/lib/python3.14t/asyncio/locks.py", line 192, in set fut.set_result(True) ~~~~~~~~~~~~~~^^^^^^ File "/opt/cpython-versions/3.14.6_nogil/lib/python3.14t/asyncio/base_events.py", line 829, in call_soon self._check_thread() ~~~~~~~~~~~~~~~~~~^^ File "/opt/cpython-versions/3.14.6_nogil/lib/python3.14t/asyncio/base_events.py", line 866, in _check_thread raise RuntimeError( "Non-thread-safe operation invoked on an event loop other " "than the current one") RuntimeError: Non-thread-safe operation invoked on an event loop other than the current one INFO:BlockProcessor:fetch_and_process_blocks taskgroup stopped. INFO:SessionManager:stop listening on servers, so no new sessions can be created INFO:SessionManager:closing down server for rpc://127.0.0.1:8000 INFO:SessionManager:closing down server for tcp://127.0.0.1:51001 INFO:SessionManager:closing down server for ssl://127.0.0.1:51002 INFO:SessionManager:closing down server for tcp://[::1]:51001 INFO:SessionManager:closing down server for ssl://[::1]:51002 INFO:SessionManager:closing 0 active sessions INFO:SessionManager:waiting for all server's resources to close INFO:SessionManager:all servers closed <hang> ``` * db: db_flushed_event: fix thread-safety for asyncio.Event() regression from spesmilo/electrumx@34d7111 Was visible when running with: export PYTHONASYNCIODEBUG=1 export PYTHONDEVMODE=1 ``` DEBUG:Daemon:new height: 8886 INFO:Prefetcher:new block height 8,886 hash 4662fcd5308e94cbbe82754d94a59123942c4009d62e88a653d5dabca48423dd DEBUG:BlockProcessor:new height: 8886 INFO:DB:flush took 0.0s. Height 8,886 txs: 9,518 (+1) ERROR:BlockProcessor:_process_prefetched_blocks got exception: Non-thread-safe operation invoked on an event loop other than the current one INFO:BlockProcessor:_process_prefetched_blocks exiting INFO:Prefetcher:cancelled; prefetcher stopping ERROR:BlockProcessor:fetch_and_process_blocks taskgroup died. Traceback (most recent call last): File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 884, in fetch_and_process_blocks async with OldTaskGroup() as group: ~~~~~~~~~~~~^^ File "/home/user/wspace/aiorpcX/aiorpcx/curio.py", line 304, in __aexit__ await self.join() File "/home/user/wspace/electrumx/./src/electrumx/lib/util.py", line 375, in join task.result() ~~~~~~~~~~~^^ File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 833, in _process_prefetched_blocks await self.check_and_advance_blocks(blocks) File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 264, in check_and_advance_blocks await self._maybe_flush() File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 415, in _maybe_flush await self.flush(True) File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 409, in flush await self.run_in_thread_with_lock(flush) File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 242, in run_in_thread_with_lock return await asyncio.shield(run_in_thread_locked()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 241, in run_in_thread_locked return await run_in_thread(func, *args) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/user/wspace/aiorpcX/aiorpcx/curio.py", line 57, in run_in_thread return await get_event_loop().run_in_executor(None, func, *args) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/cpython-versions/3.14.6_nogil/lib/python3.14t/concurrent/futures/thread.py", line 86, in run result = ctx.run(self.task) File "/opt/cpython-versions/3.14.6_nogil/lib/python3.14t/concurrent/futures/thread.py", line 73, in run return fn(*args, **kwargs) File "/home/user/wspace/electrumx/./src/electrumx/server/block_processor.py", line 404, in flush self.db.flush_dbs( ~~~~~~~~~~~~~~~~~^ flush_data=self.flush_data(), ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ flush_utxos=flush_utxos, ^^^^^^^^^^^^^^^^^^^^^^^^ estimate_txs_remaining=self.estimate_txs_remaining, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "/home/user/wspace/electrumx/./src/electrumx/server/db.py", line 319, in flush_dbs self.db_flushed_event.set() # ~~~~~~~~~~~~~~~~~~~~~~~~~^^ File "/opt/cpython-versions/3.14.6_nogil/lib/python3.14t/asyncio/locks.py", line 192, in set fut.set_result(True) ~~~~~~~~~~~~~~^^^^^^ File "/opt/cpython-versions/3.14.6_nogil/lib/python3.14t/asyncio/base_events.py", line 829, in call_soon self._check_thread() ~~~~~~~~~~~~~~~~~~^^ File "/opt/cpython-versions/3.14.6_nogil/lib/python3.14t/asyncio/base_events.py", line 866, in _check_thread raise RuntimeError( "Non-thread-safe operation invoked on an event loop other " "than the current one") RuntimeError: Non-thread-safe operation invoked on an event loop other than the current one INFO:BlockProcessor:fetch_and_process_blocks taskgroup stopped. INFO:SessionManager:stop listening on servers, so no new sessions can be created INFO:SessionManager:closing down server for rpc://127.0.0.1:8000 INFO:SessionManager:closing down server for tcp://127.0.0.1:51001 INFO:SessionManager:closing down server for ssl://127.0.0.1:51002 INFO:SessionManager:closing down server for tcp://[::1]:51001 INFO:SessionManager:closing down server for ssl://[::1]:51002 INFO:SessionManager:closing 0 active sessions INFO:SessionManager:waiting for all server's resources to close INFO:SessionManager:all servers closed <hang> ``` * mempool: comment how process_mempool works * mempool: topologically sort candidate txs for accept_transactions This reduces worst-case time complexity. The average case looks practically unchanged. * mempool: simplify logic: rm "accept" part of fetch_and_accept only call accept_transactions from a single place * mempool: type-hints: name utxo_map * type-hints: name TxOutpoint * mempool: run accept_txs while-loop on separate thread don't bog down event loop * mempool: more comments for process_mempool * db: during init, move long event-loop-blocking calls to other thread * db: open_dbs: move long event-loop-blocking calls to other thread * storage: rocksdb: use close method from rocksdb-ng * tests: fix confusing comment * mempool: put more event-loop-blocking calls to other thread hex_str_to_hash for 100k txs (~45 MB mempool) takes 0.20 sec handle_disappeared_txs for 5k txs (1 block's worth) takes 0.08 sec new_hashes calc (the set diff) for 100k txs takes 0.03 sec * tests: mempool: fix test_keep_synchronized: was racy/timing-dependent * fix DeprecationWarning: ssl.PROTOCOL_TLS ``` INFO:SessionManager:TCP server listening on 127.0.0.1:51001 /home/user/wspace/electrumx/./src/electrumx/server/session.py:310: DeprecationWarning: ssl.PROTOCOL_TLS is deprecated self._sslc = ssl.SSLContext(ssl.PROTOCOL_TLS) INFO:SessionManager:SSL server listening on 127.0.0.1:51002 ``` * mempool: call getrawmempool as soon as daemon's height changes - bitcoind to serve getrawmempool RPC concurrently to our BlockProcessor processing the new block - with free-threaded python, this shaves off around 0.4 sec of the time until SessionManager.notified_height gets bumped * storage: add some type hints * dedupe min python version * calm pycodestyle * db: clear_excess_undo_info: add logging and comment * db: incrementally clear excess undo infos as we go * tests: storage: add testcase for batch.delete * mempool: refresh_hashes: log time taken * util: add LogTimeTaken context manager, for benchmarking * mempool: log time taken for _update_histogram * mempool: partially revert threading _process_mempool reverts parts of e1ccd4b ead66dd self.txs and self.hashXs and self.txo_to_spender are accessed from the external interface, on the asyncio thread, and they are assumed to be consistent with each other. * session: ChainSyncError: fix some stale comments * session: protocol 1.7: revert including "chaintip" in responses follows spesmilo/electrum-protocol#17 * session: bump PROTOCOL_MAX to 1.7 * contrib/Dockerfile: switch to rocksdb also, - don't pin python patch version - use default env var values - "docker stop" seems cleaner, more intuitive, than kill * contrib: add another Dockerfile, showing use w/ free-threaded python * prepare release 2.0.0 * coins: port the Doichain classes to ElectrumX 2.0 Our fleet runs ElectrumX 1.15.0 (2020). Upstream 2.0.0 moved the tree to src/, merged the name and AuxPoW mixins into NameIndexAuxPoWMixin (which carries SESSIONCLS), turned P2SH_VERBYTES and NAME_OPERATIONS into tuples, added NAME_EXPIRATION, and dropped XPUB/XPRV/WIF entirely -- they have no remaining use anywhere in the tree. Values come from the chain and the consensus rules rather than being carried over unchanged: RPC_PORT 8339 / 18339 8338 is the P2P port since Core 31, so the old value pointed ElectrumX at the wrong socket TX_COUNT 2,742,220 getchaintxstats at height 431,763 NAME_EXPIRATION 36,000 / 30 Consensus::MainNetConsensus::NameExpirationDepth (36,000 above height 48,000; regtest 30) Testnet and regtest inherit from Doichain now instead of repeating every declaration, matching how upstream keeps the Namecoin classes. Verified with the package installed: ElectrumX resolves all three classes, with SESSIONCLS NameIndexAuxPoWElectrumX, DESERIALIZER DeserializerAuxPowSegWit, BLOCK_PROCESSOR NameIndexBlockProcessor, four name operations including name_doi, and REQUIRED_DAEMON_INDEXES empty -- so no extra daemon index is required. tests/lib/test_coins.py: 16 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tests: add Doichain block fixtures CI failed on tests/test_blocks.py::test_all_coins_are_covered: upstream requires every mainnet coin class to come with at least one block fixture, and Doichain had none. 1.15.0 had no such test, so the gap only appears with 2.0. Two blocks, taken from a synced node: - **431,320** -- two transactions, the second a `name_doi`. This is the block the name index was verified against, so the fixture now covers the operation that distinguishes Doichain from Namecoin, through DeserializerAuxPowSegWit. - **100,000** -- an ordinary single-coinbase block, for the plain path. test_block checks the header hash, the previous hash and every txid against the deserialised block; both pass. The remaining failures in that file are `ModuleNotFoundError: No module named 'blake256'` on unrelated coins, which occur with or without these files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: ghost43 <somber.night@protonmail.com> Co-authored-by: Alex Lupov <alex.lupov.20610521@proton.me> Co-authored-by: f321x <f@f321x.com> Co-authored-by: Ahmed Bodiwala <ahmed_bodi@msn.com> Co-authored-by: Jeremy Rand <jeremyrand@airmail.cc> Co-authored-by: Felix <51097237+f321x@users.noreply.github.com> Co-authored-by: ethicnology <ethicnology@pm.me> Co-authored-by: sunerok <8443713+justinvforvendetta@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
partially reverts 9c32658
ref #2 (comment)
This does not revert the change of return type (array->dict) for
blockchain.scriptpubkey.get_historyblockchain.scriptpubkey.get_mempoolblockchain.scriptpubkey.listunspentas that makes it easier and more backwards-compatible to add new fields to the responses.