Skip to content
Open
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
10 changes: 7 additions & 3 deletions crates/payload/builder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,16 +369,20 @@ where

let execution_start = Instant::now();
while let Some(pool_tx) = best_txs.next() {
// TIP-1016: State gas does not count toward block gas capacity, so use
// execution_gas_limit (= gas_limit - state_gas) for the block-level checks below.
let tx_execution_gas_limit = pool_tx.transaction.execution_gas_limit();

// Ensure we still have capacity for this transaction within the non-shared gas limit.
// The remaining `shared_gas_limit` is reserved for validator subblocks and must not
// be consumed by proposer's pool transactions.
if cumulative_gas_used + pool_tx.gas_limit() > non_shared_gas_limit {
if cumulative_gas_used + tx_execution_gas_limit > non_shared_gas_limit {
// Mark this transaction as invalid since it doesn't fit
// The iterator will handle lane switching internally when appropriate
best_txs.mark_invalid(
&pool_tx,
&InvalidPoolTransactionError::ExceedsGasLimit(
pool_tx.gas_limit(),
tx_execution_gas_limit,
non_shared_gas_limit - cumulative_gas_used,
),
);
Expand All @@ -388,7 +392,7 @@ where
// If the tx is not a payment and will exceed the general gas limit
// mark the tx as invalid and continue
if !pool_tx.transaction.is_payment()
&& non_payment_gas_used + pool_tx.gas_limit() > general_gas_limit
&& non_payment_gas_used + tx_execution_gas_limit > general_gas_limit
{
best_txs.mark_invalid(
&pool_tx,
Expand Down
42 changes: 30 additions & 12 deletions crates/revm/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1283,10 +1283,18 @@ where
}

// Validate gas limit is sufficient for initial gas
if gas_limit < init_gas.initial_total_gas {
// State gas is only included in the intrinsic check for T2+, since pre-T2
// transactions were never validated against state gas.
let total_intrinsic = init_gas.initial_total_gas
+ if spec.is_t2() {
init_gas.initial_state_gas
} else {
0
};
if gas_limit < total_intrinsic {
return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
gas_limit,
intrinsic_gas: init_gas.initial_total_gas,
intrinsic_gas: total_intrinsic,
}
.into());
}
Expand Down Expand Up @@ -1536,10 +1544,18 @@ where
}

// Validate gas limit is sufficient for initial gas
if gas_limit < batch_gas.initial_total_gas {
// State gas is only included in the intrinsic check for T2+, since pre-T2
// transactions were never validated against state gas.
let total_intrinsic = batch_gas.initial_total_gas
+ if spec.is_t2() {
batch_gas.initial_state_gas
} else {
0
};
if gas_limit < total_intrinsic {
return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
gas_limit,
intrinsic_gas: batch_gas.initial_total_gas,
intrinsic_gas: total_intrinsic,
}
.into());
}
Expand Down Expand Up @@ -2546,19 +2562,21 @@ mod tests {
} else {
spec.gas_new_nonce_key()
};
// TIP-1016: For T2+, state gas must also fit within gas_limit
let nonce_zero_state_gas = if spec.is_t2() {
gas_params.new_account_state_gas()
} else {
0
};
let nonce_zero_total = nonce_zero_gas + nonce_zero_state_gas;

let cases = if spec.is_t0() {
let mut cases = vec![
(BASE_INTRINSIC_GAS + nonce_zero_gas, 0, true), // Exactly sufficient for nonce==0
(BASE_INTRINSIC_GAS + nonce_zero_total, 0, true), // Exactly sufficient for nonce==0 (exec + state)
(BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(), 1, true), // Exactly sufficient for existing key
];
// Only test "insufficient" case when nonce_zero_gas > 10k (T1 has 250k, T2 has 5k)
if nonce_zero_gas > 10_000 {
cases.push((BASE_INTRINSIC_GAS + 10_000, 0u64, false)); // Insufficient for nonce==0
} else {
// T2: nonce_zero_gas is 5k, so BASE + nonce_zero_gas - 1 is insufficient
cases.push((BASE_INTRINSIC_GAS + nonce_zero_gas - 1, 0u64, false));
}
// Insufficient: below total required for nonce==0
cases.push((BASE_INTRINSIC_GAS + nonce_zero_total - 1, 0u64, false));
cases
} else {
// Genesis: nonce gas is added AFTER validation, so lower gas_limit still passes
Expand Down
8 changes: 7 additions & 1 deletion crates/transaction-pool/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ impl TxBuilder {
self
}

/// Set the chain ID.
pub(crate) fn chain_id(mut self, chain_id: u64) -> Self {
self.chain_id = chain_id;
self
}

/// Set the max fee per gas.
pub(crate) fn max_fee(mut self, fee: u128) -> Self {
self.max_fee_per_gas = fee;
Expand Down Expand Up @@ -189,7 +195,7 @@ impl TxBuilder {
});

let tx = TempoTransaction {
chain_id: 1,
chain_id: self.chain_id,
max_priority_fee_per_gas: self.max_priority_fee_per_gas,
max_fee_per_gas: self.max_fee_per_gas,
gas_limit: self.gas_limit,
Expand Down
66 changes: 65 additions & 1 deletion crates/transaction-pool/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ pub struct TempoPooledTransaction {
/// `Some(expiry)` for keychain transactions where expiry < u64::MAX (finite expiry).
/// `None` for non-keychain transactions or keys that never expire.
key_expiry: OnceLock<Option<u64>>,
/// Intrinsic state gas (TIP-1016 storage creation gas component).
///
/// Set during validation for T2+ transactions. The state gas portion does NOT count
/// against protocol limits (block/transaction execution gas caps), only against
/// the user's `gas_limit`.
///
/// NOTE: This value is hardfork-specific (gas parameters vary by fork). If a
/// transaction survives a hardfork boundary, the cached value may be stale.
/// Future fork transitions that change state gas parameters should add pool
/// maintenance logic (like `evict_underpriced_transactions_for_t1`) to
/// re-validate or evict affected transactions.
intrinsic_state_gas: OnceLock<u64>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isnt this problematic, because the intrinsic gas is hardfork specific, right?

so it's possible that a tx survives a hardfork boundary, making this incorrect fpr future blocks?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, added comment about it, lmk if that is enough

}

impl TempoPooledTransaction {
Expand All @@ -70,6 +82,7 @@ impl TempoPooledTransaction {
nonce_key_slot: OnceLock::new(),
tx_env: OnceLock::new(),
key_expiry: OnceLock::new(),
intrinsic_state_gas: OnceLock::new(),
}
}

Expand Down Expand Up @@ -199,6 +212,27 @@ impl TempoPooledTransaction {
pub fn key_expiry(&self) -> Option<u64> {
self.key_expiry.get().copied().flatten()
}

/// Sets the intrinsic state gas for this transaction (TIP-1016).
///
/// Called during validation for T2+ transactions. State gas is the storage creation
/// gas component that does NOT count against protocol limits.
pub fn set_intrinsic_state_gas(&self, state_gas: u64) {
let _ = self.intrinsic_state_gas.set(state_gas);
}

/// Returns the intrinsic state gas, or 0 if not set (pre-T2 or not yet validated).
pub fn intrinsic_state_gas(&self) -> u64 {
self.intrinsic_state_gas.get().copied().unwrap_or(0)
}

/// Returns the execution gas limit (gas_limit minus state gas).
///
/// For T2+ this is the portion that counts against protocol limits.
/// For pre-T2, state gas is 0, so this equals `gas_limit()`.
pub fn execution_gas_limit(&self) -> u64 {
self.gas_limit().saturating_sub(self.intrinsic_state_gas())
}
}

#[derive(Debug, Error)]
Expand Down Expand Up @@ -931,7 +965,7 @@ mod tests {
.build();

// Test various Transaction trait methods
assert_eq!(tx.chain_id(), Some(1));
assert_eq!(tx.chain_id(), Some(42431));
assert_eq!(tx.nonce(), 0);
assert_eq!(tx.gas_limit(), 1_000_000);
assert_eq!(tx.max_fee_per_gas(), 20_000_000_000);
Expand All @@ -950,6 +984,36 @@ mod tests {
// PoolTransaction::cost() returns &U256::ZERO for Tempo
assert_eq!(*tx.cost(), U256::ZERO);
}

#[test]
fn test_intrinsic_state_gas_defaults_to_zero() {
let tx = TxBuilder::aa(Address::random())
.gas_limit(1_000_000)
.build();

assert_eq!(tx.intrinsic_state_gas(), 0);
assert_eq!(tx.execution_gas_limit(), 1_000_000);
}

#[test]
fn test_set_intrinsic_state_gas() {
let tx = TxBuilder::aa(Address::random())
.gas_limit(1_000_000)
.build();

tx.set_intrinsic_state_gas(245_000);
assert_eq!(tx.intrinsic_state_gas(), 245_000);
assert_eq!(tx.execution_gas_limit(), 755_000);
}

#[test]
fn test_execution_gas_limit_saturates() {
let tx = TxBuilder::aa(Address::random()).gas_limit(100).build();

// State gas larger than gas_limit should saturate to 0
tx.set_intrinsic_state_gas(200);
assert_eq!(tx.execution_gas_limit(), 0);
}
}

// ========================================
Expand Down
Loading
Loading