Repository navigation
Electrum/Esplora: run index reads off the runtime workers, make request timeouts fire, page /txs/chain from the cursor - #947
Merged
Conversation
The status satd announced (the blockchain.scripthash.subscribe answer and every notification) re-sorted the history by (height, txid), which put the mempool rows (height 0 and -1) before every confirmed row. get_history listed confirmed rows first and then mempool rows by Txid's Ord, with 0 and -1 mixed. The Electrum protocol hashes "tx_hash:height:" over the history in the order get_history returns it: confirmed by height and position in the block, then mempool by (-height, tx_hash). A client checks an announced status by hashing get_history as received, so for any address with confirmed and unconfirmed history at once the two never matched, and Electrum's synchronizer disconnects on that. Build the history in one place, node_index::history, in protocol order, and use it for the subscribe answer, the notifier, get_history and get_mempool. The status hashes rows in the order given. Confirmed rows in one block are ordered by their chain ordinal (Store::get_tx_seq), looked up only for blocks holding more than one of the script's transactions. Mempool rows sort height 0 before -1, then by display-hex txid, as ElectrumX does. A mempool-index row whose transaction has already left the mempool is left out, as electrs does, instead of being listed with a guessed height and no fee. Subscribing now records the answered status in the subscription registry, so the next event that leaves it unchanged does not push it again. The registry takes it only while the subscribing connection is the scripthash's sole receiver and no status is recorded yet. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ness and unknown fee targets
Four answers differed from upstream Esplora (Blockstream's and
mempool.space's electrs, src/rest.rs):
- /address/:addr/txs and /scripthash/:hash/txs ignored ?after_txid=, so a
client paging with mempool.space's cursor got the first page on every
request. They now continue after the cursor: the next 25 confirmed for a
confirmed cursor (the /txs/chain/:last_seen_txid page), or the mempool
transactions after it and then the first 25 confirmed for a mempool
cursor. A cursor in neither list is 422 "after_txid not found", as
mempool.space answers; a malformed one is 400 and an empty one is the
first page. A script's mempool transactions are now listed in the order
the node admitted them (then by txid), the order upstream keeps them in,
instead of HashSet order, so a mempool cursor is stable between requests.
- /block/:hash/txs/:start_index returned [] at or past the end and served
any start. It now answers 404 "start index out of range" there and 400
"start index must be a multiple of 25" for an unaligned start, as
mempool.space does (blockstream.info answers 400 for both).
- Every legacy input carried "witness": []. Upstream sets witness only
when the input has one; satd now skips the empty array.
- /fee-estimates reported the relay floor for every target when the node
had no fee signal (empty mempool, no confirmed-block fee history, as in
IBD or right after startup). Upstream leaves out each target bitcoind
cannot estimate and answers {} when there is none. satd now leaves out
the Confidence::Low targets, which in blend mode are exactly the ones
that fell back to the floor.
The Esplora manual chapter documented the old answers; it now describes
these.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every Electrum handler is synchronous: it reads the address index,
RocksDB and the flat files inline. The connection task ran it inside
tokio::time::timeout(async { process_request(..) }), a future with no
await point, so the dispatch finished on its first poll and the timeout
could never fire. A long request held an API-runtime worker for as long
as it took, and with --api-threads at its small-box default of two, two
such requests stalled every Electrum connection, Esplora, events gRPC
and /metrics with it.
Each request now runs under spawn_blocking with the timeout around the
join. Past --electrumrequesttimeout the client gets the existing
"request timed out" error and the connection is closed: the handler's
work cannot be interrupted and the connection's subscriptions are
inside it. The connection task keeps its slot until that work ends, so
reconnecting after every timeout cannot run more handlers at once than
--electrummaxconns allows.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-confirmed one once Ordering a script's mempool transactions by admission time fetched every one of them with Mempool::get, which clones the whole entry and its transaction, where the earlier code fetched at most the 50 it served. The order now comes from Mempool::admission_times, which reads the times of the given txids under one read lock without cloning anything, and only the transactions on the page are fetched. Between a block connecting and the mempool dropping its transactions, a transaction can be in a script's mempool list and in its confirmed history at once. /txs listed it in both parts, and an after_txid naming it resolved in the mempool list, so the page restarted the confirmed history at the cursor itself. It is now listed once, as confirmed, as Blockstream's electrs does (prepare_history), and a cursor is looked up in the confirmed history first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Esplora's /txs/chain/:last_seen_txid read and resolved a script's whole confirmed history (every row's ordinal through the reverse map) for every page, then searched it for the cursor, so walking an N-transaction history 25 at a time cost O(N^2) reads. Store::addr_rows_desc reads a script's address rows newest first from an ordinal down, without resolving them: RocksDB seeks backwards over both address families within the key prefix, the CoinCache merges its unflushed rows in the way the flush will write them, and a run never ends between two rows of one transaction. The address index uses it for AddressIndex::confirmed_txs_newest_first, which starts at the end of the cursor's block (its block-mates sort by txid on either side of it) and stops once the page holds only settled blocks. The page order is unchanged: height descending, then txid descending. The trait's default keeps the whole-history read for other implementations. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e runtime The address routes did their RocksDB and flat-file reads inline in the async handler. A busy script's history held an API-runtime worker (shared with Electrum, events gRPC and /metrics) for as long as the read took, and the request TimeoutLayer could not answer until it ended, since a future that never yields cannot be dropped early. /txs/chain/:last_seen_txid also read the whole history for every page. - Every /address and /scripthash route now runs its reads on the blocking pool. The worker stays free and --esplorarequesttimeout answers 503 on time. A read cannot be stopped once started, so it holds a permit until it ends, even after its request timed out; the permits are sized from --esploramaxconns, so retrying after every timeout cannot pile up running reads. - /txs/chain[/:last_seen_txid] and the confirmed part of /txs (including ?after_txid=) use AddressIndex::confirmed_txs_newest_first, which reads from the cursor down instead of resolving the whole history. Answers and order are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eued batch of blocks Every block event made the chain notifier recompute the status of every subscribed scripthash, and every mempool event touching a subscription made the mempool index task recompute those: each recompute reads a history from the index, synchronous RocksDB work as long as the busiest subscribed history. Both tasks run on the core runtime, so a set of heavy subscriptions held a core worker (the one connecting blocks and serving P2P) for the length of that read on every block. - The recomputes, and the mempool task's lagged resync, now run under spawn_blocking. The tasks still take their events in order and wait for each recompute, so index mutation and notification keep their order; the core workers only wait. - The chain notifier takes every block event already queued before a pass and runs one pass for them: the pass reads the index as it then stands, so it covers them all. During IBD or a reorg with subscribers that was one full pass per block. - A mempool event looks up only the scripthashes it touched in the registry (SubscriptionRegistry::subscribed) instead of building the set of every subscription, and skips the thread hop when none is subscribed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e one fills first blockchain.scripthash.subscribe is bounded twice: per connection (--electrummaxsubsperconn, default 1000) and server-wide (--addrindexsubscriptions, default 10000, shared with Esplora's address SSE streams). With the defaults ten connections at their own cap fill the server-wide registry while 54 of the 64 connection slots are still free, and every other client's subscribe is then refused. Neither the manual nor the log said so. satd now logs at startup how many connections at the per-connection cap fill the registry, when that is fewer than --electrummaxconns, and what --addrindexsubscriptions would let every connection reach its own cap. The Electrum chapter gets a "Subscription limits" section with the same sizing, the manual describes the request timeout's new behaviour, and the Esplora chapter's high-history note describes the paged /txs/chain reads and the blocking-pool address routes. No default changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e new /txs rules The parent now orders mempool txids by admission time without cloning, lists a transaction in both lists once (as confirmed) and looks a cursor up in the confirmed history first. Those rules are kept on top of the paged read: the confirmed lookup asks the index for the page after the cursor, and a mempool txid counts as confirmed when the index has an (empty) page after it, which for an unconfirmed transaction is one ordinal lookup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merged with fix/esplora-paging-json's tip (5d445f8) as the base, since this branch already carried it and release/0.6 now holds its squashed equivalent. One conflict: the storefault and electrumbounds test-module declarations in rocksdb_store.rs, both kept. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The mempool part of /txs is filtered by a per-txid confirmed check, and the first confirmed page is read afterwards. A transaction that confirmed between the two could be listed in both parts. The served mempool slice is now also filtered against the page itself, so it is listed once, as confirmed; one that confirms after the page read is still listed, as mempool. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 14, 2026
bkeroack
added a commit
that referenced
this pull request
Oct 15, 2026
…st timeouts fire, page /txs/chain from the cursor (#947) * Electrum: hash scripthash status in history order The status satd announced (the blockchain.scripthash.subscribe answer and every notification) re-sorted the history by (height, txid), which put the mempool rows (height 0 and -1) before every confirmed row. get_history listed confirmed rows first and then mempool rows by Txid's Ord, with 0 and -1 mixed. The Electrum protocol hashes "tx_hash:height:" over the history in the order get_history returns it: confirmed by height and position in the block, then mempool by (-height, tx_hash). A client checks an announced status by hashing get_history as received, so for any address with confirmed and unconfirmed history at once the two never matched, and Electrum's synchronizer disconnects on that. Build the history in one place, node_index::history, in protocol order, and use it for the subscribe answer, the notifier, get_history and get_mempool. The status hashes rows in the order given. Confirmed rows in one block are ordered by their chain ordinal (Store::get_tx_seq), looked up only for blocks holding more than one of the script's transactions. Mempool rows sort height 0 before -1, then by display-hex txid, as ElectrumX does. A mempool-index row whose transaction has already left the mempool is left out, as electrs does, instead of being listed with a guessed height and no fee. Subscribing now records the answered status in the subscription registry, so the next event that leaves it unchanged does not push it again. The registry takes it only while the subscribing connection is the scripthash's sole receiver and no status is recorded yet. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Esplora: page /txs with after_txid, bound block pages, omit empty witness and unknown fee targets Four answers differed from upstream Esplora (Blockstream's and mempool.space's electrs, src/rest.rs): - /address/:addr/txs and /scripthash/:hash/txs ignored ?after_txid=, so a client paging with mempool.space's cursor got the first page on every request. They now continue after the cursor: the next 25 confirmed for a confirmed cursor (the /txs/chain/:last_seen_txid page), or the mempool transactions after it and then the first 25 confirmed for a mempool cursor. A cursor in neither list is 422 "after_txid not found", as mempool.space answers; a malformed one is 400 and an empty one is the first page. A script's mempool transactions are now listed in the order the node admitted them (then by txid), the order upstream keeps them in, instead of HashSet order, so a mempool cursor is stable between requests. - /block/:hash/txs/:start_index returned [] at or past the end and served any start. It now answers 404 "start index out of range" there and 400 "start index must be a multiple of 25" for an unaligned start, as mempool.space does (blockstream.info answers 400 for both). - Every legacy input carried "witness": []. Upstream sets witness only when the input has one; satd now skips the empty array. - /fee-estimates reported the relay floor for every target when the node had no fee signal (empty mempool, no confirmed-block fee history, as in IBD or right after startup). Upstream leaves out each target bitcoind cannot estimate and answers {} when there is none. satd now leaves out the Confidence::Low targets, which in blend mode are exactly the ones that fell back to the floor. The Esplora manual chapter documented the old answers; it now describes these. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Electrum: run requests on the blocking pool so the request timeout fires Every Electrum handler is synchronous: it reads the address index, RocksDB and the flat files inline. The connection task ran it inside tokio::time::timeout(async { process_request(..) }), a future with no await point, so the dispatch finished on its first poll and the timeout could never fire. A long request held an API-runtime worker for as long as it took, and with --api-threads at its small-box default of two, two such requests stalled every Electrum connection, Esplora, events gRPC and /metrics with it. Each request now runs under spawn_blocking with the timeout around the join. Past --electrumrequesttimeout the client gets the existing "request timed out" error and the connection is closed: the handler's work cannot be interrupted and the connection's subscriptions are inside it. The connection task keeps its slot until that work ends, so reconnecting after every timeout cannot run more handlers at once than --electrummaxconns allows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Esplora /txs: clone only the served mempool transactions; list a just-confirmed one once Ordering a script's mempool transactions by admission time fetched every one of them with Mempool::get, which clones the whole entry and its transaction, where the earlier code fetched at most the 50 it served. The order now comes from Mempool::admission_times, which reads the times of the given txids under one read lock without cloning anything, and only the transactions on the page are fetched. Between a block connecting and the mempool dropping its transactions, a transaction can be in a script's mempool list and in its confirmed history at once. /txs listed it in both parts, and an after_txid naming it resolved in the mempool list, so the page restarted the confirmed history at the cursor itself. It is now listed once, as confirmed, as Blockstream's electrs does (prepare_history), and a cursor is looked up in the confirmed history first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Address index: page a confirmed history newest first from the cursor Esplora's /txs/chain/:last_seen_txid read and resolved a script's whole confirmed history (every row's ordinal through the reverse map) for every page, then searched it for the cursor, so walking an N-transaction history 25 at a time cost O(N^2) reads. Store::addr_rows_desc reads a script's address rows newest first from an ordinal down, without resolving them: RocksDB seeks backwards over both address families within the key prefix, the CoinCache merges its unflushed rows in the way the flush will write them, and a run never ends between two rows of one transaction. The address index uses it for AddressIndex::confirmed_txs_newest_first, which starts at the end of the cursor's block (its block-mates sort by txid on either side of it) and stops once the page holds only settled blocks. The page order is unchanged: height descending, then txid descending. The trait's default keeps the whole-history read for other implementations. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Esplora: page /txs/chain from the cursor and run address reads off the runtime The address routes did their RocksDB and flat-file reads inline in the async handler. A busy script's history held an API-runtime worker (shared with Electrum, events gRPC and /metrics) for as long as the read took, and the request TimeoutLayer could not answer until it ended, since a future that never yields cannot be dropped early. /txs/chain/:last_seen_txid also read the whole history for every page. - Every /address and /scripthash route now runs its reads on the blocking pool. The worker stays free and --esplorarequesttimeout answers 503 on time. A read cannot be stopped once started, so it holds a permit until it ends, even after its request timed out; the permits are sized from --esploramaxconns, so retrying after every timeout cannot pile up running reads. - /txs/chain[/:last_seen_txid] and the confirmed part of /txs (including ?after_txid=) use AddressIndex::confirmed_txs_newest_first, which reads from the cursor down instead of resolving the whole history. Answers and order are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Address index: run status recomputes on the blocking pool, one per queued batch of blocks Every block event made the chain notifier recompute the status of every subscribed scripthash, and every mempool event touching a subscription made the mempool index task recompute those: each recompute reads a history from the index, synchronous RocksDB work as long as the busiest subscribed history. Both tasks run on the core runtime, so a set of heavy subscriptions held a core worker (the one connecting blocks and serving P2P) for the length of that read on every block. - The recomputes, and the mempool task's lagged resync, now run under spawn_blocking. The tasks still take their events in order and wait for each recompute, so index mutation and notification keep their order; the core workers only wait. - The chain notifier takes every block event already queued before a pass and runs one pass for them: the pass reads the index as it then stands, so it covers them all. During IBD or a reorg with subscribers that was one full pass per block. - A mempool event looks up only the scripthashes it touched in the registry (SubscriptionRegistry::subscribed) instead of building the set of every subscription, and skips the thread hop when none is subscribed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Electrum: document the subscription caps, and log when the server-wide one fills first blockchain.scripthash.subscribe is bounded twice: per connection (--electrummaxsubsperconn, default 1000) and server-wide (--addrindexsubscriptions, default 10000, shared with Esplora's address SSE streams). With the defaults ten connections at their own cap fill the server-wide registry while 54 of the 64 connection slots are still free, and every other client's subscribe is then refused. Neither the manual nor the log said so. satd now logs at startup how many connections at the per-connection cap fill the registry, when that is fewer than --electrummaxconns, and what --addrindexsubscriptions would let every connection reach its own cap. The Electrum chapter gets a "Subscription limits" section with the same sizing, the manual describes the request timeout's new behaviour, and the Esplora chapter's high-history note describes the paged /txs/chain reads and the blocking-pool address routes. No default changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Esplora /txs: drop a mempool txid the confirmed page already lists The mempool part of /txs is filtered by a per-txid confirmed check, and the first confirmed page is read afterwards. A transaction that confirmed between the two could be listed in both parts. The served mempool slice is now also filtered against the page itself, so it is listed once, as confirmed; one that confirms after the page read is still listed, as mempool. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit c50f8ab)
bkeroack
added a commit
that referenced
this pull request
Oct 15, 2026
* chore(release): cut 0.6.1 Rename docs/release-notes/0.6.1-pre.md to 0.6.1.md with the release date and tag header, and rewrite it shorter: each fix is one to three sentences with its PR, the detail stays in the linked PRs. The upgrade notes keep their wording, now say that a trailing comment on an rpcpassword line is refused too, and gain the Electrum and Esplora request timeouts that now take effect (#947). The file goes from 36,787 to 14,970 bytes; build-release-notes.sh writes a 15,347-byte body. Bump the workspace version 0.6.0 -> 0.6.1 (Cargo.lock changes only the 19 workspace crates), the satd-events-client pin on satd-events-proto, the Go SDK's Version, and the illustrative versions in the appliance README, the manual's Electrum and installing chapters, and the Warnet README, examples, Dockerfile comment and render script. The changelog's [Unreleased] list becomes the 0.6.1 releases-table row, and the release-notes index gains the 0.6.1 row. Not touched, as for 0.6.0: the Umbrel and StartOS image pins and package versions, which name a per-commit image digest that only exists once the tag has built it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Release notes: rustls 0.23.46 in 0.6.1 The 0.6.1 notes and changelog row name the rustls update from #966. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <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.
Problem
Electrum and Esplora do their index work synchronously (RocksDB and flat-file reads), and several places ran that work where it held a runtime worker, or repeated it without need:
handle_connectionwrappedprocess_requestintokio::time::timeout(request_timeout, async { process_request(..) }). The inner future has no await point, so the dispatch ran to completion on its first poll. A long request (ascripthash.subscribebatch over long histories,get_balance,get_first_use) held an API-runtime worker for as long as it took; with--api-threadsat its small-box default of 2, two such requests stalled every Electrum connection, Esplora, events gRPC and/metrics./address/:a,/txs,/txs/chain[/:last],/txs/mempool,/utxoand the scripthash forms) did the same inline in theasync fn, soTimeoutLayercould not answer before the read ended either./txs/chain/:last_seen_txidread and resolved the whole history for every page and then searched it for the cursor, so walking an N-transaction history 25 at a time cost O(N²) reads. The confirmed part of/txs?after_txid=(added in Esplora: page /txs with after_txid, 404 past a block's last page, omit empty witness and unknown fee targets #943) shared that path.tokio::spawninrun), so heavy subscriptions held a core worker on every block. Queued block events (IBD, a reorg) each got a full pass, and each mempool event built the set of every subscription to filter its touched scripthashes.--electrummaxsubsperconn(1000) and the server-wide--addrindexsubscriptions(10000, shared with Esplora's address SSE streams) are not coherent: ten connections at their own cap fill the registry while 54 of 64 connection slots are free, after which every other client's subscribe is refused. Nothing documented or logged this.Fix
electrum-proto/src/server.rs): each request runs underspawn_blockingwith the timeout around the join. Past--electrumrequesttimeoutthe client gets the existingrequest timed outerror and the connection is closed (the handler cannot be interrupted and the connection's subscriptions are inside it). The connection task keeps its--electrummaxconnsslot until the work ends, so reconnecting after every timeout cannot run more handlers at once than the connection cap.block_in_placein the tweaks handler runs the closure inline on a blocking-pool thread, so it keeps working; its comment is updated.esplora-handlers/src/blocking.rs,handlers/address.rs): every address and scripthash route runs its reads on the blocking pool under a work permit held until the work ends, even after its request timed out. The permits are sized from--esploramaxconns(none when it is 0, as for the request cap), so retries after timeouts cannot pile up running reads.Store::addr_rows_desc,AddressIndex::confirmed_txs_newest_first): the store reads a script's address rows newest first from an ordinal down without resolving them. RocksDB seeks backwards over both address families in total order and checks the 16-byte key prefix on every key; theCoinCachemerges its unflushed rows in exactly as the flush will write them; a run never ends between two rows of one transaction. The address index starts at the end of the cursor's block (block-mates sort by txid on either side of the cursor) and stops once the page holds only settled blocks. Output and order are unchanged: height descending, thenTxiddescending, an unknown cursor[]on/txs/chain/:lastand 422 on?after_txid=. The trait default keeps the whole-history read for other implementations. Esplora: page /txs with after_txid, 404 past a block's last page, omit empty witness and unknown fee targets #943's/txsrules are kept: a cursor is looked up in the confirmed history first (one paged read), and a mempool txid is dropped from the mempool part when the index has a page after it, which for an unconfirmed transaction costs one ordinal lookup.node/src/index/address/notifier.rs,mempool.rs): the recomputes and the mempool task's lagged resync run underspawn_blocking; the tasks still take events in order and wait for each recompute, so index mutation and notification keep their order. The chain notifier drains every queued block event before a pass and runs one pass for them (the pass reads the index as it then stands). Mempool events look up only their touched scripthashes (SubscriptionRegistry::subscribed) and skip the thread hop when none is subscribed. The two tasks stay on the core runtime: block connection emits the events they consume there, andrpc/access.rs/emit_chain_eventdocument the notifier as a core-runtime consumer of those events, so moving the consumers was left alone.--electrummaxconns, and the value of--addrindexsubscriptionsthat lets every connection reach its own cap. The Electrum chapter gets a "Subscription limits" section; the config reference and the Esplora chapter are updated for the timeout, permit and paging behaviour.Tests
All fail without the fix (run on the unfixed code first, then each guard reverted):
electrum-protoserver::electrumbounds_testsrequest_past_the_timeout_is_answered_with_an_error_and_the_connection_closed: fails without the fix: yes (no timeout error; the slow answer arrives after 1.5 s).a_slow_request_does_not_hold_up_other_connections(one worker; blocking-socket client on its own thread): fails without the fix: yes (a ping waited 1.300s behind another connection's slow request).a_timed_out_request_keeps_its_connection_slot_until_its_work_ends: fails without the fix: yes; with only the wait-for-the-work loop removed: yes (the second connection is admitted instead of refused at capacity, so no capacity answer arrives).esplora-handlershandlers::address::electrumbounds_tests(a real router over an empty regtest chain and an index whose history read sleeps):a_slow_address_read_does_not_hold_up_other_requests: fails without the fix: yes (/blocks/tip/height waited 1.303s).an_address_read_past_the_request_timeout_is_answered_503_on_time: fails without the fix: yes (200 after the read instead of 503).a_timed_out_read_keeps_its_work_permit_until_it_ends: fails without the fix: yes; with only the permit removed: yes (a second read started while the first still held the only permit).nodeindex::address::lookups::electrumbounds_testspages_match_the_whole_history_from_every_cursor: every cursor (and four that are not in the history) at limits 1, 2, 7, 25 against the trait's whole-history default, over blocks where several of the script's transactions sort against their block position. With the settled-block guard loosened: fails.a_cursor_outside_the_confirmed_history_is_none.a_deep_page_resolves_only_the_rows_near_it: 3000-transaction history; with the paged override removed: fails (read 3000 rows).nodestorage::rocksdb_store::electrumbounds_tests:reads_both_families_newest_first_within_the_prefix,runs_stop_after_min_txs_and_continue_below(neighbouring prefixes on both sides; with the never-split rule removed: fails).nodestorage::coin_cache::electrumbounds_tests::pending_rows_merge_as_the_flush_will_write_them: new, interleaved, extra-vout, removed and put-and-removed pending rows; every run size before the flush equals the walk after it. With the remove filter or the floor filter removed: fails.nodeindex::address::notifier::electrumbounds_tests:a_block_recompute_does_not_run_on_the_runtime_thread,a_mempool_recompute_does_not_run_on_the_runtime_thread(fail without the fix: yes,a 50ms sleep took 800ms),queued_block_events_share_one_recompute(fails without the fix: yes, five passes for five queued events).electrum-protoconfig::electrumbounds_testsfor the registry-fill helper.electrum-proto,esplora-handlers,node(index::address,storage),node-indexlib suites; regtestesplora/electrum; e2eelectrum/esplora(incl. Electrum: hash scripthash status in history order #938'selectrum_statusand Esplora: page /txs with after_txid, 404 past a block's last page, omit empty witness and unknown fee targets #943'sesplora_paging).Notes
ChainEventcarries no block. That is a design change; this PR keeps the full pass and moves it off the core workers.--addrindexsubscriptionstoelectrummaxconns × electrummaxsubsperconn(64,000) costs a few KB of channel per subscribed scripthash and a history read per subscription per block; that trade-off is the operator's or a later release's call.blockchain.scripthash.subscribe,get_balance,get_first_use, Esplora/address/:astats and/utxostill read the whole history per request (now off the runtime workers, bounded by the connection/permit caps and answered on time). A cap would change answers for large scripts (Blockstream's electrs refuses/utxopast itsutxos_limit), which is a behaviour decision.release/0.6(Electrum: hash scripthash status in history order #938 and Esplora: page /txs with after_txid, 404 past a block's last page, omit empty witness and unknown fee targets #943 merged); no longer stacked.id: null).Release notes
Electrum and Esplora requests that read a long address history no longer hold the API runtime's worker threads: they run on a blocking thread pool, so one slow request no longer stalls other Electrum connections, Esplora, events gRPC or
/metrics.--electrumrequesttimeoutnow takes effect (a request past it is answered with an error and its connection closed), and Esplora's address routes answer 503 at--esplorarequesttimeout. Paging an address with/txs/chain/:last_seen_txidor?after_txid=reads only the requested page instead of the whole history, and status notifications for subscribed addresses are computed off the threads that connect blocks, once per batch of queued blocks. satd now logs at startup when fewer Electrum connections than--electrummaxconnscan fill the server-wide--addrindexsubscriptionsregistry; see the Electrum chapter's "Subscription limits".🤖 Generated with Claude Code