Skip to content

protocol 1.7: revert adding chaintip to responses - #17

Merged
ecdsa merged 1 commit into
spesmilo:masterfrom
SomberNight:202606_protocol_1_7_revert_chaintip
Jul 1, 2026
Merged

ecdsa merged 1 commit into
spesmilo:masterfrom
SomberNight:202606_protocol_1_7_revert_chaintip

Conversation

@SomberNight

@SomberNight SomberNight commented Jul 1, 2026 •

Copy link
Copy Markdown
Member

partially reverts 9c32658

ref #2 (comment)


This does not revert the change of return type (array->dict) for

  • blockchain.scriptpubkey.get_history
  • blockchain.scriptpubkey.get_mempool
  • blockchain.scriptpubkey.listunspent

as that makes it easier and more backwards-compatible to add new fields to the responses.

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 SomberNight added this to the protocol 1.7 milestone Jul 1, 2026
@ecdsa
ecdsa merged commit 0264166 into spesmilo:master 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
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants