From ceb057bc067591d5594a2c7697260e6f14acd49c Mon Sep 17 00:00:00 2001 From: blockchaindevsh Date: Sat, 11 Apr 2026 00:49:52 +0800 Subject: [PATCH 1/2] feat(txpool): add additional_balance_provider for effective balance computation Add an optional callback to the transaction pool that provides additional balance beyond native (e.g., SGT on OP Stack) for the ENOUGH_BALANCE check during pool maintenance and tx insertion. This allows L2 chains with alternative gas payment mechanisms to keep transactions in the pending pool. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/transaction-pool/src/lib.rs | 12 ++++++ crates/transaction-pool/src/pool/mod.rs | 12 ++++++ crates/transaction-pool/src/pool/txpool.rs | 43 +++++++++++++++++++--- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/crates/transaction-pool/src/lib.rs b/crates/transaction-pool/src/lib.rs index 2a216955ad0..48970f466fe 100644 --- a/crates/transaction-pool/src/lib.rs +++ b/crates/transaction-pool/src/lib.rs @@ -369,6 +369,18 @@ where &self.pool } + /// Sets an additional balance provider for the pool. + /// + /// The provider returns extra balance (beyond native) for a given address, + /// used during pool maintenance to compute effective balance for subpool + /// placement (e.g., SGT balance on OP Stack). + pub fn set_additional_balance_provider( + &self, + f: std::sync::Arc alloy_primitives::U256 + Send + Sync>, + ) { + self.inner().set_additional_balance_provider(f); + } + /// Get the config the pool was configured with. pub fn config(&self) -> &PoolConfig { self.inner().config() diff --git a/crates/transaction-pool/src/pool/mod.rs b/crates/transaction-pool/src/pool/mod.rs index e6114e7e361..80caf5191d0 100644 --- a/crates/transaction-pool/src/pool/mod.rs +++ b/crates/transaction-pool/src/pool/mod.rs @@ -544,6 +544,18 @@ where self.notify_on_transaction_updates(promoted, discarded); } + /// Sets an additional balance provider for the pool. + /// + /// The provider returns extra balance (beyond native) for a given address. + /// This is used during pool maintenance to compute effective balance for + /// subpool placement (e.g., SGT balance on OP Stack). + pub fn set_additional_balance_provider( + &self, + f: std::sync::Arc alloy_primitives::U256 + Send + Sync>, + ) { + self.pool.write().set_additional_balance_provider(f); + } + /// Add a single validated transaction into the pool. /// /// Returns the outcome and optionally metadata to be processed after the pool lock is diff --git a/crates/transaction-pool/src/pool/txpool.rs b/crates/transaction-pool/src/pool/txpool.rs index 40f3e2c64b8..b6ac95feadf 100644 --- a/crates/transaction-pool/src/pool/txpool.rs +++ b/crates/transaction-pool/src/pool/txpool.rs @@ -30,9 +30,7 @@ use alloy_eips::{ eip4844::BLOB_TX_MIN_BLOB_GASPRICE, Typed2718, }; -#[cfg(test)] -use alloy_primitives::Address; -use alloy_primitives::{map::AddressSet, TxHash, B256}; +use alloy_primitives::{map::AddressSet, Address, TxHash, B256}; use rustc_hash::FxHashMap; use smallvec::SmallVec; use std::{ @@ -137,6 +135,14 @@ impl TxPool { } } + /// Sets an additional balance provider for effective balance computation. + pub(crate) fn set_additional_balance_provider( + &mut self, + f: Arc U256 + Send + Sync>, + ) { + self.all_transactions.additional_balance_provider = Some(f); + } + /// Retrieves the highest nonce for a specific sender from the transaction pool. pub fn get_highest_nonce_by_sender(&self, sender: SenderId) -> Option { self.all().txs_iter(sender).last().map(|(_, tx)| tx.transaction.nonce()) @@ -1372,11 +1378,15 @@ pub(crate) struct AllTransactions { local_transactions_config: LocalTransactionConfig, /// All accounts with a pooled authorization auths: FxHashMap>, + /// Optional provider for additional balance beyond native (e.g., SGT). + /// Given an address, returns the additional balance to add to the native balance. + additional_balance_provider: Option U256 + Send + Sync>>, /// All Transactions metrics metrics: AllTransactionsMetrics, } impl AllTransactions { + /// Create a new instance fn new(config: &PoolConfig) -> Self { Self { @@ -1486,6 +1496,15 @@ impl AllTransactions { // pre-allocate a few updates let mut updates = Vec::with_capacity(64); + // Clone the additional balance provider before mutably borrowing txs + let additional_balance = self.additional_balance_provider.clone(); + let effective_bal = |native: &U256, addr: Address| -> U256 { + match &additional_balance { + Some(f) => native.saturating_add(f(addr)), + None => *native, + } + }; + let mut iter = self.txs.iter_mut().peekable(); // Loop over all individual senders and update all affected transactions. @@ -1528,7 +1547,8 @@ impl AllTransactions { tx.state.insert(TxState::NO_NONCE_GAPS); tx.state.insert(TxState::NO_PARKED_ANCESTORS); tx.cumulative_cost = U256::ZERO; - if tx.transaction.cost() > &info.balance { + let effective = effective_bal(&info.balance, tx.transaction.sender()); + if tx.transaction.cost() > &effective { // sender lacks sufficient funds to pay for this transaction tx.state.remove(TxState::ENOUGH_BALANCE); } else { @@ -1589,7 +1609,8 @@ impl AllTransactions { // If the account changed in the block, check the balance. if let Some(changed_balance) = changed_balance { - if &cumulative_cost > changed_balance { + let effective = effective_bal(changed_balance, tx.transaction.sender()); + if cumulative_cost > effective { // sender lacks sufficient funds to pay for this transaction tx.state.remove(TxState::ENOUGH_BALANCE); } else { @@ -1956,6 +1977,14 @@ impl AllTransactions { let mut cumulative_cost = U256::ZERO; let mut updates = Vec::new(); + let additional_balance = self.additional_balance_provider.clone(); + let effective_bal = |native: &U256, addr: Address| -> U256 { + match &additional_balance { + Some(f) => native.saturating_add(f(addr)), + None => *native, + } + }; + // Current tx does not exceed block gas limit after ensure_valid check state.insert(TxState::NOT_TOO_MUCH_GAS); @@ -2080,7 +2109,8 @@ impl AllTransactions { // Update for next transaction cumulative_cost = tx.next_cumulative_cost(); - if cumulative_cost > on_chain_balance { + let effective = effective_bal(&on_chain_balance, tx.transaction.sender()); + if cumulative_cost > effective { // sender lacks sufficient funds to pay for this transaction tx.state.remove(TxState::ENOUGH_BALANCE); } else { @@ -2171,6 +2201,7 @@ impl Default for AllTransactions { price_bumps: Default::default(), local_transactions_config: Default::default(), auths: Default::default(), + additional_balance_provider: None, metrics: Default::default(), } } From 4efc6c7141d7eeb75f62336a662bdbf86f139954 Mon Sep 17 00:00:00 2001 From: blockchaindevsh Date: Mon, 13 Apr 2026 20:58:51 +0800 Subject: [PATCH 2/2] fix(txpool): propagate additional_balance_provider errors instead of silently returning 0 Change the additional_balance_provider callback signature from `Fn(Address) -> U256` to `Fn(Address) -> Result>`. On error, warn-log and fall back to native-only balance instead of silently treating the balance as 0, which was indistinguishable from an account that truly has zero additional balance. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/transaction-pool/src/lib.rs | 2 +- crates/transaction-pool/src/pool/mod.rs | 2 +- crates/transaction-pool/src/pool/txpool.rs | 20 ++++++++++++++++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/transaction-pool/src/lib.rs b/crates/transaction-pool/src/lib.rs index 48970f466fe..d64714c5010 100644 --- a/crates/transaction-pool/src/lib.rs +++ b/crates/transaction-pool/src/lib.rs @@ -376,7 +376,7 @@ where /// placement (e.g., SGT balance on OP Stack). pub fn set_additional_balance_provider( &self, - f: std::sync::Arc alloy_primitives::U256 + Send + Sync>, + f: std::sync::Arc Result> + Send + Sync>, ) { self.inner().set_additional_balance_provider(f); } diff --git a/crates/transaction-pool/src/pool/mod.rs b/crates/transaction-pool/src/pool/mod.rs index 80caf5191d0..b8d17c8953c 100644 --- a/crates/transaction-pool/src/pool/mod.rs +++ b/crates/transaction-pool/src/pool/mod.rs @@ -551,7 +551,7 @@ where /// subpool placement (e.g., SGT balance on OP Stack). pub fn set_additional_balance_provider( &self, - f: std::sync::Arc alloy_primitives::U256 + Send + Sync>, + f: std::sync::Arc Result> + Send + Sync>, ) { self.pool.write().set_additional_balance_provider(f); } diff --git a/crates/transaction-pool/src/pool/txpool.rs b/crates/transaction-pool/src/pool/txpool.rs index b6ac95feadf..3669996cc36 100644 --- a/crates/transaction-pool/src/pool/txpool.rs +++ b/crates/transaction-pool/src/pool/txpool.rs @@ -138,7 +138,7 @@ impl TxPool { /// Sets an additional balance provider for effective balance computation. pub(crate) fn set_additional_balance_provider( &mut self, - f: Arc U256 + Send + Sync>, + f: Arc Result> + Send + Sync>, ) { self.all_transactions.additional_balance_provider = Some(f); } @@ -1380,7 +1380,7 @@ pub(crate) struct AllTransactions { auths: FxHashMap>, /// Optional provider for additional balance beyond native (e.g., SGT). /// Given an address, returns the additional balance to add to the native balance. - additional_balance_provider: Option U256 + Send + Sync>>, + additional_balance_provider: Option Result> + Send + Sync>>, /// All Transactions metrics metrics: AllTransactionsMetrics, } @@ -1500,7 +1500,13 @@ impl AllTransactions { let additional_balance = self.additional_balance_provider.clone(); let effective_bal = |native: &U256, addr: Address| -> U256 { match &additional_balance { - Some(f) => native.saturating_add(f(addr)), + Some(f) => match f(addr) { + Ok(additional) => native.saturating_add(additional), + Err(err) => { + warn!(target: "txpool", %err, %addr, "failed to read additional balance; using native-only"); + *native + } + }, None => *native, } }; @@ -1980,7 +1986,13 @@ impl AllTransactions { let additional_balance = self.additional_balance_provider.clone(); let effective_bal = |native: &U256, addr: Address| -> U256 { match &additional_balance { - Some(f) => native.saturating_add(f(addr)), + Some(f) => match f(addr) { + Ok(additional) => native.saturating_add(additional), + Err(err) => { + warn!(target: "txpool", %err, %addr, "failed to read additional balance; using native-only"); + *native + } + }, None => *native, } };