Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

isthmus: operator fee #1798

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
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
109 changes: 107 additions & 2 deletions crates/optimism/src/handler_register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use super::{
optimism_spec_to_generic, OptimismContext, OptimismHaltReason, OptimismInvalidTransaction,
OptimismSpec, OptimismSpecId, OptimismTransaction, OptimismWiring,
};
use crate::{BASE_FEE_RECIPIENT, L1_FEE_RECIPIENT};
use crate::{l1block::OPERATOR_FEE_RECIPIENT, BASE_FEE_RECIPIENT, L1_FEE_RECIPIENT};
use core::ops::Mul;
use revm::{
database_interface::Database,
Expand Down Expand Up @@ -47,6 +47,7 @@ where
// Refund is calculated differently then mainnet.
handler.execution.last_frame_return = Arc::new(last_frame_return::<EvmWiringT, SPEC>);
handler.post_execution.refund = Arc::new(refund::<EvmWiringT, SPEC>);
handler.post_execution.reimburse_caller = Arc::new(reimburse_caller::<EvmWiringT, SPEC>);
handler.post_execution.reward_beneficiary =
Arc::new(reward_beneficiary::<EvmWiringT, SPEC>);
// In case of halt of deposit transaction return Error.
Expand Down Expand Up @@ -177,6 +178,39 @@ pub fn refund<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
}
}

/// Reimburse the transaction caller.
#[inline]
pub fn reimburse_caller<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
context: &mut Context<EvmWiringT>,
gas: &Gas,
) -> EVMResultGeneric<(), EvmWiringT> {
mainnet::reimburse_caller::<EvmWiringT>(context, gas)?;
let caller = *context.evm.env.tx.caller();
let caller_account = context
.evm
.inner
.journaled_state
.load_account(caller, &mut context.evm.inner.db)
.map_err(EVMError::Database)?;
// In additional to the normal transaction fee, additionally refund the caller
// for the operator fee.
let operator_fee_refund = context
.evm
.inner
.chain
.l1_block_info()
.expect("L1BlockInfo should be loaded")
.operator_fee_refund(gas, SPEC::OPTIMISM_SPEC_ID);

caller_account.data.info.balance = caller_account
.data
.info
.balance
.saturating_add(operator_fee_refund);

Ok(())
}

/// Load precompiles for Optimism chain.
#[inline]
pub fn load_precompiles<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
Expand Down Expand Up @@ -248,6 +282,7 @@ pub fn deduct_caller<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(

// If the transaction is not a deposit transaction, subtract the L1 data fee from the
// caller's balance directly after minting the requested amount of ETH.
// Additionally deduct the operator fee from the caller's account.
if context.evm.inner.env.tx.source_hash().is_none() {
// get envelope
let Some(enveloped_tx) = &context.evm.inner.env.tx.enveloped_tx() else {
Expand All @@ -273,6 +308,22 @@ pub fn deduct_caller<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
));
}
caller_account.info.balance = caller_account.info.balance.saturating_sub(tx_l1_cost);

// Deduct the operator fee from the caller's account.
let gas_limit = U256::from(context.evm.inner.env.tx.gas_limit());

let operator_fee_charge = context
.evm
.inner
.chain
.l1_block_info()
.expect("L1BlockInfo should be loaded")
.operator_fee_charge(gas_limit, SPEC::OPTIMISM_SPEC_ID);

caller_account.info.balance = caller_account
.info
.balance
.saturating_sub(operator_fee_charge);
}
Ok(())
}
Expand Down Expand Up @@ -306,6 +357,10 @@ pub fn reward_beneficiary<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
};

let l1_cost = l1_block_info.calculate_tx_l1_cost(enveloped_tx, SPEC::OPTIMISM_SPEC_ID);
let operator_fee_cost = l1_block_info.operator_fee_charge(
U256::from(gas.spent() - gas.refunded() as u64),
SPEC::OPTIMISM_SPEC_ID,
);

// Send the L1 cost of the transaction to the L1 Fee Vault.
let mut l1_fee_vault_account = context
Expand All @@ -332,6 +387,20 @@ pub fn reward_beneficiary<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
.block
.basefee()
.mul(U256::from(gas.spent() - gas.refunded() as u64));

// Send the operator fee of the transaction to the coinbase.
let operator_fee_vault_account = context
.evm
.inner
.journaled_state
.load_account(OPERATOR_FEE_RECIPIENT, &mut context.evm.inner.db)
.map_err(EVMError::Database)?;

operator_fee_vault_account.data.info.balance = operator_fee_vault_account
.data
.info
.balance
.saturating_add(operator_fee_cost);
}
Ok(())
}
Expand Down Expand Up @@ -428,7 +497,9 @@ pub fn end<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
#[cfg(test)]
mod tests {
use super::*;
use crate::{BedrockSpec, L1BlockInfo, LatestSpec, OptimismEvmWiring, RegolithSpec};
use crate::{
BedrockSpec, HoloceneSpec, L1BlockInfo, LatestSpec, OptimismEvmWiring, RegolithSpec,
};
use database::InMemoryDB;
use revm::{
database_interface::EmptyDB,
Expand Down Expand Up @@ -630,6 +701,40 @@ mod tests {
assert_eq!(account.info.balance, U256::from(1));
}

#[test]
fn test_remove_operator_cost() {
let caller = Address::ZERO;
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
AccountInfo {
balance: U256::from(151),
..Default::default()
},
);
let mut context = Context::<TestMemOpWiring>::new_with_db(db);
*context.evm.chain.l1_block_info_mut() = Some(L1BlockInfo {
operator_fee_scalar: Some(U256::from(10_000_000)),
operator_fee_constant: Some(U256::from(50)),
..Default::default()
});
context.evm.inner.env.tx.base.gas_limit = 10;

// operator fee cost is operator_fee_scalar * gas_limit / 1e6 + operator_fee_constant
// 10_000_000 * 10 / 1_000_000 + 50 = 150
context.evm.inner.env.tx.enveloped_tx = Some(bytes!("FACADE"));
deduct_caller::<TestMemOpWiring, HoloceneSpec>(&mut context).unwrap();

// Check the account balance is updated.
let account = context
.evm
.inner
.journaled_state
.load_account(caller, &mut context.evm.inner.db)
.unwrap();
assert_eq!(account.info.balance, U256::from(1));
}

#[test]
fn test_remove_l1_cost_lack_of_funds() {
let caller = Address::ZERO;
Expand Down
122 changes: 109 additions & 13 deletions crates/optimism/src/l1block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::fast_lz::flz_compress_len;
use core::ops::Mul;
use revm::{
database_interface::Database,
interpreter::Gas,
primitives::{address, Address, U256},
};

Expand All @@ -16,6 +17,17 @@ const BASE_FEE_SCALAR_OFFSET: usize = 16;
/// The two 4-byte Ecotone fee scalar values are packed into the same storage slot as the 8-byte sequence number.
/// Byte offset within the storage slot of the 4-byte blobBaseFeeScalar attribute.
const BLOB_BASE_FEE_SCALAR_OFFSET: usize = 20;
/// The two 8-byte Isthmus operator fee scalar values are similarly packed. Byte offset within
/// the storage slot of the 8-byte operatorFeeScalar attribute.
const OPERATOR_FEE_SCALAR_OFFSET: usize = 4;
/// The two 8-byte Isthmus operator fee scalar values are similarly packed. Byte offset within
/// the storage slot of the 8-byte operatorFeeConstant attribute.
const OPERATOR_FEE_CONSTANT_OFFSET: usize = 8;

/// The fixed point decimal scaling factor associated with the operator fee scalar.
///
/// Allows users to use 6 decimal points of precision when specifying the operator_fee_scalar.
const OPERATOR_FEE_SCALAR_DECIMAL: u64 = 1_000_000;

const L1_BASE_FEE_SLOT: U256 = U256::from_limbs([1u64, 0, 0, 0]);
const L1_OVERHEAD_SLOT: U256 = U256::from_limbs([5u64, 0, 0, 0]);
Expand All @@ -28,6 +40,10 @@ const ECOTONE_L1_BLOB_BASE_FEE_SLOT: U256 = U256::from_limbs([7u64, 0, 0, 0]);
/// offsets [BASE_FEE_SCALAR_OFFSET] and [BLOB_BASE_FEE_SCALAR_OFFSET] respectively.
const ECOTONE_L1_FEE_SCALARS_SLOT: U256 = U256::from_limbs([3u64, 0, 0, 0]);

/// This storage slot stores the 32-bit operatorFeeScalar and operatorFeeConstant attributes at
/// offsets [OPERATOR_FEE_SCALAR_OFFSET] and [OPERATOR_FEE_CONSTANT_OFFSET] respectively.
const OPERATOR_FEE_SCALARS_SLOT: U256 = U256::from_limbs([8u64, 0, 0, 0]);

/// An empty 64-bit set of scalar values.
const EMPTY_SCALARS: [u8; 8] = [0u8; 8];

Expand All @@ -37,6 +53,9 @@ pub const L1_FEE_RECIPIENT: Address = address!("42000000000000000000000000000000
/// The address of the base fee recipient.
pub const BASE_FEE_RECIPIENT: Address = address!("4200000000000000000000000000000000000019");

/// The address of the operator fee recipient.
pub const OPERATOR_FEE_RECIPIENT: Address = address!("420000000000000000000000000000000000001B");

/// The address of the L1Block contract.
pub const L1_BLOCK_CONTRACT: Address = address!("4200000000000000000000000000000000000015");

Expand All @@ -63,8 +82,12 @@ pub struct L1BlockInfo {
pub l1_blob_base_fee: Option<U256>,
/// The current L1 blob base fee scalar. None if Ecotone is not activated.
pub l1_blob_base_fee_scalar: Option<U256>,
/// The current L1 blob base fee. None if Isthmus is not activated, except if `empty_scalars` is `true`.
pub operator_fee_scalar: Option<U256>,
/// The current L1 blob base fee scalar. None if Isthmus is not activated.
pub operator_fee_constant: Option<U256>,
/// True if Ecotone is activated, but the L1 fee scalars have not yet been set.
pub(crate) empty_scalars: bool,
pub(crate) empty_ecotone_scalars: bool,
}

impl L1BlockInfo {
Expand Down Expand Up @@ -107,21 +130,56 @@ impl L1BlockInfo {

// Check if the L1 fee scalars are empty. If so, we use the Bedrock cost function. The L1 fee overhead is
// only necessary if `empty_scalars` is true, as it was deprecated in Ecotone.
let empty_scalars = l1_blob_base_fee.is_zero()
let empty_ecotone_scalars = l1_blob_base_fee.is_zero()
&& l1_fee_scalars[BASE_FEE_SCALAR_OFFSET..BLOB_BASE_FEE_SCALAR_OFFSET + 4]
== EMPTY_SCALARS;
let l1_fee_overhead = empty_scalars

let l1_fee_overhead = empty_ecotone_scalars
.then(|| db.storage(L1_BLOCK_CONTRACT, L1_OVERHEAD_SLOT))
.transpose()?;

Ok(L1BlockInfo {
l1_base_fee,
l1_base_fee_scalar,
l1_blob_base_fee: Some(l1_blob_base_fee),
l1_blob_base_fee_scalar: Some(l1_blob_base_fee_scalar),
empty_scalars,
l1_fee_overhead,
})
// Pre-isthmus L1 block info
if !spec_id.is_enabled_in(OptimismSpecId::ISTHMUS) {
Ok(L1BlockInfo {
l1_base_fee,
l1_base_fee_scalar,
l1_blob_base_fee: Some(l1_blob_base_fee),
l1_blob_base_fee_scalar: Some(l1_blob_base_fee_scalar),
empty_ecotone_scalars,
l1_fee_overhead,
..Default::default()
})
} else {
let operator_fee_scalars = db
.storage(L1_BLOCK_CONTRACT, OPERATOR_FEE_SCALARS_SLOT)?
.to_be_bytes::<32>();

// Post-isthmus L1 block info
// The `operator_fee_scalar` is stored as a big endian u32 at
// OPERATOR_FEE_SCALAR_OFFSET.
let operator_fee_scalar = U256::from_be_slice(
operator_fee_scalars
[OPERATOR_FEE_SCALAR_OFFSET..OPERATOR_FEE_SCALAR_OFFSET + 4]
.as_ref(),
);
// The `operator_fee_constant` is stored as a big endian u64 at
// OPERATOR_FEE_CONSTANT_OFFSET.
let operator_fee_constant = U256::from_be_slice(
operator_fee_scalars
[OPERATOR_FEE_CONSTANT_OFFSET..OPERATOR_FEE_CONSTANT_OFFSET + 8]
.as_ref(),
);
Ok(L1BlockInfo {
l1_base_fee,
l1_base_fee_scalar,
l1_blob_base_fee: Some(l1_blob_base_fee),
l1_blob_base_fee_scalar: Some(l1_blob_base_fee_scalar),
empty_ecotone_scalars,
l1_fee_overhead,
operator_fee_scalar: Some(operator_fee_scalar),
operator_fee_constant: Some(operator_fee_constant),
})
}
}
}

Expand Down Expand Up @@ -157,6 +215,44 @@ impl L1BlockInfo {
rollup_data_gas_cost
}

/// Calculate the operator fee for executing this transaction.
///
/// Introduced in isthmus. Prior to isthmus, the operator fee is always zero.
pub fn operator_fee_charge(&self, gas_limit: U256, spec_id: OptimismSpecId) -> U256 {
if !spec_id.is_enabled_in(OptimismSpecId::ISTHMUS) {
return U256::ZERO;
}
let operator_fee_scalar = self
.operator_fee_scalar
.expect("Missing operator fee scalar for isthmus L1 Block");
let operator_fee_constant = self
.operator_fee_constant
.expect("Missing operator fee constant for isthmus L1 Block");

let product = gas_limit.saturating_mul(operator_fee_scalar)
/ (U256::from(OPERATOR_FEE_SCALAR_DECIMAL));

product.saturating_add(operator_fee_constant)
}

/// Calculate the operator fee for executing this transaction.
///
/// Introduced in isthmus. Prior to isthmus, the operator fee is always zero.
pub fn operator_fee_refund(&self, gas: &Gas, spec_id: OptimismSpecId) -> U256 {
if !spec_id.is_enabled_in(OptimismSpecId::ISTHMUS) {
return U256::ZERO;
}

let operator_fee_scalar = self
.operator_fee_scalar
.expect("Missing operator fee scalar for isthmus L1 Block");

// We're computing the difference between two operator fees, so no need to include the
// constant.

operator_fee_scalar.saturating_mul(U256::from(gas.remaining() + gas.refunded() as u64))
}

// Calculate the estimated compressed transaction size in bytes, scaled by 1e6.
// This value is computed based on the following formula:
// max(minTransactionSize, intercept + fastlzCoef*fastlzSize)
Expand Down Expand Up @@ -209,7 +305,7 @@ impl L1BlockInfo {
// There is an edgecase where, for the very first Ecotone block (unless it is activated at Genesis), we must
// use the Bedrock cost function. To determine if this is the case, we can check if the Ecotone parameters are
// unset.
if self.empty_scalars {
if self.empty_ecotone_scalars {
return self.calculate_tx_l1_cost_bedrock(input, spec_id);
}

Expand Down Expand Up @@ -367,7 +463,7 @@ mod tests {
assert_eq!(gas_cost, U256::ZERO);

// If the scalars are empty, the bedrock cost function should be used.
l1_block_info.empty_scalars = true;
l1_block_info.empty_ecotone_scalars = true;
let input = bytes!("FACADE");
let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, OptimismSpecId::ECOTONE);
assert_eq!(gas_cost, U256::from(1048));
Expand Down
6 changes: 4 additions & 2 deletions crates/optimism/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ mod spec;

pub use handler_register::{
deduct_caller, end, last_frame_return, load_accounts, load_precompiles,
optimism_handle_register, output, refund, reward_beneficiary, validate_env,
optimism_handle_register, output, refund, reimburse_caller, reward_beneficiary, validate_env,
validate_tx_against_state,
};
pub use l1block::{L1BlockInfo, BASE_FEE_RECIPIENT, L1_BLOCK_CONTRACT, L1_FEE_RECIPIENT};
pub use l1block::{
L1BlockInfo, BASE_FEE_RECIPIENT, L1_BLOCK_CONTRACT, L1_FEE_RECIPIENT, OPERATOR_FEE_RECIPIENT,
};
pub use result::{OptimismHaltReason, OptimismInvalidTransaction};
use revm::{
primitives::{Bytes, B256},
Expand Down
Loading