Skip to content

Commit 9cf32a0

Browse files
mempool: make the local-orphan capacity configurable
Address OCR findings on PR #2126: - local_orphan_pool_capacity becomes a MempoolConfig setting (default DEFAULT_LOCAL_ORPHAN_POOL_CAPACITY = 25), exposed through the mempool RPC config and the node config file, like the sibling limits; validated to be non-zero in MempoolConfig::validate; - OrphanPoolError::LocalCapacityExceeded carries the configured limit, mirroring TooLarge; - the on_new_tip work-queue drain is bounded to 64 work units per event to keep per-event latency bounded on deep reorgs; the rest of the queue is processed by the round-based scheduler.
1 parent 485ca5f commit 9cf32a0

13 files changed

Lines changed: 103 additions & 14 deletions

File tree

‎blockprod/src/detail/tests/produce_block/tx_selection_by_deps.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ async fn token_account_deps(#[case] seed: Seed, fees_selection: FeesSelection) {
119119
.with_chain_config(Arc::clone(&chain_config))
120120
.with_mempool_config(MempoolConfig {
121121
allow_local_orphans: Default::default(),
122+
local_orphan_pool_capacity: Default::default(),
122123
min_tx_relay_fee_rate: FeeRate::from_amount_per_kb(Amount::ZERO).into(),
123124
max_cluster_tx_count: Default::default(),
124125
max_cluster_size_bytes: Default::default(),
@@ -578,6 +579,7 @@ async fn pool_creation_and_decommissioning(#[case] seed: Seed) {
578579
.with_chain_config(Arc::clone(&chain_config))
579580
.with_mempool_config(MempoolConfig {
580581
allow_local_orphans: Default::default(),
582+
local_orphan_pool_capacity: Default::default(),
581583
min_tx_relay_fee_rate: FeeRate::from_amount_per_kb(Amount::ZERO).into(),
582584
max_cluster_tx_count: Default::default(),
583585
max_cluster_size_bytes: Default::default(),
@@ -709,6 +711,7 @@ async fn pool_creation_and_delegation(#[case] seed: Seed, fees_selection: FeesSe
709711
.with_chain_config(Arc::clone(&chain_config))
710712
.with_mempool_config(MempoolConfig {
711713
allow_local_orphans: Default::default(),
714+
local_orphan_pool_capacity: Default::default(),
712715
min_tx_relay_fee_rate: FeeRate::from_amount_per_kb(Amount::ZERO).into(),
713716
max_cluster_tx_count: Default::default(),
714717
max_cluster_size_bytes: Default::default(),
@@ -934,6 +937,7 @@ async fn pool_creation_and_delegation_withdrawals(
934937
.with_chain_config(Arc::clone(&chain_config))
935938
.with_mempool_config(MempoolConfig {
936939
allow_local_orphans: Default::default(),
940+
local_orphan_pool_capacity: Default::default(),
937941
min_tx_relay_fee_rate: FeeRate::from_amount_per_kb(Amount::ZERO).into(),
938942
max_cluster_tx_count: Default::default(),
939943
max_cluster_size_bytes: Default::default(),
@@ -1098,6 +1102,7 @@ async fn order_creation_and_usage(
10981102
.with_chain_config(Arc::clone(&chain_config))
10991103
.with_mempool_config(MempoolConfig {
11001104
allow_local_orphans: Default::default(),
1105+
local_orphan_pool_capacity: Default::default(),
11011106
min_tx_relay_fee_rate: FeeRate::from_amount_per_kb(Amount::ZERO).into(),
11021107
max_cluster_tx_count: Default::default(),
11031108
max_cluster_size_bytes: Default::default(),

‎mempool/src/config.rs‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,14 @@ make_config_setting!(
148148
make_config_setting!(MaxClusterTxCount, usize, 64);
149149
make_config_setting!(MaxClusterSizeBytes, usize, 100_000);
150150

151+
// Separate cap for local-origin orphans, so that they cannot starve remote peers'
152+
// orphans out of the main orphan pool when `AllowLocalOrphans` is enabled.
153+
make_config_setting!(
154+
LocalOrphanPoolCapacity,
155+
usize,
156+
DEFAULT_LOCAL_ORPHAN_POOL_CAPACITY
157+
);
158+
151159
// When disabled (the default), transactions submitted locally whose inputs are not yet known are
152160
// rejected outright. When enabled, they are parked in the orphan pool instead and re-processed
153161
// once their parents arrive (or are mined), same as remote transactions.
@@ -166,6 +174,9 @@ pub struct MempoolConfig {
166174

167175
/// Whether to park local-origin transactions with unknown inputs in the orphan pool.
168176
pub allow_local_orphans: AllowLocalOrphans,
177+
178+
/// Capacity for local-origin orphans within the orphan pool.
179+
pub local_orphan_pool_capacity: LocalOrphanPoolCapacity,
169180
}
170181

171182
impl MempoolConfig {
@@ -179,13 +190,15 @@ impl MempoolConfig {
179190
max_cluster_tx_count,
180191
max_cluster_size_bytes,
181192
allow_local_orphans,
193+
local_orphan_pool_capacity,
182194
} = self;
183195

184196
RpcMempoolConfig {
185197
min_tx_relay_fee_rate: **min_tx_relay_fee_rate,
186198
max_cluster_tx_count: **max_cluster_tx_count,
187199
max_cluster_size_bytes: **max_cluster_size_bytes,
188200
allow_local_orphans: **allow_local_orphans,
201+
local_orphan_pool_capacity: **local_orphan_pool_capacity,
189202
}
190203
}
191204

@@ -202,6 +215,17 @@ impl MempoolConfig {
202215
*self.max_cluster_size_bytes > 0,
203216
ConfigError::MaxClusterSizeBytesCannotBeZero
204217
);
218+
// Note: a value of 0 combined with `allow_local_orphans = true` would
219+
// silently disable local orphan parking, so we forbid it like the
220+
// other zero-valued limits. Also note there is no upper bound relative
221+
// to the overall orphan pool capacity
222+
// (`config::DEFAULT_ORPHAN_POOL_CAPACITY`): a value above it makes the
223+
// separate local cap non-binding, since the total pool limit evicts
224+
// without regard to origin.
225+
ensure!(
226+
*self.local_orphan_pool_capacity > 0,
227+
ConfigError::LocalOrphanPoolCapacityCannotBeZero
228+
);
205229
Ok(())
206230
}
207231
}
@@ -213,13 +237,15 @@ impl From<RpcMempoolConfig> for MempoolConfig {
213237
max_cluster_tx_count,
214238
max_cluster_size_bytes,
215239
allow_local_orphans,
240+
local_orphan_pool_capacity,
216241
} = value;
217242

218243
Self {
219244
min_tx_relay_fee_rate: min_tx_relay_fee_rate.into(),
220245
max_cluster_tx_count: max_cluster_tx_count.into(),
221246
max_cluster_size_bytes: max_cluster_size_bytes.into(),
222247
allow_local_orphans: allow_local_orphans.into(),
248+
local_orphan_pool_capacity: local_orphan_pool_capacity.into(),
223249
}
224250
}
225251
}
@@ -231,13 +257,21 @@ pub struct RpcMempoolConfig {
231257
pub max_cluster_tx_count: usize,
232258
pub max_cluster_size_bytes: usize,
233259
pub allow_local_orphans: bool,
260+
pub local_orphan_pool_capacity: usize,
234261
}
235262

263+
/// Mempool configuration errors.
264+
// The variant names intentionally share the `CannotBeZero` suffix: they all
265+
// describe zero-values that make no sense for the corresponding limit.
266+
#[allow(clippy::enum_variant_names)]
236267
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
237268
pub enum ConfigError {
238269
#[error("Maximum cluster transaction count cannot be zero")]
239270
MaxClusterTxCountCannotBeZero,
240271

272+
#[error("Local orphan pool capacity cannot be zero")]
273+
LocalOrphanPoolCapacityCannotBeZero,
274+
241275
#[error("Maximum cluster size in bytes cannot be zero")]
242276
MaxClusterSizeBytesCannotBeZero,
243277
}

‎mempool/src/error/mod.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,8 @@ pub enum OrphanPoolError {
205205
#[error("Orphan pool full")]
206206
Full,
207207

208-
#[error("Local orphan capacity exceeded")]
209-
LocalCapacityExceeded,
208+
#[error("Local orphan capacity exceeded (limit: {0})")]
209+
LocalCapacityExceeded(usize),
210210

211211
#[error("Account nonces too distant, gap: {0}")]
212212
NonceGapTooLarge(u64),

‎mempool/src/pool/mod.rs‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -550,8 +550,14 @@ impl<M: MemoryUsageEstimator + ShallowClone> Mempool<M> {
550550

551551
// The rebuild above may have enqueued orphans for reprocessing (e.g. parked
552552
// local-origin transactions whose parents have just been re-added from blocks).
553-
// Pump the work queue now so they don't have to wait for an external trigger.
554-
while self.has_work() {
553+
// Pump a bounded number of work units now so they don't have to wait for an
554+
// external trigger, while keeping per-event processing latency bounded; the
555+
// rest stays in the work queue for the round-based scheduler.
556+
const MAX_WORK_UNITS_PER_TIP: usize = 64;
557+
for _ in 0..MAX_WORK_UNITS_PER_TIP {
558+
if !self.has_work() {
559+
break;
560+
}
555561
self.perform_work_unit();
556562
}
557563
}
@@ -749,10 +755,11 @@ impl<'a> TxFinalizer<'a> {
749755
// Keep local-origin orphans from crowding out remote peers' orphans. Resubmissions
750756
// of already-parked transactions bypass the capacity check so that they keep
751757
// yielding the idempotent `InOrphanPoolDuplicate` status instead of an error.
758+
let local_capacity = *tx_pool.mempool_config().local_orphan_pool_capacity;
752759
if !self.orphan_pool.contains(transaction.tx_id())
753-
&& self.orphan_pool.local_len() >= config::DEFAULT_LOCAL_ORPHAN_POOL_CAPACITY
760+
&& self.orphan_pool.local_len() >= local_capacity
754761
{
755-
return Err(OrphanPoolError::LocalCapacityExceeded);
762+
return Err(OrphanPoolError::LocalCapacityExceeded(local_capacity));
756763
}
757764
}
758765

‎mempool/src/pool/tests/basic.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ async fn get_all_txs_in_insertion_order(#[case] seed: Seed) {
560560
.build();
561561
let mempool_config = MempoolConfig {
562562
allow_local_orphans: Default::default(),
563+
local_orphan_pool_capacity: Default::default(),
563564
min_tx_relay_fee_rate: FeeRate::from_amount_per_kb(Amount::ZERO).into(),
564565
max_cluster_tx_count: Default::default(),
565566
max_cluster_size_bytes: Default::default(),

‎mempool/src/pool/tests/orphans.rs‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ async fn transaction_graph_subset_permutation(#[case] seed: Seed) {
238238

239239
let mempool_config = MempoolConfig {
240240
allow_local_orphans: Default::default(),
241+
local_orphan_pool_capacity: Default::default(),
241242
min_tx_relay_fee_rate: TEST_MIN_TX_RELAY_FEE_RATE.into(),
242243
// Make sure we don't hit the max cluster tx count limit.
243244
max_cluster_tx_count: num_txs.into(),
@@ -464,15 +465,28 @@ fn make_parking_mempool(
464465
) -> (
465466
crate::pool::Mempool<crate::pool::memory_usage_estimator::StoreMemoryUsageEstimator>,
466467
Id<GenBlock>,
468+
) {
469+
make_parking_mempool_with_config(rng, |_| {})
470+
}
471+
472+
/// Same as [`make_parking_mempool`], but with local-orphan parking always
473+
/// enabled and the mempool config adjustable before the mempool is created.
474+
fn make_parking_mempool_with_config(
475+
rng: &mut impl CryptoRng,
476+
configure: impl FnOnce(&mut MempoolConfig),
477+
) -> (
478+
crate::pool::Mempool<crate::pool::memory_usage_estimator::StoreMemoryUsageEstimator>,
479+
Id<GenBlock>,
467480
) {
468481
let tf = TestFramework::builder(rng).build();
469-
let genesis_id = tf.genesis().get_id();
482+
let genesis_id = tf.genesis().get_id().into();
470483

471484
let mut config = create_mempool_config();
472485
config.allow_local_orphans = true.into();
486+
configure(&mut config);
473487

474488
let mempool = setup_with_chainstate_generic(tf.chainstate(), config, Default::default());
475-
(mempool, genesis_id.into())
489+
(mempool, genesis_id)
476490
}
477491

478492
#[rstest]
@@ -543,9 +557,12 @@ async fn local_orphan_reprocessed_when_parent_arrives(
543557
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
544558
async fn local_orphan_capacity_limit(#[case] seed: Seed) {
545559
let mut rng = make_seedable_rng(seed);
546-
let (mut mempool, genesis_id) = make_parking_mempool(&mut rng);
547-
548-
let local_capacity = crate::config::DEFAULT_LOCAL_ORPHAN_POOL_CAPACITY;
560+
// Use a non-default capacity so the test verifies that the configured
561+
// limit is propagated into the error, not just the compile-time default.
562+
let local_capacity = 5;
563+
let (mut mempool, genesis_id) = make_parking_mempool_with_config(&mut rng, |config| {
564+
config.local_orphan_pool_capacity = local_capacity.into();
565+
});
549566

550567
// A parent with `local_capacity + 1` outputs, so that each child conflicts with no other
551568
let tx0 = make_tx(
@@ -584,7 +601,9 @@ async fn local_orphan_capacity_limit(#[case] seed: Seed) {
584601
let res = mempool
585602
.add_transaction_with_origin(children[local_capacity].clone(), LocalTxOrigin::P2p.into());
586603
match res {
587-
Err(Error::Orphan(OrphanPoolError::LocalCapacityExceeded)) => {}
604+
Err(Error::Orphan(OrphanPoolError::LocalCapacityExceeded(limit))) => {
605+
assert_eq!(limit, local_capacity);
606+
}
588607
other => panic!("Expected LocalCapacityExceeded error, got {other:?}"),
589608
}
590609
}

‎mempool/src/pool/tests/relatives.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,7 @@ fn create_mempool(
832832
) -> MempoolType {
833833
let mempool_config = MempoolConfig {
834834
allow_local_orphans: Default::default(),
835+
local_orphan_pool_capacity: Default::default(),
835836
min_tx_relay_fee_rate: FeeRate::from_amount_per_kb(Amount::ZERO).into(),
836837
max_cluster_tx_count,
837838
max_cluster_size_bytes,

‎mempool/src/pool/tx_pool/tests/basic.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ async fn tx_bigger_than_cluster_size(#[case] seed: Seed) -> anyhow::Result<()> {
388388
let tx_size = tx.encoded_size();
389389
let mempool_config = MempoolConfig {
390390
allow_local_orphans: Default::default(),
391+
local_orphan_pool_capacity: Default::default(),
391392
min_tx_relay_fee_rate: TEST_MIN_TX_RELAY_FEE_RATE.into(),
392393
max_cluster_size_bytes: max_cluster_size.into(),
393394

@@ -1421,6 +1422,7 @@ async fn no_empty_bags_in_indices(#[case] seed: Seed) -> anyhow::Result<()> {
14211422

14221423
let mempool_config = MempoolConfig {
14231424
allow_local_orphans: Default::default(),
1425+
local_orphan_pool_capacity: Default::default(),
14241426
min_tx_relay_fee_rate: TEST_MIN_TX_RELAY_FEE_RATE.into(),
14251427
// Make sure we don't hit the max cluster tx count limit.
14261428
max_cluster_tx_count: (num_child_txs + 1).into(),
@@ -1557,6 +1559,7 @@ async fn accepted_tx_size(
15571559

15581560
let mempool_config = MempoolConfig {
15591561
allow_local_orphans: Default::default(),
1562+
local_orphan_pool_capacity: Default::default(),
15601563
min_tx_relay_fee_rate: TEST_MIN_TX_RELAY_FEE_RATE.into(),
15611564
max_cluster_size_bytes: max_cluster_size.into(),
15621565

@@ -1598,6 +1601,7 @@ async fn initial_values(#[case] seed: Seed) {
15981601

15991602
let mempool_config = MempoolConfig {
16001603
allow_local_orphans: Default::default(),
1604+
local_orphan_pool_capacity: Default::default(),
16011605
min_tx_relay_fee_rate: FeeRate::from_atoms_per_kb(
16021606
rng.random_range(100_000..100_000_000_000_000),
16031607
)
@@ -1679,6 +1683,7 @@ fn stack_overflow_on_transaction_addition(
16791683
tf.chainstate(),
16801684
MempoolConfig {
16811685
allow_local_orphans: Default::default(),
1686+
local_orphan_pool_capacity: Default::default(),
16821687
min_tx_relay_fee_rate: FeeRate::from_amount_per_kb(Amount::ZERO).into(),
16831688
max_cluster_size_bytes: usize::MAX.into(),
16841689
max_cluster_tx_count: max_cluster_tx_count.into(),
@@ -1840,6 +1845,7 @@ async fn storage_capacity_shrinkage(#[case] seed: Seed) {
18401845

18411846
let mempool_config = MempoolConfig {
18421847
allow_local_orphans: Default::default(),
1848+
local_orphan_pool_capacity: Default::default(),
18431849
min_tx_relay_fee_rate: min_tx_relay_fee_rate.into(),
18441850
// Make sure we don't hit the max cluster tx count limit.
18451851
max_cluster_tx_count: txs.len().into(),

‎mempool/src/pool/tx_pool/tests/tx_ids_by_score_and_ancestry.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ async fn tx_ids_by_score_and_ancestry(#[case] seed: Seed) {
5050
tf.chainstate(),
5151
MempoolConfig {
5252
allow_local_orphans: Default::default(),
53+
local_orphan_pool_capacity: Default::default(),
5354
min_tx_relay_fee_rate: FeeRate::from_amount_per_kb(Amount::ZERO).into(),
5455
max_cluster_tx_count: Default::default(),
5556
max_cluster_size_bytes: Default::default(),
@@ -218,6 +219,7 @@ fn stack_overflow_on_misconfigured_cluster_size_limit(#[case] seed: Seed) {
218219
tf.chainstate(),
219220
MempoolConfig {
220221
allow_local_orphans: Default::default(),
222+
local_orphan_pool_capacity: Default::default(),
221223
min_tx_relay_fee_rate: FeeRate::from_amount_per_kb(Amount::ZERO).into(),
222224
max_cluster_size_bytes: usize::MAX.into(),
223225
max_cluster_tx_count: usize::MAX.into(),

‎mempool/src/pool/tx_pool/tests/utils.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub fn create_mempool_config() -> MempoolConfig {
4848
max_cluster_tx_count: Default::default(),
4949
max_cluster_size_bytes: Default::default(),
5050
allow_local_orphans: false.into(),
51+
local_orphan_pool_capacity: Default::default(),
5152
}
5253
}
5354

@@ -385,6 +386,7 @@ pub fn setup_with_min_tx_relay_fee_rate(fee_rate: FeeRate) -> TxPool<StoreMemory
385386
max_cluster_tx_count: Default::default(),
386387
max_cluster_size_bytes: Default::default(),
387388
allow_local_orphans: false.into(),
389+
local_orphan_pool_capacity: Default::default(),
388390
};
389391
let chainstate_interface = start_chainstate_with_config(Arc::clone(&chain_config));
390392
TxPool::new(

0 commit comments

Comments
 (0)