Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ Prefer **one high-level scenario** per behavior cluster. Delete lower-level test
| `three_stage_confirm_and_parent_pin_surface` | Consensus+query | Split load鈫抯cripts鈫抴rite of pad+spend from genesis (header-plan BIP68 MTP); parent pin; load ready timeout/cancel; instance-owned `last_write` / `last_pin` / `take_window` meters (a second engine's window stays empty); txstat fee / size / weight from the load assemble, restamp and its refuses; same-run create then spend; 546-shaped 2-vout merge + same-block chain + cross-batch head resolve; an already-at-height retry finishes a spend annotate |
| `mempool_under_pressure` | Mempool + RPC (crate) | One entry in `orphanage`, `accept`, `tx_relay`, and `methods_tests`: orphan reserve and expiry, sigops before script, rolling fee floor, cluster cap, parked min-relay orphan, and the package RPC rejects (unsorted, missing inputs, conflict, min-relay parent with maxfeerate child). |
| `mempool_accept_life` | Mempool (crate) | One `ActiveMempool` against one chain view that blocks move. Sigop-adjusted vsize (boundary, min relay, full-pool floor, RBF, package and 1p1c), the raw-weight cluster limit, the shared block sigop budget, and sigop cost plus bytes-per-sigop and reserve overlays across reopen and compact. Orphan parks and re-announce, dry run not parked, parent promotes the child; missing vout, invalid parent, and block-spent coin reject without parking. Full RBF and no return, the staged commit failing closed on a conflict that landed after prepare, pure RBFR unpinning a child, replaced txs out of the cluster count; a ~30 kvB single tx under the vsize cap and the ten-way merge over it. Package order, CPFP, child fail restoring the RBF victim. A block evicts double-spent txs with descendants; a reorg readmits the parent and evicts the BIP68 and coinbase-maturity spends. Raised `-minrelaytxfee`: 1p1c needs a paying child, an unrelated tx does not ride the waiver, child fail takes a promoted spender down. Full pool: a protected lone worst chunk evicts nothing and leaves the floor, the next arrival evicts the CPFP pair together. |
| `rpc_regtest_chain_ops` | RPC (crate) | One regtest hub from genesis through `dispatch`. At genesis: `size_on_disk` is the store walk, IBD comes from the hub and not the stale atomic, buried deployments, `generateblock submit=false` connects nothing, and the priority, mocktime, mockscheduler, submitheader decode, and not-found refuses. The first block pays a p2wpkh address: display-order hashes and txids, raw `getblock` and header, a headers-only child at progress 0.5. Mocktime stamps `generate` and makes a far block `time-too-new`. Coinbase-only blocks: verbosity 1 without a seqsigwit zip, `getnetworkhashps` over chainwork, the empty template and proposal needles, a `time-too-old` header, an invalid parent body that marks its branch, and the `submitblock` merkle, length, coinbase, duplicate, value, and missing-input rejects. After a 120-block pad: the `nblocks=0` window, GBT sigops (bare, P2SH, P2WSH), sigop-adjusted mempool vsize (`getmempoolentry`, package retry, `blockmintxfee`; weight and the Esplora/Electrum histogram stay raw) and a big-sigops cluster under the block budget, a non-DER spend with Core `reject-details`, deprioritise, `generateblock` reject shapes then parent-first mining, a premature coinbase, proposal spend/value/final needles against the chain, default and explicit `maxfeerate`, `testmempoolaccept` known vs mempool vs archived, invalidate and reconsider, and a parked sibling (held `getblock`, `preciousblock`). Last, a mainnet view of the same hub refuses the regtest-only methods and still takes `submitblock`. |
| `rpc_regtest_chain_ops` | RPC (crate) | One regtest hub from genesis through `dispatch`. At genesis: `size_on_disk` is the store walk, IBD comes from the hub and not the stale atomic, buried deployments, `generateblock submit=false` connects nothing, and the priority, mocktime, mockscheduler, submitheader decode, and not-found refuses. The first block pays a p2wpkh address: display-order hashes and txids, raw `getblock` and header, a headers-only child at progress 0.5. Mocktime stamps `generate` and makes a far block `time-too-new`. Coinbase-only blocks: verbosity 1 without a seqsigwit zip, `getnetworkhashps` over chainwork, the empty template and proposal needles, a `time-too-old` header, an invalid parent body that marks its branch, and the `submitblock` merkle, length, coinbase, duplicate, value, and missing-input rejects. After a 120-block pad: the `nblocks=0` window, GBT fee and sigops (bare, P2SH, P2WSH), sigop-adjusted mempool vsize (`getmempoolentry`, package retry, `blockmintxfee`; weight and the Esplora/Electrum histogram stay raw) and a big-sigops cluster under the block budget, a non-DER spend with Core `reject-details`, deprioritise, `generateblock` reject shapes then parent-first mining, a premature coinbase, proposal spend/value/final needles against the chain, default and explicit `maxfeerate`, `testmempoolaccept` known vs mempool vs archived, invalidate and reconsider, and a parked sibling (held `getblock`, `preciousblock`). Last, a mainnet view of the same hub refuses the regtest-only methods and still takes `submitblock`. |
| `block_cache_and_mempool_hub_surface` | Net | BlockCache locator/eviction + MempoolHub accept/remove/reorg on mature chain. `DEFAULT_BODY_DEPTH == 16` stays a unit. |
| `store_error_and_corrupt_paths` | Store | Error/corrupt surfaces |
| `store_table_header_and_idx_corrupt` | Store | Table header/head corrupt open |
Expand Down
7 changes: 7 additions & 0 deletions changelog.d/gbt-selection-meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Fixed

- **`getblocktemplate` fees come from the selection.** Each
transaction's `fee` and `sigops`, and the `coinbasevalue`, are read
under the same mempool lock that selected it. A transaction evicted
while the template was built no longer reports `fee: 0` and
understates `coinbasevalue`.
6 changes: 6 additions & 0 deletions changelog.d/sigop-cap-inclusive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fixed

- **Block sigop cap includes 80,000.** Admission and template selection
allow a running cost of exactly 80,000, with `--block-reserved-sigops`
counted in that total. A cost that would pass 80,000 is still
`bad-txns-too-many-sigops`.
34 changes: 20 additions & 14 deletions crates/rbitcoin-mempool/src/accept.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//! Single-tx accept: Libre policy + cluster limits + durable slot write.

use crate::error::MempoolError;
use crate::graph::{sigops_adjusted_weight, TxEntry, TxGraph, MAX_BLOCK_SIGOPS_COST};
use crate::graph::{
sigops_adjusted_weight, SelectBudget, Selected, TxEntry, TxGraph, MAX_BLOCK_SIGOPS_COST,
};
use crate::orphanage::Orphanage;
use crate::packed::VinAux;
use crate::store::Mempool;
Expand Down Expand Up @@ -344,7 +346,7 @@ fn block_fit_sigop_cost(
.map(|o| o.script_pubkey.as_bytes())
.collect();
let cost = rbitcoin_consensus::tx_sigop_cost(tx, &spks, true, true);
if reserved_sigops.saturating_add(cost) >= MAX_BLOCK_SIGOPS_COST {
if reserved_sigops.saturating_add(cost) > MAX_BLOCK_SIGOPS_COST {
return Err(AcceptError::TooManySigops { cost });
}
Ok(cost)
Expand Down Expand Up @@ -502,7 +504,7 @@ impl ActiveMempool {
self.graph.set_bytes_per_sigop(bytes_per_sigop);
}

/// Set the sigop reserve shared by admission and block-template selection.
/// Set the sigop reserve shared by admission and [`Self::template_budget`].
pub fn set_block_reserved_sigops(&mut self, reserved_sigops: u64) {
self.graph.set_block_reserved_sigops(reserved_sigops);
}
Expand Down Expand Up @@ -1824,23 +1826,27 @@ impl ActiveMempool {
.collect()
}

/// Mining-order live txs that fit in `max_weight_wu` (best chunks first).
pub fn select_block_txs(&self, max_weight_wu: u64) -> Vec<Transaction> {
self.select_block_txs_delta(max_weight_wu, 0, |_| 0)
/// This node's own block budget (GBT / `generate`): template weight and
/// the admission sigop reserve, with a `-blockmintxfee` floor.
pub fn template_budget(&self, min_sat_kvb: u64) -> SelectBudget {
SelectBudget {
max_weight_wu: TxGraph::template_tx_weight(),
reserved_sigops: self.graph.block_reserved_sigops(),
min_sat_kvb,
}
}

/// Like [`Self::select_block_txs`] with `prioritisetransaction` fee deltas
/// and a `-blockmintxfee` chunk floor (sat/kvB).
pub fn select_block_txs_delta(
/// Mining-order live txs that fit `budget` (best chunks first) with
/// `prioritisetransaction` deltas, each with its [`Selected`] meta.
pub fn select_block_template(
&self,
max_weight_wu: u64,
min_sat_kvb: u64,
budget: SelectBudget,
delta: impl Fn(Txid) -> i64,
) -> Vec<Transaction> {
) -> Vec<(Transaction, Selected)> {
self.graph
.select_block_txids_delta(max_weight_wu, min_sat_kvb, delta)
.select_block_template(budget, delta)
.into_iter()
.filter_map(|id| self.get_tx(&id).cloned())
.filter_map(|s| self.get_tx(&s.txid).map(|tx| (tx.clone(), s)))
.collect()
}
}
Expand Down
35 changes: 27 additions & 8 deletions crates/rbitcoin-mempool/src/accept_life_journey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -630,35 +630,54 @@ fn sigop_block_budget(life: &mut Life) {
.expect("79,520 fits beside the default reserve");
assert_eq!(
life.mp
.select_block_txs(TxGraph::template_tx_weight())
.select_block_template(life.mp.template_budget(0), |_| 0)
.len(),
1
);
life.mp
.remove_for_block(&[fits_default.compute_txid()])
.unwrap();
life.mp
.accept_tx(&exact_default_budget, &utxos, TIP_OK)
.expect("79,600 beside the default reserve is exactly 80,000");
assert_eq!(
life.mp
.select_block_template(life.mp.template_budget(0), |_| 0)
.len(),
1
);
life.mp
.remove_for_block(&[exact_default_budget.compute_txid()])
.unwrap();
assert!(matches!(
life.mp.accept_tx(&exact_default_budget, &utxos, TIP_OK),
Err(AcceptError::TooManySigops { cost: 79_600 })
life.mp
.accept_tx(&multisig_outputs_tx(op, 996), &utxos, TIP_OK),
Err(AcceptError::TooManySigops { cost: 79_680 })
));
life.mp.set_block_reserved_sigops(0);
let fits_without_reserve = multisig_outputs_tx(op, 999);
let exact_block = multisig_outputs_tx(op, 1000);
life.mp
.accept_tx(&fits_without_reserve, &utxos, TIP_OK)
.expect("79,920 fits when the template reserve is zero");
life.mp
.remove_for_block(&[fits_without_reserve.compute_txid()])
.unwrap();
life.mp
.accept_tx(&exact_block, &utxos, TIP_OK)
.expect("80,000 fits when the template reserve is zero");
assert_eq!(
life.mp
.select_block_txs(TxGraph::template_tx_weight())
.select_block_template(life.mp.template_budget(0), |_| 0)
.len(),
1
);
life.mp
.remove_for_block(&[fits_without_reserve.compute_txid()])
.remove_for_block(&[exact_block.compute_txid()])
.unwrap();
assert!(matches!(
life.mp
.accept_tx(&multisig_outputs_tx(op, 1000), &utxos, TIP_OK),
Err(AcceptError::TooManySigops { cost: 80_000 })
life.mp.accept_tx(&multisig_outputs_tx(op, 1001), &utxos, TIP_OK),
Err(AcceptError::TooManySigops { cost: 80_080 })
));
assert_eq!(life.mp.live_count(), 0);
restore_sigop_knobs(&mut life.mp);
Expand Down
Loading
Loading