Repository navigation
Forward-port the 0.6.1 fixes to master - #964
Merged
Merged
Conversation
…e as an invalid mark (#936) * Check a block's merkle root first, and mark only what Core marks check_block tested for an empty transaction list before comparing the merkle root, so an empty body sent for a real header failed bad-blk-length rather than Core's bad-txnmrklroot. accept_block then marked the header's block Invalid for that and for every other non-mutation check_block failure. Core runs CheckMerkleRoot first (validation.cpp CheckBlock), and ProcessNewBlock never marks a block for a CheckBlock failure; of the rules in satd's check_block, Core marks only bad-blk-weight, from ContextualCheckBlock. A block that extends the tip and arrived without its header first had no index entry until it connected, so a failed connect marked nothing and each copy sent again was validated and written again. accept_block now writes the HeaderOnly row a header announcement would leave before connecting, as Core's AcceptBlockHeader creates the index entry before ConnectTip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * accept_block: say what a non-verdict failure leaves for an unannounced block The comment on the HeaderOnly row written before connecting said every outcome matched the headers-first path. A failure that is not a verdict on the block does not move the best-header pointer, which an announced header would have done. Say so, and say what reaches the block then. 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 6be5669)
A block whose parent had no index entry was parked in the block processor's buffer keyed by its prev hash: up to 8192 entries with no byte bound, no per-peer bound and no expiry, whether or not this node had asked for the block, before any proof-of-work or size check. Each entry kept its sender's in-flight guard, so that peer's pongs waited for the 60-second bound until the block's parent connected, and the block went through the per-input fee pass before accept_block refused it. Bitcoin Core keeps no such block: AcceptBlockHeader runs CheckBlockHeader (validation.cpp:4206) and then refuses an unknown parent as prev-blk-not-found (validation.cpp:4216). satd answers a block inv with a getdata, so a block it asked for can arrive before the parent it is still learning about, and keeping that one saves a second download. Now: - Only a block this node asked for (Core's IsBlockRequested, read in handle_block before the request record is cleared) may wait. - A waiting block must pass check_proof_of_work_bounded and be at most MAX_BLOCK_SERIALIZED_SIZE (4,000,000) bytes. - At most 4 per peer, 64 blocks and 32 MB in all; the sender's oldest goes first when it is at its share, then the oldest of all. Each block expires after 5 minutes. - The in-flight guard is counted out when the block is set aside, as Core is done with it once refused. - A block whose parent is unknown skips the fee pass and accept_block. - The sender is asked for headers, as satd already does for a cmpctblock on an unknown parent. - A block on a parent marked invalid is not kept. - Two blocks on the same parent can both wait; when one fails to connect the next is tried. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit e431f4f)
* Check a block's merkle root first, and mark only what Core marks check_block tested for an empty transaction list before comparing the merkle root, so an empty body sent for a real header failed bad-blk-length rather than Core's bad-txnmrklroot. accept_block then marked the header's block Invalid for that and for every other non-mutation check_block failure. Core runs CheckMerkleRoot first (validation.cpp CheckBlock), and ProcessNewBlock never marks a block for a CheckBlock failure; of the rules in satd's check_block, Core marks only bad-blk-weight, from ContextualCheckBlock. A block that extends the tip and arrived without its header first had no index entry until it connected, so a failed connect marked nothing and each copy sent again was validated and written again. accept_block now writes the HeaderOnly row a header announcement would leave before connecting, as Core's AcceptBlockHeader creates the index entry before ConnectTip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Bound the unknown-parent block buffer A block whose parent had no index entry was parked in the block processor's buffer keyed by its prev hash: up to 8192 entries with no byte bound, no per-peer bound and no expiry, whether or not this node had asked for the block, before any proof-of-work or size check. Each entry kept its sender's in-flight guard, so that peer's pongs waited for the 60-second bound until the block's parent connected, and the block went through the per-input fee pass before accept_block refused it. Bitcoin Core keeps no such block: AcceptBlockHeader runs CheckBlockHeader (validation.cpp:4206) and then refuses an unknown parent as prev-blk-not-found (validation.cpp:4216). satd answers a block inv with a getdata, so a block it asked for can arrive before the parent it is still learning about, and keeping that one saves a second download. Now: - Only a block this node asked for (Core's IsBlockRequested, read in handle_block before the request record is cleared) may wait. - A waiting block must pass check_proof_of_work_bounded and be at most MAX_BLOCK_SERIALIZED_SIZE (4,000,000) bytes. - At most 4 per peer, 64 blocks and 32 MB in all; the sender's oldest goes first when it is at its share, then the oldest of all. Each block expires after 5 minutes. - The in-flight guard is counted out when the block is set aside, as Core is done with it once refused. - A block whose parent is unknown skips the fee pass and accept_block. - The sender is asked for headers, as satd already does for a cmpctblock on an unknown parent. - A block on a parent marked invalid is not kept. - Two blocks on the same parent can both wait; when one fails to connect the next is tried. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * accept_block: say what a non-verdict failure leaves for an unannounced block The comment on the HeaderOnly row written before connecting said every outcome matched the headers-first path. A failure that is not a verdict on the block does not move the best-header pointer, which an announced header would have done. Say so, and say what reaches the block then. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Stop on a coin read error instead of treating the coin as missing RocksDbStore turned a failed coin read into "no such coin": get_coin did `.ok()??`, get_coins_batch `.ok().flatten()` and has_coin matched only `Ok(Some(_))`. A row that did not decode was logged and returned as `None` too. connect_block takes a missing coin as a verdict on the block that spends it (bad-txns-inputs-missingorspent), and accept_block and the reorg path then record that block as invalid, a record that survives a restart. Bitcoin Core reads coins through CCoinsViewErrorCatcher, which logs "Error reading from database" and calls std::abort() on any read error (coins.cpp:398-412), because returning would be "interpreted as 'entry not found'". Each coin read now goes through one helper: a missing row is `None`, a read error or an undecodable row stops the node with abort(), as Core does and as satd's other fail-stops do. A test build panics instead so the path can be tested. The Operator Manual says what the stop looks like and what to do after it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Manual: an unreadable coin stops the offline audit too satd-chainstate-audit looks coins up through the same store, so a coin the database cannot read now stops it with the outpoint and the RocksDB error instead of listing the coin as missing. Say so beside its exit statuses, and say where the stop applies. 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 104b5b0)
satd counted sigops with rust-bitcoin 0.32's `Script::count_sigops` and `Transaction::total_sigop_cost`. In accurate mode those keep the last OP_1..OP_16 across later OP_CHECKSIG and OP_CHECKMULTISIG opcodes, where Core's `GetSigOpCount` reads the opcode immediately before each multisig opcode (src/script/script.cpp). An OP_CHECKMULTISIG that followed another signature opcode took the earlier number instead of 20, so P2SH redeem scripts and P2WSH witness scripts could count lower than Core counts them (`OP_1 OP_CHECKSIG OP_CHECKMULTISIG`: 2 instead of 21). This is rust-bitcoin issue #6368; no released rust-bitcoin version has the fix. Add `validation::sigops`, a port of Core's `GetSigOpCount(fAccurate)`, `GetSigOpCount(scriptSig)`, `CountWitnessSigOps`, `GetLegacySigOpCount`, `GetP2SHSigOpCount` and `GetTransactionSigOpCost`, with Core's script parsing, and Core's sigopcount_tests, script_p2sh_tests and transaction_tests vectors. Use it for the block limit in `connect_block` and `check_block`, the cost each mempool entry records (which the block template budgets by and getblocktemplate reports), the policy engine's `tx.sigops_cost`, and the Stratum coinbase bound. The submitpackage fee-bypass path recorded a sigop cost of 0 and no prevout metadata for the parents it admits, so the template counted them as free. It now records what `accept_transaction` records, through one shared helper. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit c78d096)
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> (cherry picked from commit 76cd867)
…ds per peer (#939) * p2p: apply Core's message size and count limits, and bound queued sends per peer - v1 frames are capped at Core's MAX_PROTOCOL_MESSAGE_LENGTH (4,000,000 bytes) instead of 32 MiB. The v1 and v2 receive buffers grow as payload bytes arrive, at most 256 KiB ahead of them (V1Transport::readData, V2Transport MAX_RESERVE_AHEAD), and a large v1 buffer is released after its message. - inv/getdata over MAX_INV_SZ (50,000), headers over MAX_HEADERS_RESULTS (2000) and addr/addrv2 over MAX_ADDR_TO_SEND (1000) are misbehaviour, as in Core; a notfound over 5016 entries is ignored. - A peer's queue counts its bytes. getdata is served as Core's ProcessGetData does: entries are kept in a per-peer backlog and served until the queue passes 1,000,000 bytes (-maxsendbuffer's default), and the rest as it drains. The peer's next message is not read while a getdata of its is being served or its queue is over the limit (ProcessMessages). -maxuploadtarget is charged only for blocks queued. - The write loop watches the disconnect signal during a write, and drops a peer whose socket takes no bytes for 20 minutes (TIMEOUT_INTERVAL, "socket sending timeout"). Sent bytes are counted as the socket takes them. - A version whose user agent is over MAX_SUBVERSION_LENGTH is dropped, as Core's LIMITED_STRING read does; the user agent and unknown message types are sanitized with Core's SanitizeString before they are stored or logged. - getcfilters/getcfheaders/getcfcheckpt requests that Core's PrepareBlockFilterRequest refuses now disconnect instead of being ignored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: wake the manager when a getdata or a getdata resume is handed over The write loop reads nothing more from a peer until its getdata has been served, so the manager's 500 ms drain tick bounded a peer fetching blocks to about one getdata, or one send buffer's worth of a large one, per tick. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: keep a getdata cmpctblock entry that could not be queued serve_compact_block reported a MSG_CMPCT_BLOCK entry as answered even when its cmpctblock could not be queued, so the entry left the getdata backlog with nothing sent. It now says whether the message was queued, and an entry that was not stays first, as every other entry does. 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 b66aa26)
) The Rust interpreter's OP_SUCCESSx set left out 0x80, 0x81 and 0x83-0x86 (OP_LEFT, OP_RIGHT, OP_INVERT, OP_AND, OP_OR, OP_XOR). In a tapscript leaf those are OP_SUCCESSx under BIP342 and Bitcoin Core's IsOpSuccess (src/script/script.cpp:364-370), so a leaf containing one is valid. The Rust engine instead evaluated the leaf and failed it as a disabled opcode. libbitcoinconsensus already had Core's set. is_op_success now uses Core's ranges as written. Pay-to-anchor (witness v1, program 0x4e73, not P2SH-wrapped) is now exempt from DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM, as in Core's VerifyWitnessProgram (src/script/interpreter.cpp:1990). satd passes no policy flags to the interpreter today, so this changes no current verdict. Tests: - every opcode checked against Core's OP_SUCCESS list, and every opcode as a tapscript leaf (alone and in an unexecuted IF) compared with libbitcoinconsensus; - the five taproot rows of Core's script_tests.json now run in the script_tests and differential harnesses instead of being skipped; - a port of Core's script_assets_test, run on a committed 225-vector subset of Core's qa-assets script_assets_test.json, and on the full file when DIR_UNIT_TEST_DATA points at it (as in Core); - pay-to-anchor with and without the policy flag, bare and P2SH-wrapped. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit 7ff0c67)
…s, no request dumps in trace logs (#940) * tor: read the SAFECOOKIE cookie file with a size bound, regular files only The control port names the cookie file in its PROTOCOLINFO reply, and satd read it with fs::read, to end of file and without checking what it was. Bitcoin Core reads it with ReadBinaryFile(cookiefile, TOR_COOKIE_SIZE) (src/torcontrol.cpp:616), which never reads more than 32 bytes. satd now opens the file non-blocking, refuses anything that is not a regular file, and reads at most 33 bytes, so /dev/zero or a FIFO named as the cookie fails authentication at once instead of filling memory or blocking startup in open(2). A file longer than 32 bytes is still refused, as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Read bitcoin.conf comments as Core does; keep request dumps and secrets out of logs bitcoin.conf: satd skipped only lines that start with `#`, so a comment after a value became part of it. `txindex=1 # c` left txindex off, `rpcport=8332 # c` fell back to the default port, and `rpcpassword=pw # c` set the password to the whole string, all without a warning. Bitcoin Core's GetConfigOptions (src/common/config.cpp:41-62) drops everything from the first `#`, trims " \t\r\n", and refuses an rpcpassword line holding a `#` ("using # in rpcpassword can be ambiguous and should be avoided"). satd's parser and sat-cli's now do the same, with the same message. Trace logging: under -loglevel=trace (or RUST_LOG=trace) jsonrpsee-server logged every HTTP request with its headers, the Authorization header included, before satd's auth layer ran (jsonrpsee-server 0.26 src/server.rs:1026), and jsonrpsee-core logs call parameters; tungstenite logs WebSocket payloads. Core logs no credentials or parameters at any level. A global filter layer now drops TRACE output from the jsonrpsee and tungstenite targets whatever the reloadable filter allows; their DEBUG output is unchanged. Debug: Config, RpcAuthEntry, the reorg webhook target and the satd-auth operator credentials (behind RpcAuth) derived Debug over their secrets. Their Debug output now names them without the password, cookie token, rpcauth salt and tag, or webhook secret. Nothing printed them today. 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 aa95d9f)
…t empty witness and unknown fee targets (#943) * 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> * 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> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit 2958b91)
…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)
…(Core parity) (#944) * p2p: apply Core's message size and count limits, and bound queued sends per peer - v1 frames are capped at Core's MAX_PROTOCOL_MESSAGE_LENGTH (4,000,000 bytes) instead of 32 MiB. The v1 and v2 receive buffers grow as payload bytes arrive, at most 256 KiB ahead of them (V1Transport::readData, V2Transport MAX_RESERVE_AHEAD), and a large v1 buffer is released after its message. - inv/getdata over MAX_INV_SZ (50,000), headers over MAX_HEADERS_RESULTS (2000) and addr/addrv2 over MAX_ADDR_TO_SEND (1000) are misbehaviour, as in Core; a notfound over 5016 entries is ignored. - A peer's queue counts its bytes. getdata is served as Core's ProcessGetData does: entries are kept in a per-peer backlog and served until the queue passes 1,000,000 bytes (-maxsendbuffer's default), and the rest as it drains. The peer's next message is not read while a getdata of its is being served or its queue is over the limit (ProcessMessages). -maxuploadtarget is charged only for blocks queued. - The write loop watches the disconnect signal during a write, and drops a peer whose socket takes no bytes for 20 minutes (TIMEOUT_INTERVAL, "socket sending timeout"). Sent bytes are counted as the socket takes them. - A version whose user agent is over MAX_SUBVERSION_LENGTH is dropped, as Core's LIMITED_STRING read does; the user agent and unknown message types are sanitized with Core's SanitizeString before they are stored or logged. - getcfilters/getcfheaders/getcfcheckpt requests that Core's PrepareBlockFilterRequest refuses now disconnect instead of being ignored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: wake the manager when a getdata or a getdata resume is handed over The write loop reads nothing more from a peer until its getdata has been served, so the manager's 500 ms drain tick bounded a peer fetching blocks to about one getdata, or one send buffer's worth of a large one, per tick. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: don't ban local, inbound-onion or manual peers for misbehaviour Deal with a peer whose misbehaviour reaches the threshold as Core's MaybeDiscourageAndDisconnect does (net_processing.cpp), in its order: a noban peer and a manual connection are not punished; a peer on a local address (CNetAddr::IsLocal: 127.0.0.0/8, 0.0.0.0/8, ::1) is disconnected without punishing the address; anyone else is disconnected and its address banned for -bantime, as before (Core discourages it). satd banned every peer but noban ones. Every inbound onion peer arrives from the address Tor dialled from, usually 127.0.0.1, as does every unwhitelisted local client, so one such peer's misbehaviour refused all of them until the ban expired, across restarts. A manual peer was banned and then skipped by the redial loop. Peers accepted on the onion listener are now marked (Core's m_inbound_onion), and getpeerinfo reports them on the onion network, as Core's ConnectedThroughNetwork does. An outbound onion peer is banned by its onion host, the address Core punishes, rather than by the 0.0.0.0 placeholder socket every onion peer shares. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: keep a getdata cmpctblock entry that could not be queued serve_compact_block reported a MSG_CMPCT_BLOCK entry as answered even when its cmpctblock could not be queued, so the entry left the getdata backlog with nothing sent. It now says whether the message was queued, and an entry that was not stays first, as every other entry does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: tidy the MisbehaviourAction docs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: disconnect an inbound onion peer without a ban wherever Tor dials from Core decides the disconnect-only rung on the address alone (pnode.addr.IsLocal(), net_processing.cpp), and Tor on the same host dials from loopback. With Tor on another host (-bind=<lan address>=onion) every onion peer arrives from that host's address. Core discourages it, and still admits it while inbound slots are free (net.cpp); satd's ban would refuse every onion peer for -bantime. An inbound onion peer is now disconnected without a ban whatever address it arrives from. 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 e2e40ca)
… the automatic cap (#952) * p2p: apply Core's message size and count limits, and bound queued sends per peer - v1 frames are capped at Core's MAX_PROTOCOL_MESSAGE_LENGTH (4,000,000 bytes) instead of 32 MiB. The v1 and v2 receive buffers grow as payload bytes arrive, at most 256 KiB ahead of them (V1Transport::readData, V2Transport MAX_RESERVE_AHEAD), and a large v1 buffer is released after its message. - inv/getdata over MAX_INV_SZ (50,000), headers over MAX_HEADERS_RESULTS (2000) and addr/addrv2 over MAX_ADDR_TO_SEND (1000) are misbehaviour, as in Core; a notfound over 5016 entries is ignored. - A peer's queue counts its bytes. getdata is served as Core's ProcessGetData does: entries are kept in a per-peer backlog and served until the queue passes 1,000,000 bytes (-maxsendbuffer's default), and the rest as it drains. The peer's next message is not read while a getdata of its is being served or its queue is over the limit (ProcessMessages). -maxuploadtarget is charged only for blocks queued. - The write loop watches the disconnect signal during a write, and drops a peer whose socket takes no bytes for 20 minutes (TIMEOUT_INTERVAL, "socket sending timeout"). Sent bytes are counted as the socket takes them. - A version whose user agent is over MAX_SUBVERSION_LENGTH is dropped, as Core's LIMITED_STRING read does; the user agent and unknown message types are sanitized with Core's SanitizeString before they are stored or logged. - getcfilters/getcfheaders/getcfcheckpt requests that Core's PrepareBlockFilterRequest refuses now disconnect instead of being ignored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: wake the manager when a getdata or a getdata resume is handed over The write loop reads nothing more from a peer until its getdata has been served, so the manager's 500 ms drain tick bounded a peer fetching blocks to about one getdata, or one send buffer's worth of a large one, per tick. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: don't ban local, inbound-onion or manual peers for misbehaviour Deal with a peer whose misbehaviour reaches the threshold as Core's MaybeDiscourageAndDisconnect does (net_processing.cpp), in its order: a noban peer and a manual connection are not punished; a peer on a local address (CNetAddr::IsLocal: 127.0.0.0/8, 0.0.0.0/8, ::1) is disconnected without punishing the address; anyone else is disconnected and its address banned for -bantime, as before (Core discourages it). satd banned every peer but noban ones. Every inbound onion peer arrives from the address Tor dialled from, usually 127.0.0.1, as does every unwhitelisted local client, so one such peer's misbehaviour refused all of them until the ban expired, across restarts. A manual peer was banned and then skipped by the redial loop. Peers accepted on the onion listener are now marked (Core's m_inbound_onion), and getpeerinfo reports them on the onion network, as Core's ConnectedThroughNetwork does. An outbound onion peer is banned by its onion host, the address Core punishes, rather than by the 0.0.0.0 placeholder socket every onion peer shares. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: keep a getdata cmpctblock entry that could not be queued serve_compact_block reported a MSG_CMPCT_BLOCK entry as answered even when its cmpctblock could not be queued, so the entry left the getdata backlog with nothing sent. It now says whether the message was queued, and an entry that was not stays first, as every other entry does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: tidy the MisbehaviourAction docs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: disconnect an inbound onion peer without a ban wherever Tor dials from Core decides the disconnect-only rung on the address alone (pnode.addr.IsLocal(), net_processing.cpp), and Tor on the same host dials from loopback. With Tor on another host (-bind=<lan address>=onion) every onion peer arrives from that host's address. Core discourages it, and still admits it while inbound slots are free (net.cpp); satd's ban would refuse every onion peer for -bantime. An inbound onion peer is now disconnected without a ban whatever address it arrives from. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: bound learned addresses and outbound dials; manual peers outside the automatic cap - Core's address rate limit (ProcessAddrs): a token bucket per peer, one token to start, 0.1 a second up to 1000, plus 1000 when the node sends getaddr; entries are shuffled first and the addr permission is exempt. getpeerinfo reports addr_processed and addr_rate_limited. - A gossiped address joins the dial list only if the address book holds it. The list is a set in arrival order, capped at the book's size. - reserve_dial checks a dial's limit and takes its place under one lock, counting dials in flight: automatic dials against the automatic target, added nodes at most 8 at a time (Core's semAddnode), -connect, -seednode and onetry without a limit. - The reconnect tick redials manual peers whatever the automatic count, then fills the free automatic slots. Automatic dials skip banned addresses, onion hosts included; manual dials do not (Core's OpenNetworkConnection). Gossip naming a banned onion host is not kept. - Manual backoff stops at 60 s; backoff is cleared only by a dial that connects, not by a new tip. - addnode onetry is outside every limit and succeeds for a peer already connected or being dialled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: exempt a two-address test peer from the address rate limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: count only the announced addresses in the addr-fetch test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: setban of an onion host disconnects that onion peer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: a full address book replaces a terrible entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: a full network group replaces a terrible entry; a replaced address's backoff goes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: a full book of terrible tried entries takes new addresses; lock the backoff while checking membership Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit 77e7fbd)
* p2p: apply Core's message size and count limits, and bound queued sends per peer - v1 frames are capped at Core's MAX_PROTOCOL_MESSAGE_LENGTH (4,000,000 bytes) instead of 32 MiB. The v1 and v2 receive buffers grow as payload bytes arrive, at most 256 KiB ahead of them (V1Transport::readData, V2Transport MAX_RESERVE_AHEAD), and a large v1 buffer is released after its message. - inv/getdata over MAX_INV_SZ (50,000), headers over MAX_HEADERS_RESULTS (2000) and addr/addrv2 over MAX_ADDR_TO_SEND (1000) are misbehaviour, as in Core; a notfound over 5016 entries is ignored. - A peer's queue counts its bytes. getdata is served as Core's ProcessGetData does: entries are kept in a per-peer backlog and served until the queue passes 1,000,000 bytes (-maxsendbuffer's default), and the rest as it drains. The peer's next message is not read while a getdata of its is being served or its queue is over the limit (ProcessMessages). -maxuploadtarget is charged only for blocks queued. - The write loop watches the disconnect signal during a write, and drops a peer whose socket takes no bytes for 20 minutes (TIMEOUT_INTERVAL, "socket sending timeout"). Sent bytes are counted as the socket takes them. - A version whose user agent is over MAX_SUBVERSION_LENGTH is dropped, as Core's LIMITED_STRING read does; the user agent and unknown message types are sanitized with Core's SanitizeString before they are stored or logged. - getcfilters/getcfheaders/getcfcheckpt requests that Core's PrepareBlockFilterRequest refuses now disconnect instead of being ignored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: wake the manager when a getdata or a getdata resume is handed over The write loop reads nothing more from a peer until its getdata has been served, so the manager's 500 ms drain tick bounded a peer fetching blocks to about one getdata, or one send buffer's worth of a large one, per tick. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: don't ban local, inbound-onion or manual peers for misbehaviour Deal with a peer whose misbehaviour reaches the threshold as Core's MaybeDiscourageAndDisconnect does (net_processing.cpp), in its order: a noban peer and a manual connection are not punished; a peer on a local address (CNetAddr::IsLocal: 127.0.0.0/8, 0.0.0.0/8, ::1) is disconnected without punishing the address; anyone else is disconnected and its address banned for -bantime, as before (Core discourages it). satd banned every peer but noban ones. Every inbound onion peer arrives from the address Tor dialled from, usually 127.0.0.1, as does every unwhitelisted local client, so one such peer's misbehaviour refused all of them until the ban expired, across restarts. A manual peer was banned and then skipped by the redial loop. Peers accepted on the onion listener are now marked (Core's m_inbound_onion), and getpeerinfo reports them on the onion network, as Core's ConnectedThroughNetwork does. An outbound onion peer is banned by its onion host, the address Core punishes, rather than by the 0.0.0.0 placeholder socket every onion peer shares. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: keep a getdata cmpctblock entry that could not be queued serve_compact_block reported a MSG_CMPCT_BLOCK entry as answered even when its cmpctblock could not be queued, so the entry left the getdata backlog with nothing sent. It now says whether the message was queued, and an entry that was not stays first, as every other entry does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: tidy the MisbehaviourAction docs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: disconnect an inbound onion peer without a ban wherever Tor dials from Core decides the disconnect-only rung on the address alone (pnode.addr.IsLocal(), net_processing.cpp), and Tor on the same host dials from loopback. With Tor on another host (-bind=<lan address>=onion) every onion peer arrives from that host's address. Core discourages it, and still admits it while inbound slots are free (net.cpp); satd's ban would refuse every onion peer for -bantime. An inbound onion peer is now disconnected without a ban whatever address it arrives from. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: bound learned addresses and outbound dials; manual peers outside the automatic cap - Core's address rate limit (ProcessAddrs): a token bucket per peer, one token to start, 0.1 a second up to 1000, plus 1000 when the node sends getaddr; entries are shuffled first and the addr permission is exempt. getpeerinfo reports addr_processed and addr_rate_limited. - A gossiped address joins the dial list only if the address book holds it. The list is a set in arrival order, capped at the book's size. - reserve_dial checks a dial's limit and takes its place under one lock, counting dials in flight: automatic dials against the automatic target, added nodes at most 8 at a time (Core's semAddnode), -connect, -seednode and onetry without a limit. - The reconnect tick redials manual peers whatever the automatic count, then fills the free automatic slots. Automatic dials skip banned addresses, onion hosts included; manual dials do not (Core's OpenNetworkConnection). Gossip naming a banned onion host is not kept. - Manual backoff stops at 60 s; backoff is cleared only by a dial that connects, not by a new tip. - addnode onetry is outside every limit and succeeds for a peer already connected or being dialled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: exempt a two-address test peer from the address rate limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: count only the announced addresses in the addr-fetch test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: setban of an onion host disconnects that onion peer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: a full address book replaces a terrible entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: a full network group replaces a terrible entry; a replaced address's backoff goes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: a full book of terrible tried entries takes new addresses; lock the backoff while checking membership Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * p2p: count only requested blocks in the IBD download window; ask each peer for blocks it can serve The IBD scheduler recorded any re-sent block whose hash matched a height as downloaded, including heights the connector had already passed. Those entries drain only as the cursor advances, so they stayed in the `-maxahead` window, and the near-cursor priority zone was gated by that window: once it was full while the next height was not in flight, nothing handed that height out again. Any delivered block also reset the sender's stall clock. Now only an in-flight height above the cursor is recorded, only a block asked of the peer resets its clock (Core's RemoveBlockRequest), the priority zone is exempt from the window (Core's window starts at the last common block), and a pooled height the cursor has passed is dropped. A failed block-file or database write while storing an IBD or background block was charged to the sender (+10 / +5). Only a Validation or CheckpointMismatch error is the block's fault now. The tip-following sweep sent every missing block to every peer on each pass. It now asks one peer per block, at most 16 per peer, and asks again after 30 s, of a different peer when there is one. Block download now requires NODE_NETWORK or NODE_NETWORK_LIMITED (Core's CanServeBlocks); a limited peer gets IBD work only when every height it could be given is within 286 of its tip, and the AssumeUTXO background download uses NODE_NETWORK peers only. The background download caps what is outstanding per peer at 16, rotates its starting peer, gives a released height to a different peer, and releases a height on notfound. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs: the IBD window always fetches the blocks above the tip; which peers are asked for blocks Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * p2p: the sweep asks for a block only a peer known to have it; notfound frees a sweep request Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * tests: four compact peers are each asked for the block by inv, not by the sweep The tip-following sweep now asks one peer per block, so it no longer asks every peer for the same block. 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 6a18e0f)
…t start from the branch (#954) * Header sync: continue from the peer's last header; derive the retarget start from the branch After a full headers batch, ask that peer for the next headers with a locator built from the last header it sent, as Core's ProcessHeadersMessage does (MaybeSendGetHeaders(pfrom, GetLocator(pindexLast))), whether or not the batch held new headers. satd built every locator from its own height rows and only continued when a batch added headers, so a peer whose chain forks below a competing branch with more work than that peer had shown so far was answered from the fork point with the same batch each time, and header sync could not continue past the competing branch. Find the first block of a retarget period by following parent links from the candidate's parent (Core: pindexLast->GetAncestor(nHeightFirst)) instead of through the height index, so a block on a branch that forked before the period is judged against its own branch's timestamps. The by-height lookup argument is removed from check_difficulty/next_bits. Check headers and blocks with check_proof_of_work_bounded, which also requires the claimed target to be within the network's powLimit (Core's CheckProofOfWork / DeriveTarget). Signet does not pin nBits to the retarget schedule, so its headers were bounded only by their own claim. Compare the testnet 20-minute rule in 64 bits, as Core does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Header sync: test the branch locator's shape 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 ee05798)
* Count sigops exactly as Bitcoin Core does satd counted sigops with rust-bitcoin 0.32's `Script::count_sigops` and `Transaction::total_sigop_cost`. In accurate mode those keep the last OP_1..OP_16 across later OP_CHECKSIG and OP_CHECKMULTISIG opcodes, where Core's `GetSigOpCount` reads the opcode immediately before each multisig opcode (src/script/script.cpp). An OP_CHECKMULTISIG that followed another signature opcode took the earlier number instead of 20, so P2SH redeem scripts and P2WSH witness scripts could count lower than Core counts them (`OP_1 OP_CHECKSIG OP_CHECKMULTISIG`: 2 instead of 21). This is rust-bitcoin issue #6368; no released rust-bitcoin version has the fix. Add `validation::sigops`, a port of Core's `GetSigOpCount(fAccurate)`, `GetSigOpCount(scriptSig)`, `CountWitnessSigOps`, `GetLegacySigOpCount`, `GetP2SHSigOpCount` and `GetTransactionSigOpCost`, with Core's script parsing, and Core's sigopcount_tests, script_p2sh_tests and transaction_tests vectors. Use it for the block limit in `connect_block` and `check_block`, the cost each mempool entry records (which the block template budgets by and getblocktemplate reports), the policy engine's `tx.sigops_cost`, and the Stratum coinbase bound. The submitpackage fee-bypass path recorded a sigop cost of 0 and no prevout metadata for the parents it admits, so the template counted them as free. It now records what `accept_transaction` records, through one shared helper. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * mempool: verify scripts before evicting, and make eviction linear A transaction arriving at a full pool evicted entries to make room for itself before its scripts were checked. Core checks the scripts (PolicyScriptChecks, ConsensusScriptChecks) before it adds the transaction and trims the pool (src/validation.cpp, AcceptSingleTransactionInternal), so a transaction whose scripts fail changes nothing. satd now does the same: the cheap fee-rate refusal stays ahead of the scripts, and eviction comes after them. Eviction itself rescanned the whole pool for every evicted entry and every descendant, checking each candidate against a Vec of what was already chosen. It now pops a heap built in one pass, finds descendants through the spends index, and tracks membership in a HashSet. Around the same path: - Eviction never takes the incoming transaction's own ancestors, which left the child in the pool spending an output nothing provided. The fee-rate bar is the cheapest entry it may actually evict. - Admission trims the pool down to its limit, as Core's TrimToSize does, instead of freeing only its own size. A lowered -maxmempool trims the pool at once on reload; before, every admission was refused until the pool drained. - A refusal after eviction reports the entries it evicted. - The specific script error behind a libbitcoinconsensus rejection is found by running the native engine over the failing input only, after the pool's write lock is released. - The orphanage's FIFO queue holds each live orphan once, keyed by an arrival sequence as Core's orphanage is, instead of keeping the txid of every orphan it ever held. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * mempool: report only acting entries evicted; test helpers index spends Eviction removes a held descendant of an evicted acting entry with it. That entry never emitted an Enter, so a LeaveEvicted for it is a Leave for a txid the standard stream never showed. The eviction now returns each entry's scope, and admission (success and refusal) and the -maxmempool reload trim emit LeaveEvicted only for entries that were acting. The test insert helpers that take a caller's transaction now index its spends, as every admission does, since eviction finds descendants through that index. 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 97298b0)
…ettxout and sendrawtransaction (#946) * Count sigops exactly as Bitcoin Core does satd counted sigops with rust-bitcoin 0.32's `Script::count_sigops` and `Transaction::total_sigop_cost`. In accurate mode those keep the last OP_1..OP_16 across later OP_CHECKSIG and OP_CHECKMULTISIG opcodes, where Core's `GetSigOpCount` reads the opcode immediately before each multisig opcode (src/script/script.cpp). An OP_CHECKMULTISIG that followed another signature opcode took the earlier number instead of 20, so P2SH redeem scripts and P2WSH witness scripts could count lower than Core counts them (`OP_1 OP_CHECKSIG OP_CHECKMULTISIG`: 2 instead of 21). This is rust-bitcoin issue #6368; no released rust-bitcoin version has the fix. Add `validation::sigops`, a port of Core's `GetSigOpCount(fAccurate)`, `GetSigOpCount(scriptSig)`, `CountWitnessSigOps`, `GetLegacySigOpCount`, `GetP2SHSigOpCount` and `GetTransactionSigOpCost`, with Core's script parsing, and Core's sigopcount_tests, script_p2sh_tests and transaction_tests vectors. Use it for the block limit in `connect_block` and `check_block`, the cost each mempool entry records (which the block template budgets by and getblocktemplate reports), the policy engine's `tx.sigops_cost`, and the Stratum coinbase bound. The submitpackage fee-bypass path recorded a sigop cost of 0 and no prevout metadata for the parents it admits, so the template counted them as free. It now records what `accept_transaction` records, through one shared helper. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * mempool: verify scripts before evicting, and make eviction linear A transaction arriving at a full pool evicted entries to make room for itself before its scripts were checked. Core checks the scripts (PolicyScriptChecks, ConsensusScriptChecks) before it adds the transaction and trims the pool (src/validation.cpp, AcceptSingleTransactionInternal), so a transaction whose scripts fail changes nothing. satd now does the same: the cheap fee-rate refusal stays ahead of the scripts, and eviction comes after them. Eviction itself rescanned the whole pool for every evicted entry and every descendant, checking each candidate against a Vec of what was already chosen. It now pops a heap built in one pass, finds descendants through the spends index, and tracks membership in a HashSet. Around the same path: - Eviction never takes the incoming transaction's own ancestors, which left the child in the pool spending an output nothing provided. The fee-rate bar is the cheapest entry it may actually evict. - Admission trims the pool down to its limit, as Core's TrimToSize does, instead of freeing only its own size. A lowered -maxmempool trims the pool at once on reload; before, every admission was refused until the pool drained. - A refusal after eviction reports the entries it evicted. - The specific script error behind a libbitcoinconsensus rejection is found by running the native engine over the failing input only, after the pool's write lock is released. - The orphanage's FIFO queue holds each live orphan once, keyed by an arrival sequence as Core's orphanage is, instead of keeping the txid of every orphan it ever held. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * mempool RPCs: match Bitcoin Core's entry, info, gettxout and sendrawtransaction answers getmempoolentry / getrawmempool verbose: vsize rounds up as Core's GetVirtualTransactionSize does; height is the tip at admission; depends lists direct in-mempool parents only; bip125-replaceable follows an ancestor that signals (IsRBFOptIn); the ancestor and descendant fee rollups sum modified fees. getmempoolsummary rows follow. getmempoolinfo: bytes is the sum of virtual sizes, usage an estimate of the entries' memory, total_fee the base fees, and loaded reports whether the startup load of mempool.dat has finished. sendrawtransaction reads maxfeerate and maxburnamount as Core does (AmountFromValue / ParseFeeRate): a negative, unreadable or >= 1 BTC/kvB value is an error instead of a silently weakened limit. -27 is decided from the UTXO set, not the txindex. With a policy ruleset loaded, a transaction the dry run refused but the submission would admit is still held to maxfeerate. gettxout honours include_mempool (default true): an output a mempool transaction spends is null, and a mempool transaction's outputs are reported with zero confirmations. remove_for_block removes the descendants of a block conflict and clears the prioritisation of confirmed transactions and direct conflicts, as Core's removeForBlock / removeConflicts do. Mempool reload applies the persisted delta before admission, keeps each record's entry time and skips expired records (Core's LoadMempool). The expiry pass keeps a lower bound on entry times and walks the pool only when something can have expired, so an admission no longer costs a walk over the pool and the reload is linear. Docs: -limitclustercount bounds ancestors only; -maxmempool counts serialized bytes; submitpackage applies maxfeerate and maxburnamount. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * mempool: report only acting entries evicted; test helpers index spends Eviction removes a held descendant of an evicted acting entry with it. That entry never emitted an Enter, so a LeaveEvicted for it is a Leave for a txid the standard stream never showed. The eviction now returns each entry's scope, and admission (success and refusal) and the -maxmempool reload trim emit LeaveEvicted only for entries that were acting. The test insert helpers that take a caller's transaction now index its spends, as every admission does, since eviction finds descendants through that index. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * gettxout: read the pool inside the chain snapshot; fee rollups keep negative modified fees gettxout read a pool output and the chain tip separately, outside the coherent snapshot. The pool trails the chain until remove_for_block runs, so a block that confirmed a pool transaction and spent its output could be named as bestblock beside that output reported unspent. The pool reads now sit inside the snapshot, and a pool transaction counts only while every coin it and its in-pool ancestors spend is still a coin at the tip. The ancestor and descendant fee rollups added each modified fee clamped at zero. Core keeps modified fees signed (CTxMemPoolEntry::m_modified_fee) and sums them as they are (CalculateAncestorData), so a negative prioritisetransaction delta now lowers the totals, and fees.modified can be negative, formatted as Core's ValueFromAmount does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * gettxout: one pool read for spend and output; keep the expiry floor on a missed lookup gettxout asked the pool whether the outpoint was spent and what output created it under two separate read locks, so a policy reload that promoted a parent and child between them could be read half before and half after. Mempool::acting_view_of answers both under one lock. EntryMap::get_mut reset the expiry time floor even when the entry was absent. A mempool.dat reload prioritises each record before admitting it, so every prioritised record forced the next admission to walk the pool again. A miss now leaves the floor alone. 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 fbad3bd)
…ter and failed-auth answers (#955) - Doubles (difficulty, networkhashps, verificationprogress) are printed as Core's UniValue::setFloat prints them, %.16g, via rpc::util::univalue_double. - getnetworkinfo relayfee/incrementalfee and the PSBT fee (v0 and v2) are amounts with eight decimals (and follow -rpcdefaultunits). - submitblock: -22 "Block decode failed" for undecodable hex; "duplicate-invalid" for a block whose index entry is marked invalid; -25 for node failures (storage, block files, snapshot load, interrupted connect); fills in the coinbase witness reserved value as Core's UpdateUncommittedBlockStructures does. - getblockfilter: Core's order, codes and messages ("Unknown filtertype", "Index is not enabled for filtertype basic", "Block not found", ParseHashV -8). - getnetworkhashps -1 uses DifficultyAdjustmentInterval (144 on regtest). - getrawtransaction's genesis coinbase refusal uses Core's exact text. - A credential that does not authenticate is logged as "ThreadRPCServer incorrect password attempt from <ip:port>" and answered after 250 ms; a request without credentials gets its 401 at once, as in Core. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit b5a18d4)
…l window (#948) * Check a block's merkle root first, and mark only what Core marks check_block tested for an empty transaction list before comparing the merkle root, so an empty body sent for a real header failed bad-blk-length rather than Core's bad-txnmrklroot. accept_block then marked the header's block Invalid for that and for every other non-mutation check_block failure. Core runs CheckMerkleRoot first (validation.cpp CheckBlock), and ProcessNewBlock never marks a block for a CheckBlock failure; of the rules in satd's check_block, Core marks only bad-blk-weight, from ContextualCheckBlock. A block that extends the tip and arrived without its header first had no index entry until it connected, so a failed connect marked nothing and each copy sent again was validated and written again. accept_block now writes the HeaderOnly row a header announcement would leave before connecting, as Core's AcceptBlockHeader creates the index entry before ConnectTip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Bound the unknown-parent block buffer A block whose parent had no index entry was parked in the block processor's buffer keyed by its prev hash: up to 8192 entries with no byte bound, no per-peer bound and no expiry, whether or not this node had asked for the block, before any proof-of-work or size check. Each entry kept its sender's in-flight guard, so that peer's pongs waited for the 60-second bound until the block's parent connected, and the block went through the per-input fee pass before accept_block refused it. Bitcoin Core keeps no such block: AcceptBlockHeader runs CheckBlockHeader (validation.cpp:4206) and then refuses an unknown parent as prev-blk-not-found (validation.cpp:4216). satd answers a block inv with a getdata, so a block it asked for can arrive before the parent it is still learning about, and keeping that one saves a second download. Now: - Only a block this node asked for (Core's IsBlockRequested, read in handle_block before the request record is cleared) may wait. - A waiting block must pass check_proof_of_work_bounded and be at most MAX_BLOCK_SERIALIZED_SIZE (4,000,000) bytes. - At most 4 per peer, 64 blocks and 32 MB in all; the sender's oldest goes first when it is at its share, then the oldest of all. Each block expires after 5 minutes. - The in-flight guard is counted out when the block is set aside, as Core is done with it once refused. - A block whose parent is unknown skips the fee pass and accept_block. - The sender is asked for headers, as satd already does for a cmpctblock on an unknown parent. - A block on a parent marked invalid is not kept. - Two blocks on the same parent can both wait; when one fails to connect the next is tried. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * accept_block: say what a non-verdict failure leaves for an unannounced block The comment on the HeaderOnly row written before connecting said every outcome matched the headers-first path. A failure that is not a verdict on the block does not move the best-header pointer, which an announced header would have done. Say so, and say what reaches the block then. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Stop on a coin read error instead of treating the coin as missing RocksDbStore turned a failed coin read into "no such coin": get_coin did `.ok()??`, get_coins_batch `.ok().flatten()` and has_coin matched only `Ok(Some(_))`. A row that did not decode was logged and returned as `None` too. connect_block takes a missing coin as a verdict on the block that spends it (bad-txns-inputs-missingorspent), and accept_block and the reorg path then record that block as invalid, a record that survives a restart. Bitcoin Core reads coins through CCoinsViewErrorCatcher, which logs "Error reading from database" and calls std::abort() on any read error (coins.cpp:398-412), because returning would be "interpreted as 'entry not found'". Each coin read now goes through one helper: a missing row is `None`, a read error or an undecodable row stops the node with abort(), as Core does and as satd's other fail-stops do. A test build panics instead so the path can be tested. The Operator Manual says what the stop looks like and what to do after it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * signet: rewrite the commitment section exactly as Core does A signet solution signs the block with the solution cut out of the coinbase witness commitment, so the bytes left behind are part of the signed message. satd rebuilt that script from rust-bitcoin's `instructions()`, which differs from Core's FetchAndClearCommitmentSection (signet.cpp:32-57) in two places: - A zero-length OP_PUSHDATA1/2/4 comes back as an empty push, which satd wrote as OP_0 (0x00). Core writes an op with no data back as its own opcode byte (`CScript << opcode`), 0x4c, 0x4d or 0x4e. - An op that does not parse made satd give up on the whole script and check the block as if it carried no solution. Core's GetOp loop stops at that op, keeps the section it found before it, and drops the rest from the rebuilt script. Either way the merkle root the solution signs differed from Core's, so satd refused blocks Core accepts and could accept, under a challenge an empty solution meets, a block whose solution Core refuses. The rewrite is now a port of Core's: GetScriptOp to read each op, the opcode byte for an op with no data, and AppendDataSize (the shortest length prefix) for a push with data, which Core also applies to pushes that used a longer prefix. The expected rewrites in the tests are Core's own output: the function from signet.cpp, compiled with Core's script.cpp, run over each input. Core's signet_parse_tests are ported too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Take the median time past over a whole window, or fail the connect The connect paths read the median time past through the height index (or a replay plan) and skipped any height whose block they could not resolve, taking the median of the rest; with nothing resolved they used 0. Core's GetMedianTimePast (chain.h) walks the block and up to ten ancestors through pprev and has fewer only near genesis, so it never takes a median over a window with a hole. A shorter window moved BIP 113 and BIP 68 decisions, and an empty one met every BIP 68 time lock. A refusal it caused was then recorded against the block as invalid. A window with a missing block is now ConnectError::MedianTimeWindowGap, which is not a verdict on the block: the connect fails, nothing is marked, and the block connects once the index is whole. Every connect path uses it: accept_block and both reorg loops, the IBD connector, the reindex paths, the background chainstate and TestBlockValidity (through ChainState::median_time_past_for_connect, which still answers from the MTP cache first). The prefetcher hands such a block to the direct read. The mempool, templates and RPCs keep the old lenient get_median_time_past. BIP 68 now takes the coin's median as Core's CalculateSequenceLocks does (consensus/tx_verify.cpp:74), from the block before the coin's, GetAncestor(max(nCoinHeight - 1, 0)). That only differs for a coin at height 0, which the genesis coinbase never creates. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Count block work as Core's GetBitsProof does work_for_bits decoded nBits with target_from_compact, which ignores the sign bit and has no notion of overflow, and gave a zero target 2^256 - 1 work. Core's GetBitsProof (chain.cpp:121-134) gives no work to bits that SetCompact (arith_uint256.cpp:176-194) flags as negative or overflowing, or that decode to zero, and decodes exponents 33 and 34 differently from target_from_compact. work_for_bits now decodes as SetCompact does and returns zero work in those cases. For every target a header's proof of work can meet the value is unchanged, so stored chainwork stays valid; a test checks that over exponents 1 to 32. add_u256 wrapped on overflow. Chainwork is a sum of block work, and a sum that wrapped would rank the chain with the most work below every other, so it now saturates. Core's arith_uint256 wraps, but no reachable chain gets near 2^256. The expected values in the tests are Core's own output: GetBitsProof from chain.cpp, compiled with Core's arith_uint256.cpp, run over each nBits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Manual: an unreadable coin stops the offline audit too satd-chainstate-audit looks coins up through the same store, so a coin the database cannot read now stops it with the outpoint and the RocksDB error instead of listing the coin as missing. Say so beside its exit statuses, and say where the stop applies. 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 b311bd1)
…e header tip (#957) * Retry the best valid chain after a failed connect, and recalculate the header tip When a block fails to connect, Core's InvalidBlockFound marks it BLOCK_FAILED_VALID whichever path reached ConnectTip, and InvalidChainFound recalculates the header tip when that tip descends from the failed block (validation.cpp). satd did neither consistently: - mark_subtree_invalid now recalculates the header tip when the marked subtree contains it, and clears the activate_best_stored_chain attempt key. When the failed reorg was that function's own, no part of the key changed, so the stored prefix below the failed block was not tried until another block or header arrived. - The connector's connect_stored_block and connect_preprocessed_block mark a stored block that fails a consensus rule, as accept_block does. A failure that reports this node's storage (such as a gap in the median-time-past window) is not a verdict and still leaves the block DataStored. - The IBD connector leaves IBD when the block it connects is marked, instead of retrying it and restarting IBD onto it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Test the connector's verdict filter directly 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 b296fb4)
… blocks (#958) * Tests: index serving gate, backfill reorg cleanup, flush window, coin-free batches Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Tests: flush the live rows before planting backfill rows Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Index serving gate, backfill reorg safety, flush window, coin-free batches - The filter, address and spend indexes now serve only while their backfill cursor is Idle or Completed, as the silent-payment index already does. The "complete" marker stays set while a later backfill runs over the same index and after one stops part-way. The filter gate also drives the NODE_COMPACT_FILTERS service bit. - The filter and address backfills check their anchor and write each batch under accept_lock, so a reorg can no longer land between the check and the write and leave rows for a disconnected block. The filter tail catch-up re-walks from the new tip after a reorg instead of writing a row for a replaced block. - Reorg cleanup after an invalidated run no longer removes rows the active chain owns: filter rows at heights the active chain still has, and address funding/spending/spend rows the replacement blocks wrote under the same ordinal keys. - CoinCache::flush_inner holds the pending-batch lock from the take until the backing-store write returns (and, on failure, until the rows are back), so a read during a flush finds each row in pending or in the store, never in neither. - A coin-free batch that moves the tip (a block whose outputs are all unspendable and which spends nothing) is buffered like any other connect or disconnect, so a reorg's pending removes no longer delete its rows and a reorg abort retracts them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Tests: build the cursor batch with a struct initializer Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Push status updates refused during an address backfill once it ends While the address backfill cursor is not Idle or Completed, the address index refuses reads, so a status recompute triggered by a block or a mempool transaction in that time cannot run. Record the refusal and have the notifier recompute every subscription once serving resumes, so a subscriber whose history changed meanwhile is told. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Regtest: address history is refused while the backfill is Failed; check rows after the re-run 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 43fede2)
dump_mempool wrote mempool.dat in the pool's map order, and load_mempool re-admitted records in order of their one-second entry time, keeping file order on a tie. A child that entered in the same second as its parent was often tried first, refused for missing inputs, and its descendants after it, so a restart could drop most of the saved mempool. Bitcoin Core writes the file from infoAll(), which is in topological order (src/txmempool.cpp, GetSortedScoreWithTopology), and LoadMempool admits in file order (src/node/mempool_persist.cpp). satd now writes the file oldest-first with every parent ahead of its children, and on load runs the same stable dependency pass over the records after the time sort, so a file written by 0.6.0 in arbitrary order also reloads in full. The pass is one depth-first walk over the in-file spend edges, linear in inputs. The file format is unchanged. Fixes #949. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit 1899cf2)
) * chain: index the header of a block stored before its parent (#922) A block passed to `submitblock` before its parent's block was stored by `accept_block` without moving the best header. The stored-tail drain connects stored blocks along the best-header chain, so once the parent connected nothing reached the child: it stayed `valid-headers` and `getblockchaininfo.headers` did not count it until another block or header arrived. Core's `AcceptBlock` indexes every block's header through `AcceptBlockHeader` (src/validation.cpp:4307), and `BlockManager::AddToBlockIndex` raises `m_best_header` to an entry with more work (src/node/blockstorage.cpp:249). The stored path of `accept_block` now commits its index entry through the header path (`commit_accepted_headers_locked`), so the best header, the header height and the height rows above the tip follow it; the drain connects the child as soon as its parent connects. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs: changelog and 0.6.1 notes for #922 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 4484846)
…rs after IBD (#894, #895) (#962) * net: one automatic outbound peer per network group; trim outbound peers after IBD Automatic outbound dials now skip a candidate whose network group (the /16, the /32, or the ASN under -asmap) already holds a manual, full-relay or block-relay-only peer, or a dial in flight, as Core's ThreadOpenConnections requires (src/net.cpp:2654-2690, :2829). Addresses that are not publicly routable, which reach the dial list on regtest only, are outside the rule, as are onion peers. When the outbound full-relay target drops from the IBD figure back to 8, the surplus is disconnected one peer per reconnect pass, modelled on Core's EvictExtraOutboundPeers (src/net_processing.cpp:5264): the peer that least recently announced a block with more work than the tip, never a manual or block-relay-only peer, a chain-sync-protected peer or the only peer on its network, and not while it has blocks in flight or has been connected under 30 seconds. A peer sharing its group with another goes first. Headers now record each peer's last block announcement and protect the first four outbound full-relay peers whose chain has the tip's work, as Core's UpdatePeerStateForReceivedHeaders does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Notes: one outbound peer per network group; outbound peers trimmed after IBD (#894, #895) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * net: clippy question_mark in evict_extra_outbound_peer Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * net: dial DNS and fixed seeds through the automatic dial pass Seed answers were each dialled by a task of their own (connect_peer_addr_automatic), which checked neither the outbound target nor the one-peer-per-network-group rule, so a seed answering with several addresses in one /16 could fill the outbound slots with them. They now join the learned addresses and are dialled by the same pass as every other automatic connection (PeerManager::dial_learned_peers), as Core adds a seed's answers to its address book and leaves the dialling to ThreadOpenConnections. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * net: collect outbound groups under the dial list's lock A dial registers its peer before it leaves pending_connections. Reading the peers and then the dial list in two separate steps could miss a dial that completed between them, and let a second automatic dial into its group. The dial list is now read first and its lock held while the peers are read, the order connect_outbound_inner takes them in. 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 7b04ff4)
…963) * index: a filter backfill row replaces the pending live row for its height The filter backfill (main walk and tail catch-up) writes its rows through CoinCache's pass-through branch straight to the backing store. The live connect path's rows for recent blocks sit in the pending batch until the next flush. Where both held a row for the same height, the pending one shadowed the backfill's in reads and the flush wrote it back over it. Those live rows were emitted before the rows below them existed, so their headers chained off the all-zero fallback, and every later live header chained off them: the stored BIP 157 header chain broke from the first such height on. The pass-through branch now drops the pending filter rows its batch supersedes, last-writer-wins by (filter_type, height), as it already does for block-index rows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * index: check the stored filter header chain once and repair it A datadir on which an earlier release ran a filter backfill can hold filter headers chained off zeros. The filters themselves are correct (a filter depends only on its block), so the headers can be recomputed from them exactly: header[h] = sha256d(sha256d(filter[h]) || header[h-1]). On start, before resuming or starting any backfill, the filter supervisor walks the stored rows from genesis to the first gap or the tip, in chunks under accept_lock, and rewrites each header that differs. It runs once per datadir (block_filter_index.header_chain_checked), and not at all where no filter backfill has ever run (cursor Idle): there every row came from in-order live connects. With the index off it waits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Changelog and 0.6.1 notes: filter backfill header chain Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Tests: build the cursor batch in its initializer (clippy) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * storage: drop superseded pending filter rows only after the write succeeds A failed pass-through write used to have already removed the pending live rows it would have superseded, so the next flush stored the block without them. `pending` is now held across the backing write, as `flush_inner` holds it, and the superseded rows are dropped only once the write returned Ok. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * index: name the current CoinCache helper in the header chain doc 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 ab5a73b)
bkeroack
added a commit
that referenced
this pull request
Oct 15, 2026
Brings the 0.6.1 release notes, the 0.6.1 CHANGELOG releases-table row and the release-notes index row from release/0.6, as the 0.6.1 fixes reached master in #964. Master's [Unreleased] section (0.7.0) is unchanged. 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.
Brings the 0.6.1 fixes from
release/0.6tomaster: 24 commits, each cherry-picked with-xand keeping its original subject, in the order they landed onrelease/0.6. #959 (3067a487) is not ported because it only touches the release notes. Everything up to33902a46was already onmaster(#921, #925, #926, #928, #934).Please merge this PR with a rebase-merge, not a squash. That keeps one commit per original with its
(cherry picked from commit …)line.Release notes
The
CHANGELOG.mdbullets and thedocs/release-notes/0.6.1-pre.mdsections stay onrelease/0.6, as in #921, #926, #928 and #934. They will reachmasterwith the 0.6.1 cut. Each pick that touched those paths keepsmaster's version of them. No ported change contradictsdocs/release-notes/0.7.0-pre.md, so this PR leaves that file alone. TheCORE_DIFFERENCES.mdand Operator Manual changes are ported.Patch identity
For each pair, I compared the
git patch-id --stableof the original and of the pick, over every path exceptCHANGELOG.mdanddocs/release-notes/:release/0.66be5669a#936012f9e9fe431f4ff#941dc00f7ed104b5b00#945b8b94de0c78d0963#9373a359c5f76cd867b#938c5934964b66aa269#93927a6cce27ff0c67b#9337a9d9586aa95d9f6#9409ef914a42958b919#9431590bb40c50f8ab6#94752b670f8e2e40ca0#944d0fda70077e7fbd7#952a150719a6a18e0f3#9531c517f89ee057982#95479607f7197298b0d#942896f670dfbad3bd1#9465904571cb5a18d42#9556723d87db311bd1e#948a12b2859b296fb44#9573bbd00dc43fede28#958a7ef9cc01899cf22#9604ff8b62444848468#96105703c577b04ff43#9628931a246ab5a73b8#963bcba9bebAll four differences come from one source. On
master, the Core-compatible ZMQ work (#929) gave every mempool removal a sequence number,Mempool::accept_transaction_bypass_feenow returns(Txid, u64), and #935 added aleadargument to the admission path.release/0.6has none of these.node/src/mempool/pool_sigops_tests.rs: the new fee-bypass sigop test destructures(txid, _)fromaccept_transaction_bypass_fee. No production code differs, and the commit applied without conflicts.node/src/index/address/notifier_electrumbounds_tests.rs: theLeaveEvictedthe test builds now setsmempool_sequence: 0. No production code differs, and the commit applied without conflicts.node/src/mempool/pool.rs(conflict) andpool_fullpool_tests.rs:evict_lowest_fee_entries_except) returns(txid, scope, sequence)instead of(txid, scope).emit_evictedputs the sequence on each acting entry'sLeaveEvicted.evict_lowest_fee_entrieskeepsmaster's(txid, sequence)return type for its existing callers: the policy reload and tests.master's order: theleadevents (Announce a package's ephemeral-dust parent before its child #935) first, then the evictions, then the replacements.-maxmempoolreload trims the pool. Only acting entries get aLeaveEvicted.master's RBF path, a held entry takes a sequence number but emits nothing.LeaveEvictedpattern gains.., and twoevict_lowest_fee_entriescalls map(txid, _).node/src/mempool/pool.rs(conflict) andpool_mempoolrpc_tests.rs:accept_transaction_unexpiredtakes bothmaster'sleadand the fix'sentry_time.accept_transaction_ledpassesNone, and the newaccept_persisted_transactionpassesVec::new()and the persisted time.#[allow(clippy::too_many_arguments)]with a comment.remove_for_blocknow also removes a block conflict's descendants. Each one records the sequence number its removal took, asmasteralready did for the direct conflict.remove_expiredkeepsmaster's(txid, sequence)list and adds the fix's cutoff and time floor.LeaveEvictedpattern gains...The other 20 picks applied cleanly, apart from the release-notes paths.
Code that exists only on
masterI compared
release/0.6with this branch, leaving out the release notes. The only code that differs ismaster's own work: ZMQ (#929), the ephemeral-dust parent announcement (#935) and the version bump.node/src/net/,node/src/storage/,node/src/index/,node/src/chain/(apart from one test and the chain-event channel capacity) and every RPC handler apart fromgetzmqnotificationsare byte-identical torelease/0.6. So:masterhas no dial path thatrelease/0.6lacks, so nothing more was needed.masteradds no store writer, so nothing more was needed.masteradds isgetzmqnotifications. It returns strings and an integerhwm, so no change was needed.count_sigopsortotal_sigop_costis left outsidevalidation::sigops. Announce a package's ephemeral-dust parent before its child #935's package path admits through the sharedspend_recordhelper.accept_transaction_led. A dust parent is an ancestor of its child, so eviction never takes it.Follow-up left out of scope
The sequence numbers mentioned above predate this PR and are not changed here. On
master, a quarantined entry that leaves the pool still takes a mempool sequence number with no event. That happens in RBF, in admission eviction, and now also for a held descendant of an evicted or block-conflicting entry. AsequenceZMQ subscriber sees that as a gap. Whether held entries should take numbers at all is a separate question for the ZMQ design.Verified
Ran locally on this branch:
cargo clippy --all-targets --all-features -- -D warningscargo build --tests --features e2ecargo test -p node --lib: 2437 passednodeintegration testsfeature_block_consensusandrpc_auth_failure;consensus,electrum-proto,esplora-handlers,node-index,sat-cli,satd-authand thesatdbinariessatdtargetssubmitblock_order,p2p_addr_dials,p2p_limits,p2p_punish,mempool_rpc,local_hardening,rpc_parity,compact_blocksandzmq: all passedsatd --test regtest, filtered tofilter,cfheader,mempool,reload,address_index,reorgandsubmitblock: 72 passed and 11 failed. All 11 failures aretest_address_index_backfill_*.backfillindexrefused to start because/tmphad about 79 GB free and it needs 80 GB. That limit only exists on the local machine, and CI runs these tests.satd --features e2e --test e2e, filtered toelectrumandesplora: 43 passedOn
release/0.6, every original passed CI and a Devin review before it merged.🤖 Generated with Claude Code