Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
9 changes: 7 additions & 2 deletions src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,12 @@ where
};

// Deduct l1 fee from caller.
let tx_l1_cost =
l1_block_info.calculate_tx_l1_cost(rlp_bytes, spec, ctx.tx().compression_ratio());
let tx_l1_cost = l1_block_info.calculate_tx_l1_cost(
rlp_bytes,
spec,
ctx.tx().compression_ratio(),
ctx.tx().compressed_size(),
);
let caller_account = ctx.journal_mut().load_account(caller)?;
if tx_l1_cost.gt(&caller_account.info.balance) {
return Err(InvalidTransaction::LackOfFundForMaxFee {
Expand Down Expand Up @@ -242,6 +246,7 @@ where
rlp_bytes,
ctx.cfg().spec(),
ctx.tx().compression_ratio(),
ctx.tx().compressed_size(),
)
} else {
U256::from(0)
Expand Down
102 changes: 101 additions & 1 deletion src/l1block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,23 +248,123 @@ impl L1BlockInfo {
.wrapping_div(TX_L1_FEE_PRECISION_U256) // account for penalty
}

fn calculate_tx_l1_cost_galileo(
&self,
tx_size: u32, // size of the original rlp-encoded transaction
spec_id: ScrollSpecId,
compressed_size: u32, // size of the compressed rlp-encoded transaction
) -> U256 {
// Post Galileo rollup fee formula:
// rollup_fee(tx) = fee_per_byte * compressed_size(tx) * (1 + penalty(tx)) / PRECISION
//
// Where:
// fee_per_byte = (exec_scalar * l1_base_fee + blob_scalar * l1_blob_base_fee)
// compressed_size(tx) = min(len(zstd(rlp(tx))), len(rlp(tx)))
// penalty(tx) = compressed_size(tx) / penalty_factor

assert!(
compressed_size <= tx_size,
"transaction compressed size {compressed_size} must be less than or equal to the original size {tx_size}"
);

let compressed_size = U256::from(compressed_size);

let exec_scalar = self
.l1_commit_scalar
.unwrap_or_else(|| panic!("missing exec scalar in spec_id={spec_id:?}"));

let blob_scalar = self
.l1_blob_scalar
.unwrap_or_else(|| panic!("missing l1 blob scalar in spec_id={spec_id:?}"));

let l1_blob_base_fee = self
.l1_blob_base_fee
.unwrap_or_else(|| panic!("missing l1 blob base fee in spec_id={spec_id:?}"));

let penalty_factor = match self.penalty_factor {
Some(f) if f == U256::from(0) => U256::from(1), // sanitize zero penalty factor
Some(f) => f,
None => panic!("missing penalty factor in spec_id={spec_id:?}"),
};

// fee_per_byte = (exec_scalar * l1_base_fee) + (blob_scalar * l1_blob_base_fee)
let component_exec = exec_scalar.saturating_mul(self.l1_base_fee);
let component_blob = blob_scalar.saturating_mul(l1_blob_base_fee);
let fee_per_byte = component_exec.saturating_add(component_blob);

// base_term = fee_per_byte * compressed_size
let base_term = fee_per_byte.saturating_mul(compressed_size);

// penalty_term = (base_term * compressed_size) / penalty_factor
let penalty_term = base_term.saturating_mul(compressed_size).wrapping_div(penalty_factor);

// rollup_fee = (base_term + penalty_term) / PRECISION
base_term.saturating_add(penalty_term).wrapping_div(TX_L1_FEE_PRECISION_U256)
}

/// Calculate the gas cost of a transaction based on L1 block data posted on L2.
pub fn calculate_tx_l1_cost(
&self,
input: &[u8],
spec_id: ScrollSpecId,
compression_ratio: Option<U256>,
compressed_size: Option<u32>,
) -> U256 {
let l1_cost = if !spec_id.is_enabled_in(ScrollSpecId::CURIE) {
self.calculate_tx_l1_cost_shanghai(input, spec_id)
} else if !spec_id.is_enabled_in(ScrollSpecId::FEYNMAN) {
self.calculate_tx_l1_cost_curie(input, spec_id)
} else {
} else if !spec_id.is_enabled_in(ScrollSpecId::GALILEO) {
let compression_ratio = compression_ratio.unwrap_or_else(|| {
panic!("compression ratio should be set in spec_id={spec_id:?}")
});
self.calculate_tx_l1_cost_feynman(input, spec_id, compression_ratio)
} else {
let compressed_size = compressed_size
.unwrap_or_else(|| panic!("compressed size should be set in spec_id={spec_id:?}"));
self.calculate_tx_l1_cost_galileo(input.len() as u32, spec_id, compressed_size)
};
l1_cost.min(U64_MAX)
}
}

#[cfg(test)]
mod tests {
use super::*;
use revm::primitives::uint;
use std::str::FromStr;

#[test]
fn test_rollup_fee_galileo() {
struct TestCase<'a> {
name: &'a str, // test case label
csize: u32, // compressed size
expected: &'a str, // use string to avoid long literal issues
}

let tests = [
TestCase { name: "50-byte tx", csize: 50, expected: "171557471810" }, /* ~0.06 cents */
TestCase { name: "100-byte tx", csize: 100, expected: "344821983141" }, /* ~0.12 cents */
TestCase { name: "1-KiB tx", csize: 1024, expected: "3854009072433" }, /* ~1.35 cents */
TestCase { name: "10-KiB tx", csize: 10 * 1024, expected: "70759382824796" }, /* ~24.77 cents */
TestCase { name: "1-MiB", csize: 1024 * 1024, expected: "378961881717079120" }, /* ~1325 USD */
];

let gpo = L1BlockInfo {
l1_base_fee: uint!(1_000_000_000_U256), // 1 gwei
l1_blob_base_fee: Some(uint!(1_000_000_000_U256)), // 1 gwei
l1_commit_scalar: Some(uint!(2394981796_U256)), // 2.39
l1_blob_scalar: Some(uint!(1019097245_U256)), // 1.02
penalty_factor: Some(uint!(10000_U256)),
..Default::default()
};

for tt in tests {
println!("Running test: {}", tt.name);
let spec = ScrollSpecId::GALILEO;
let expected = U256::from_str(tt.expected).unwrap();
let actual = gpo.calculate_tx_l1_cost_galileo(1e10 as u32, spec, tt.csize);
assert_eq!(expected, actual, "failed case: {}", tt.name);
}
}
}
27 changes: 24 additions & 3 deletions src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ pub trait ScrollTxTr: Transaction {
/// with posting the transaction on L1.
/// Note: compression_ratio(tx) = size(tx) * 1e9 / size(zstd(tx))
fn compression_ratio(&self) -> Option<U256>;

/// The size of the full rlp-encoded transaction after compression.
/// This is used for calculating the cost associated with posting the transaction on L1.
/// Note: compressed_size(tx) = min(size(zstd(rlp(tx))), size(rlp(tx)))
fn compressed_size(&self) -> Option<u32>;
}

/// A Scroll transaction. Wraps around a base transaction and provides the optional RLPed bytes for
Expand All @@ -36,17 +41,28 @@ pub struct ScrollTransaction<T: Transaction> {
pub base: T,
pub rlp_bytes: Option<Bytes>,
pub compression_ratio: Option<U256>,
pub compressed_size: Option<u32>,
}

impl<T: Transaction> ScrollTransaction<T> {
pub fn new(base: T, rlp_bytes: Option<Bytes>, compression_ratio: Option<U256>) -> Self {
Self { base, rlp_bytes, compression_ratio }
pub fn new(
base: T,
rlp_bytes: Option<Bytes>,
compression_ratio: Option<U256>,
compressed_size: Option<u32>,
) -> Self {
Self { base, rlp_bytes, compression_ratio, compressed_size }
}
}

impl Default for ScrollTransaction<TxEnv> {
fn default() -> Self {
Self { base: TxEnv::default(), rlp_bytes: None, compression_ratio: None }
Self {
base: TxEnv::default(),
rlp_bytes: None,
compression_ratio: None,
compressed_size: None,
}
}
}

Expand Down Expand Up @@ -137,6 +153,10 @@ impl<T: Transaction> ScrollTxTr for ScrollTransaction<T> {
fn compression_ratio(&self) -> Option<U256> {
self.compression_ratio
}

fn compressed_size(&self) -> Option<u32> {
self.compressed_size
}
}

impl<TX: Transaction + SystemCallTx> SystemCallTx for ScrollTransaction<TX> {
Expand All @@ -151,6 +171,7 @@ impl<TX: Transaction + SystemCallTx> SystemCallTx for ScrollTransaction<TX> {
TX::new_system_tx_with_caller(caller, system_contract_address, data),
None,
None,
None,
)
}
}
Loading