From e1480da18d2c8b78a4bd752a263ef73660cfce4c Mon Sep 17 00:00:00 2001 From: Youngjoon Lee <5462944+youngjoon-lee@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:33:25 +0900 Subject: [PATCH] fix(ledger): accumulate storage gas consumption (#3181) --- ledger/src/cryptarchia/mod.rs | 46 ++++++++++++++++++++++++++++- ledger/src/lib.rs | 54 +++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/ledger/src/cryptarchia/mod.rs b/ledger/src/cryptarchia/mod.rs index d481ab9d8..655116c63 100644 --- a/ledger/src/cryptarchia/mod.rs +++ b/ledger/src/cryptarchia/mod.rs @@ -9,7 +9,7 @@ use lb_core::{ events::TxEvent, mantle::{ NoteId, Utxo, Value, - gas::{Gas, GasConstants, GasCost, GasPrice}, + gas::{Gas, GasConstants, GasCost, GasOverflow, GasPrice}, ledger::Operation as _, ops::transfer::TransferOp, traits::GenesisTx, @@ -413,6 +413,18 @@ impl LedgerState { } } + /// Accumulates the storage gas consumed by an applied block into the + /// current epoch's counter, which drives the storage price update at the + /// next epoch rotation. + pub fn add_storage_gas_consumed(self, storage_gas: Gas) -> Result { + Ok(Self { + storage_gas_consumed_in_epoch: self + .storage_gas_consumed_in_epoch + .checked_add(storage_gas)?, + ..self + }) + } + fn try_apply_proof( self, slot: Slot, @@ -568,6 +580,12 @@ impl LedgerState { &self.storage_gas_price } + #[cfg(test)] + #[must_use] + pub(crate) const fn storage_gas_consumed_in_epoch(&self) -> Gas { + self.storage_gas_consumed_in_epoch + } + #[must_use] pub const fn aged_utxos(&self) -> &UtxoTree { &self.epoch_state.utxos @@ -2006,4 +2024,30 @@ pub mod tests { (30_289.into(), 1_720_000.into()) ); } + + #[test] + fn test_accumulated_storage_gas_drives_next_epoch_price() { + let config = config(); + let mut ledger = genesis_state(&[utxo()]); + + // Seed a known storage-market state, then accumulate the storage gas + // that applied transactions consume during the epoch. + ledger.storage_gas_price = 113.into(); + ledger.storage_gas_ema = 300.into(); + let ledger = ledger.add_storage_gas_consumed(600.into()).unwrap(); + + // Cross a single epoch boundary so the storage price is recomputed. + let slot: Slot = (config.epoch_length() + 1).into(); + assert_eq!(config.epoch(slot), 1); + let rotated = ledger + .update_epoch_state::(slot, &SdpLedger::new(0.into()), &config) + .unwrap(); + + // The accumulated 600 must reach the price update: with a starting price + // of 113 and EMA 300 that yields (127, 450). + assert_eq!(rotated.storage_gas_price, 127.into()); + assert_eq!(rotated.storage_gas_ema, 450.into()); + // The counter resets for the new epoch. + assert_eq!(rotated.storage_gas_consumed_in_epoch, 0.into()); + } } diff --git a/ledger/src/lib.rs b/ledger/src/lib.rs index 92479d452..69df3c5af 100644 --- a/ledger/src/lib.rs +++ b/ledger/src/lib.rs @@ -357,6 +357,21 @@ impl LedgerState { } } + /// Accumulates the storage gas consumed by the block into the current + /// epoch's counter, which drives the storage price update at the next + /// epoch rotation. + fn add_storage_gas_consumed( + self, + block_storage_gas_consumed: Gas, + ) -> Result> { + Ok(Self { + cryptarchia_ledger: self + .cryptarchia_ledger + .add_storage_gas_consumed(block_storage_gas_consumed)?, + ..self + }) + } + /// Apply the contents of an update to the ledger state. pub fn try_apply_contents<'tx, Tx, Id, Constants: GasConstants>( mut self, @@ -367,6 +382,7 @@ impl LedgerState { Tx: PreverifiedMantleTx + 'tx, { let mut total_block_execution_gas: Gas = 0.into(); + let mut total_block_storage_gas: Gas = 0.into(); let mut total_fee_burned: GasCost = 0.into(); let mut total_fee_tip: GasCost = 0.into(); let mut tx_events = Vec::new(); @@ -409,6 +425,8 @@ impl LedgerState { total_fee_tip = total_fee_tip.checked_add(tx_fee_tip)?; total_block_execution_gas = total_block_execution_gas .checked_add(tx.execution_gas_consumption::(&gas_prices)?)?; + total_block_storage_gas = + total_block_storage_gas.checked_add(tx.storage_gas_consumption(&gas_prices)?)?; // Check that the block is not exceeding the Gas limit if total_block_execution_gas > EXECUTION_GAS_LIMIT { @@ -422,6 +440,9 @@ impl LedgerState { self = self.compute_block_rewards(total_fee_burned, total_fee_tip)?; // Update Execution market state self = self.update_execution_market(total_block_execution_gas); + // Accumulate storage gas consumed so the storage market can update the + // price at the next epoch rotation. + self = self.add_storage_gas_consumed(total_block_storage_gas)?; Ok((self, tx_events)) } @@ -1731,6 +1752,39 @@ mod tests { assert!(events.is_empty()); } + #[test] + fn test_apply_contents_accumulates_storage_gas() { + let utxo = utxo(); + let config = config(); + let mut ledger = LedgerState::from_utxos([utxo], &config); + update_ledger_prices(&mut ledger, 1, 1); + + // No outputs: the whole input covers the gas cost and the remainder is + // tipped. We only assert on the storage counter, not the tip, so the + // exact balance is irrelevant. + let sk = ZkKey::from(BigUint::from(0u8)); + let tx = create_tx(vec![utxo.id()], vec![], std::slice::from_ref(&sk)) + .preverify() + .unwrap(); + + // The tx must consume a non-zero amount of storage gas for the check to + // be meaningful. + let storage_gas = tx + .storage_gas_consumption(&ledger.get_gas_prices()) + .unwrap(); + assert!(storage_gas.into_inner() > 0); + + let (applied, _) = ledger + .try_apply_contents::<_, HeaderId, MainnetGasConstants>(&config, std::iter::once(&tx)) + .unwrap(); + + // Storage gas consumed by the tx should be accumulated in the ledger + assert_eq!( + applied.cryptarchia_ledger.storage_gas_consumed_in_epoch(), + storage_gas + ); + } + #[test] fn test_leader_claim_operation() { let leaders = LeaderState::new();