fix(ledger): accumulate storage gas consumption (#3181)

This commit is contained in:
Youngjoon Lee
2026-07-24 23:33:25 +09:00
committed by GitHub
parent 14ed7b3dd0
commit e1480da18d
2 changed files with 99 additions and 1 deletions
+45 -1
View File
@@ -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<Self, GasOverflow> {
Ok(Self {
storage_gas_consumed_in_epoch: self
.storage_gas_consumed_in_epoch
.checked_add(storage_gas)?,
..self
})
}
fn try_apply_proof<LeaderProof, Id>(
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::<HeaderId>(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());
}
}
+54
View File
@@ -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<Id>(
self,
block_storage_gas_consumed: Gas,
) -> Result<Self, LedgerError<Id>> {
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<Context = GasPrices> + '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::<Constants>(&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();