Skip to content
This repository has been archived by the owner on Nov 5, 2023. It is now read-only.

[wip] feat: state function changes #6

Closed
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ target/
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

# Generated by VSCode
.vscode


# Generated by Intellij-based IDEs.
.idea
Expand Down
1 change: 1 addition & 0 deletions crates/executor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ parking_lot = "0.12"

[features]
test-utils = ["parking_lot"]
optimism = []
172 changes: 146 additions & 26 deletions crates/executor/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ use std::{
sync::Arc,
};

#[cfg(feature = "optimism")]
use crate::optimism;

/// Main block executor
pub struct Executor<DB>
where
Expand Down Expand Up @@ -385,6 +388,9 @@ where

self.init_env(&block.header, total_difficulty);

#[cfg(feature = "optimism")]
let mut l1_block_info = optimism::L1BlockInfo::new(block)?;

let mut cumulative_gas_used = 0;
let mut post_state = PostState::with_tx_capacity(block.body.len());
for (transaction, sender) in block.body.iter().zip(senders.into_iter()) {
Expand All @@ -397,33 +403,147 @@ where
block_available_gas,
})
}
// Execute transaction.
let ResultAndState { result, state } = self.transact(transaction, sender)?;

// commit changes
self.commit_changes(
state,
self.chain_spec.fork(Hardfork::SpuriousDragon).active_at_block(block.number),
&mut post_state,
);

// append gas used
cumulative_gas_used += result.gas_used();

// cast revm logs to reth logs
let logs: Vec<Log> = result.logs().into_iter().map(into_reth_log).collect();

// Push transaction changeset and calculate header bloom filter for receipt.
post_state.add_receipt(Receipt {
tx_type: transaction.tx_type(),
// Success flag was added in `EIP-658: Embedding transaction status code in
// receipts`.
success: result.is_success(),
cumulative_gas_used,
bloom: logs_bloom(logs.iter()),
logs,
});
post_state.finish_transition();
#[cfg(feature = "optimism")]
{
let db = self.db();
let l1_cost = l1_block_info.calculate_tx_l1_cost(transaction);

let sender_account = db.load_account(sender).map_err(|_| Error::ProviderError)?;
let old_sender_info = to_reth_acc(&sender_account.info);
if let Some(m) = transaction.mint() {
// Add balance to the caller account equal to the minted amount.
// Note: this is unconditional, and will not be reverted if the tx fails
// (unless the block can't be built at all due to gas limit constraints)
sender_account.info.balance += U256::from(m);
}
Comment on lines +414 to +419
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This could have been addressed in Revm's transact() function in a more clean way. In fact I have a local branch of what this change would look like here: https://github.com/merklefruit/revm/pull/1/files#diff-1d478ba44ccc56e3b1142bd3723bf97f3e254c25dd18323481aedadce0803e91R130

However that solution has some cons:

  1. we are not able to revert the deposit if the block runs out of gas, because revm's transact() context doesn't know about the outstanding block gas used (called cumulative_gas_used here)
  2. we can't cache the values to calculate l1_cost for the entire block, and would have to access the DB for each transaction in the block instead. This is the main reason why I wanted to keep all the diffs on Op-reth if possible


// Check if the sender balance can cover the L1 cost.
// Deposits pay for their gas directly on L1 so they are exempt from this
if !transaction.is_deposit() {
if sender_account.info.balance.cmp(&l1_cost) == std::cmp::Ordering::Less {
return Err(Error::InsufficientFundsForL1Cost {
have: sender_account.info.balance.to::<u64>(),
want: l1_cost.to::<u64>(),
})
}

// Safely take l1_cost from sender (the rest will be deducted by the
// internal EVM execution and included in result.gas_used())
// TODO: need to handle calls with `disable_balance_check` flag set?
sender_account.info.balance -= l1_cost;
}

let new_sender_info = to_reth_acc(&sender_account.info);
post_state.change_account(sender, old_sender_info, new_sender_info);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Potential problem: when changing the post_state before the tx execution like we do here, does this create a clash with the state object returned by the execution? hopefully not, but have to test this


// Execute transaction.
let ResultAndState { result, state } = self.transact(transaction, sender)?;

if transaction.is_deposit() && !result.is_success() {
// If the Deposited transaction failed, the deposit must still be included.
// In this case, we need to increment the sender nonce and disregard the
// state changes. The transaction is also recorded as using all gas.
let db = self.db();
let sender_account =
db.load_account(sender).map_err(|_| Error::ProviderError)?;
let old_sender_info = to_reth_acc(&sender_account.info);
sender_account.info.nonce += 1;
let new_sender_info = to_reth_acc(&sender_account.info);

post_state.change_account(sender, old_sender_info, new_sender_info);
if !transaction.is_system_transaction() {
cumulative_gas_used += transaction.gas_limit();
}

post_state.add_receipt(Receipt {
tx_type: transaction.tx_type(),
success: false,
cumulative_gas_used,
bloom: Bloom::zero(),
logs: vec![],
deposit_nonce: Some(transaction.nonce()),
});
post_state.finish_transition();
continue
}

// commit changes
self.commit_changes(
state,
self.chain_spec.fork(Hardfork::SpuriousDragon).active_at_block(block.number),
&mut post_state,
);

if !transaction.is_system_transaction() {
// After Regolith, deposits are reported as using the actual gas used instead of
// all the gas. System transactions are not reported as using any gas.
cumulative_gas_used += result.gas_used()
}

// Route the l1 cost and base fee to the appropriate optimism vaults
self.increment_account_balance(
optimism::l1_cost_recipient(),
l1_cost,
&mut post_state,
)?;
self.increment_account_balance(
optimism::base_fee_recipient(),
U256::from(
block
.base_fee_per_gas
.unwrap_or_default()
.saturating_mul(result.gas_used()),
),
&mut post_state,
)?;
Comment on lines +484 to +499
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

can probably be refactored to be prettier


// cast revm logs to reth logs
let logs: Vec<Log> = result.logs().into_iter().map(into_reth_log).collect();

// Push transaction changeset and calculate header bloom filter for receipt.
post_state.add_receipt(Receipt {
tx_type: transaction.tx_type(),
// Success flag was added in `EIP-658: Embedding transaction status code in
// receipts`.
success: result.is_success(),
cumulative_gas_used,
bloom: logs_bloom(logs.iter()),
logs,
deposit_nonce: Some(transaction.nonce()),
});
post_state.finish_transition();
}

#[cfg(not(feature = "optimism"))]
{
// Execute transaction.
let ResultAndState { result, state } = self.transact(transaction, sender)?;

// commit changes
self.commit_changes(
state,
self.chain_spec.fork(Hardfork::SpuriousDragon).active_at_block(block.number),
&mut post_state,
);

cumulative_gas_used += result.gas_used();

// cast revm logs to reth logs
let logs: Vec<Log> = result.logs().into_iter().map(into_reth_log).collect();

// Push transaction changeset and calculate header bloom filter for receipt.
post_state.add_receipt(Receipt {
tx_type: transaction.tx_type(),
// Success flag was added in `EIP-658: Embedding transaction status code in
// receipts`.
success: result.is_success(),
cumulative_gas_used,
bloom: logs_bloom(logs.iter()),
logs,
});
post_state.finish_transition();
}
}

Ok((post_state, cumulative_gas_used))
Expand Down
4 changes: 4 additions & 0 deletions crates/executor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ pub use factory::Factory;
#[cfg(any(test, feature = "test-utils"))]
/// Common test helpers for mocking out executor and executor factory
pub mod test_utils;

#[cfg(feature = "optimism")]
/// Optimism-specific utilities for the executor
pub mod optimism;
110 changes: 110 additions & 0 deletions crates/executor/src/optimism.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use std::str::FromStr;

use reth_interfaces::executor;
use reth_primitives::{Address, Block, TransactionKind, TransactionSigned, U256};

const L1_FEE_RECIPIENT: &str = "0x420000000000000000000000000000000000001A";
const BASE_FEE_RECIPIENT: &str = "0x4200000000000000000000000000000000000019";
const L1_BLOCK_CONTRACT: &str = "0x4200000000000000000000000000000000000015";

const ZERO_BYTE_COST: u64 = 4;
const NON_ZERO_BYTE_COST: u64 = 16;

/// L1 block info
///
/// We can extract L1 epoch data from each L2 block, by looking at the `setL1BlockValues`
/// transaction data. This data is then used to calculate the L1 cost of a transaction.
///
/// Here is the format of the `setL1BlockValues` transaction data:
///
/// setL1BlockValues(uint64 _number, uint64 _timestamp, uint256 _basefee, bytes32 _hash,
/// uint64 _sequenceNumber, bytes32 _batcherHash, uint256 _l1FeeOverhead, uint256 _l1FeeScalar)
///
/// For now, we only care about the fields necessary for L1 cost calculation.
pub struct L1BlockInfo {
l1_base_fee: U256,
l1_fee_overhead: U256,
l1_fee_scalar: U256,
}

impl L1BlockInfo {
/// Create a new L1 block info struct from a L2 block
pub fn new(block: &Block) -> Result<Self, executor::Error> {
let l1_block_contract = Address::from_str(L1_BLOCK_CONTRACT).unwrap();

let l1_info_tx_data = block
.body
.iter()
.find(|tx| matches!(tx.kind(), TransactionKind::Call(to) if to == &l1_block_contract))
.ok_or(executor::Error::L1BlockInfoError {
message: "could not find l1 block info tx in the L2 block".to_string(),
})
.and_then(|tx| {
tx.input().get(4..).ok_or(executor::Error::L1BlockInfoError {
message: "could not get l1 block info tx calldata bytes".to_string(),
})
})?;

// The setL1BlockValues tx calldata must be exactly 184 bytes long, considering that
// we already removed the first 4 bytes (the function selector). Detailed breakdown:
// 8 bytes for the block number
// + 8 bytes for the block timestamp
// + 32 bytes for the base fee
// + 32 bytes for the block hash
// + 8 bytes for the block sequence number
// + 32 bytes for the batcher hash
// + 32 bytes for the fee overhead
// + 32 bytes for the fee scalar
if l1_info_tx_data.len() != 184 {
return Err(executor::Error::L1BlockInfoError {
message: "unexpected l1 block info tx calldata length found".to_string(),
})
}

let l1_base_fee = U256::try_from_le_slice(&l1_info_tx_data[16..48]).ok_or(
executor::Error::L1BlockInfoError {
message: "could not convert l1 base fee".to_string(),
},
)?;
let l1_fee_overhead = U256::try_from_le_slice(&l1_info_tx_data[120..152]).ok_or(
executor::Error::L1BlockInfoError {
message: "could not convert l1 fee overhead".to_string(),
},
)?;
let l1_fee_scalar = U256::try_from_le_slice(&l1_info_tx_data[152..184]).ok_or(
executor::Error::L1BlockInfoError {
message: "could not convert l1 fee scalar".to_string(),
},
)?;

Ok(Self { l1_base_fee, l1_fee_overhead, l1_fee_scalar })
}

/// Calculate the gas cost of a transaction based on L1 block data posted on L2
pub fn calculate_tx_l1_cost(&mut self, tx: &TransactionSigned) -> U256 {
let rollup_data_gas_cost = U256::from(tx.input().iter().fold(0, |acc, byte| {
acc + if byte == &0x00 { ZERO_BYTE_COST } else { NON_ZERO_BYTE_COST }
}));

if tx.is_deposit() || rollup_data_gas_cost == U256::ZERO {
return U256::ZERO
}

rollup_data_gas_cost
.saturating_add(self.l1_fee_overhead)
.saturating_mul(self.l1_base_fee)
.saturating_mul(self.l1_fee_scalar)
.checked_div(U256::from(1_000_000))
.unwrap_or_default()
}
}

/// Get the base fee recipient address
pub fn base_fee_recipient() -> Address {
Address::from_str(BASE_FEE_RECIPIENT).unwrap()
}

/// Get the L1 cost recipient address
pub fn l1_cost_recipient() -> Address {
Address::from_str(L1_FEE_RECIPIENT).unwrap()
}
1 change: 1 addition & 0 deletions crates/interfaces/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ secp256k1 = { version = "0.26.0", default-features = false, features = [

[features]
bench = []
optimism = []
test-utils = ["tokio-stream/sync", "secp256k1"]
9 changes: 8 additions & 1 deletion crates/interfaces/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use thiserror::Error;
pub enum Error {
#[error("EVM reported invalid transaction ({hash:?}): {message}")]
EVM { hash: H256, message: String },
#[error("Example of error.")]
#[error("Verification failed")]
VerificationFailed,
#[error("Fatal internal error")]
ExecutionFatalError,
Expand Down Expand Up @@ -64,4 +64,11 @@ pub enum Error {
CanonicalCommit { inner: String },
#[error("Transaction error on pipeline status update: {inner:?}")]
PipelineStatusUpdate { inner: String },

#[cfg(feature = "optimism")]
#[error("Could not get L1 block info from L2 block: {message:?}")]
L1BlockInfoError { message: String },
#[cfg(feature = "optimism")]
#[error("Insufficient funds to cover transaction L1 cost: want {want}, have {have}")]
InsufficientFundsForL1Cost { want: u64, have: u64 },
}
1 change: 1 addition & 0 deletions crates/net/eth-wire/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ proptest-derive = "0.3"
default = ["serde"]
serde = ["dep:serde", "smol_str/serde"]
arbitrary = ["reth-primitives/arbitrary", "dep:arbitrary", "dep:proptest", "dep:proptest-derive"]
optimism = []

[[test]]
name = "fuzz_roundtrip"
Expand Down
6 changes: 6 additions & 0 deletions crates/net/eth-wire/src/types/receipts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ mod test {
cumulative_gas_used: 0,
bloom: Default::default(),
logs: vec![],
#[cfg(feature = "optimism")]
deposit_nonce: None,
}]]);

let mut out = vec![];
Expand Down Expand Up @@ -108,6 +110,8 @@ mod test {
},
],
success: false,
#[cfg(feature = "optimism")]
deposit_nonce: None,
},
],
]),
Expand Down Expand Up @@ -142,6 +146,8 @@ mod test {
},
],
success: false,
#[cfg(feature = "optimism")]
deposit_nonce: None,
},
],
]),
Expand Down
2 changes: 2 additions & 0 deletions crates/primitives/src/proofs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ mod tests {
cumulative_gas_used: 102068,
bloom,
logs,
#[cfg(feature = "optimism")]
deposit_nonce: None,
};
let receipt = vec![receipt];
let root = calculate_receipt_root(receipt.iter());
Expand Down
Loading