Repository navigation
net: pre-0.8 review fixes - #967
Conversation
A later header that claims work without a valid proof of work must not rewind a synced tip. A heavier valid held branch can still become the tip. Co-authored-by: Cursor <cursoragent@cursor.com>
More total work still wins. Equal work then prefers the precious held branch over the earlier one. Co-authored-by: Cursor <cursoragent@cursor.com>
Checked addition replaces saturating and wrapping sums, so an output, input, or fee total past u64 is a money-range reject instead of a successful proposal. Co-authored-by: Cursor <cursoragent@cursor.com>
Proposal mode runs the block script flags on prevouts the fee walk already resolved, after the coinbase amount check. Co-authored-by: Cursor <cursoragent@cursor.com>
A saturating u64 multiply treats two overflowing sides as equal and can accept a replacement below 1.25x. Co-authored-by: Cursor <cursoragent@cursor.com>
A parent under the fee floor alone was evicted between package commits, so a package that fits answered mempool full. Co-authored-by: Cursor <cursoragent@cursor.com>
Rebuilding every unsealed ancestor to answer one hash walks the chain. A sealed parent still allows that one block to be built. Co-authored-by: Cursor <cursoragent@cursor.com>
94a3e93 to
a252f30
Compare
The trim now runs after every member is in. A member that does not survive it had already replaced its conflicts, and those conflicts have to come back. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Review of #967
The precious tie helper, the u128 RBFR compare, the checked proposal walk, and the deferred package trim all read correctly. The new journeys pin the bugs they fix. Two things should change before 0.8.0: one in the header check and one in getblockfilter.
Should fix
-
A store error in the new header check marks a held block permanently invalid.
validate_headerreads the store.header_rejectturnsConsensusError::StoreintoNetError::Store, andaccept_branch_check_headersthen wraps every error asConnectFailed { hash, .. }. Inremember_failed_accept, any error that carries a block hash goes tonote_invalid_block. So one transient fault (SQ backpressure, aStaleread, a cancel) blacklists a valid side block until restart orreconsiderblock. Wrap onlyNetError::Consensusand return other errors unchanged. (inline) -
getblockfilternow refuses a stale branch more than one block deep, even when the index is fully caught up. A stale block's parent is not on the best chain, soparent_basic_filter_headerrejects it. Core'srpc_getblockfilter.pycallsgetblockfilteron everygetchaintipsentry, including a multi-block stale fork. Thecore-functionaljob was skipped on this PR, so CI did not see this. The plan only asked to refuse an unsealed best-chain gap. Suggested fix: walk a stale branch back to its fork point, and refuse only when that fork point's filter header is not sealed (that is the bounded walk). The stale-block assertion that was deleted fromgetblockfilter_rebuilds_unsealed_and_chains_from_a_sealed_parentshould come back in that form.- These still describe the old behavior:
changelog.d/core-functional-closer.md(the same release says the opposite of the new fragment),docs/rpc.mdlines 59 and 161,docs/operator/operations.mdline 94, anddocs/wallets.mdline 148. - Error text: Core says
Filter not found. Block filters are still in the process of being indexed., and/rest/blockfilter/already uses that string.Index is not caught upis new text that neither Core nor this repo used before.
- These still describe the old behavior:
Worth doing
-
getblockfilterdoes all the expensive work before it refuses. For an unsealed best-chain height,basic_filter_for_hashrebuilds the block and reads every spent prevout. Only then does it learn the parent is unsealed. Each refused call still costs a full block decode plus prevout reads. Check the parent header first. (inline) -
A proposal still skips the P2SH and witness sigop cost.
validate_block_structurecounts only legacy sigops. With prevouts now in hand,tx_sigop_cost(tx, prev_spks, bip16, witness)can enforce the 80 000 limit in the same loop (bad-blk-sigops), so a proposal over the limit stops reporting valid. (inline) -
The 1p1c rollback after trim has no test. The
accept_packagebranch is covered: thefailed_replacement_keeps_*package cases failed without it. Nothing reaches the newadmit_1p1cbranch where the trim drops the parent or child androllback_package_accepted(&[parent_res, r])runs. The repo rule is a failing test before any production change. (inline)
Notes, no change needed
- The trim is still not atomic with the package. The write lock is released between member commits, so a concurrent single admit (
defer_trim: false) can trim between them. The window is much smaller than before, but it is not closed. - The proposal money strings fire on
u64overflow, not onMAX_MONEYas Core'sMoneyRangedoes, and Core's accumulated-fee reason isbad-txns-accumulated-fee-outofrange. Neither case is reachable with real coins, so this only matters for string parity. try_apply_after_invalidateandbest_held_branchnow shareheld_branch_beats, and the precious-missing case (seq = u64::MAX) is handled by checking the precious bit first. Looks right.
| validate_header_on_parent(&self.params, Height(height), &b.header, mtp, expected) | ||
| .map_err(|e| header_reject(&b.header, &e)) | ||
| }; | ||
| if let Err(e) = checked { |
There was a problem hiding this comment.
header_reject can return NetError::Store (validate_header reads header_at_height and runs the median-time-past walk against the store). Wrapping it as ConnectFailed { hash, .. } makes remember_failed_accept call note_invalid_block(hash), which permanently invalidates a valid block on a transient IO fault. Suggest:
if let Err(e) = checked {
return Err(match e {
NetError::Consensus(msg) => NetError::ConnectFailed {
hash: b.block_hash().to_byte_array(),
msg,
},
other => other,
});
}| .ok_or(StoreError::Corrupt( | ||
| "invariant: blockfilter parent body missing", | ||
| )) | ||
| Err(StoreError::Rejected("Index is not caught up")) |
There was a problem hiding this comment.
This also refuses a stale block whose parent is stale. A two-block stale fork can never be served, even with the index caught up, which breaks Core rpc_getblockfilter.py (it queries every chaintip). Suggest walking back to the fork point for blocks that are not on the best chain, and refusing only when that fork point's header is not sealed. Core's text for this case is Filter not found. Block filters are still in the process of being indexed. (/rest/blockfilter/ already uses it).
| /// its Class A body only when the previous filter header is already | ||
| /// sealed (the zero header, for genesis). An unsealed best-chain gap is | ||
| /// refused instead of rebuilt back to genesis. | ||
| pub fn basic_filter_for_hash( |
There was a problem hiding this comment.
In this function, the unsealed best-chain arm (reconstruct_block_at_height, then spent_scripts reading every prevout in basic_filter_from_wire_block) runs before parent_basic_filter_header refuses. Look up the parent's sealed header first, so a refused call does not pay for a whole-block rebuild.
| &block.block_hash().to_byte_array(), | ||
| mtp, | ||
| ); | ||
| for (tx, ins) in block.txdata.iter().skip(1).zip(prevouts) { |
There was a problem hiding this comment.
With ins in hand, this loop can also add tx_sigop_cost(tx, &prev_spks, bip16, witness) and reject past 80 000 with bad-blk-sigops. Structure only counts legacy sigops, so a proposal over the limit through P2SH or witness sigops still reports valid.
| Ok(r) => { | ||
| self.trim_over_budget(); | ||
| if !self.try_contains(&parent_res.txid) || !self.try_contains(&r.txid) { | ||
| self.rollback_package_accepted(&[parent_res, r]); |
There was a problem hiding this comment.
No test reaches this branch, where the deferred trim drops the parent or child of a 1p1c and the conflicts it replaced are restored. The accept_package twin is pinned by the failed_replacement_keeps_* package cases. A 1p1c case in one of those journeys, through the orphan path, would cover this one.
A store fault during the held-branch header check no longer marks the block invalid. getblockfilter walks a stale branch only back to a sealed fork point and uses Core's not-indexed error. A proposal counts P2SH and witness sigops, and a 1p1c trim that drops the package restores the conflict it replaced. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed in c12b688.
Left as noted: the package trim is still not atomic with a concurrent single admit, and the proposal money strings stay on |
Plan D no longer stacks on sv2/plan-c-fee-push (merged as reardencode#949 and reardencode#951) and its D1 step is upstream: reardencode#959 moved the proposal check onto ChainHub, reardencode#963, reardencode#967, reardencode#968, and reardencode#969 made it complete, one decode per parent, bounded, and right about spent coins after a reorg. Name those as the prerequisites and why: the check is the job-validation hot path. Drop the two risks they closed (no script execution, reardencode#967; the per-input parent decode, reardencode#968), name both ends of the tip-change straddle the D11 journey now accepts, fix the "no scripts" CPU-trade lines, and remove the getblocktemplate bad-cb-amount changelog entry that reardencode#959 already carries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Plan D no longer stacks on sv2/plan-c-fee-push (merged as reardencode#949 and reardencode#951) and its D1 step is upstream: reardencode#959 moved the proposal check onto ChainHub, reardencode#963, reardencode#967, reardencode#968, and reardencode#969 made it complete, one decode per parent, bounded, and right about spent coins after a reorg. Name those as the prerequisites and why: the check is the job-validation hot path. Drop the two risks they closed (no script execution, reardencode#967; the per-input parent decode, reardencode#968), name both ends of the tip-change straddle the D11 journey now accepts, fix the "no scripts" CPU-trade lines, and remove the getblocktemplate bad-cb-amount changelog entry that reardencode#959 already carries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Summary
invalidateblockuses the same equal-work tie as an ordinary reorg: more work, thenpreciousblock, then the earlier held tip.u128.getblockfilterrefuses an unsealed best-chain gap. A sealed parent still allows that one block to be built.Test plan
reorg_same_height_then_multi_block_branch(fake-work side branch, precious invalidate)getblockfilterunsealed gap