Skip to content
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
13 changes: 8 additions & 5 deletions crates/pevm/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,21 @@ pub trait PevmChain: Debug {
true
}

/// Build [`MvMemory`]
fn build_mv_memory(&self, _block_env: &BlockEnv, txs: &[Self::EvmTx]) -> MvMemory {
MvMemory::new(txs.len(), [], [])
/// Build [`MvMemory`]. `seed` is the per-execution random seed that must be
/// mixed into every location hash & tag (see [`crate::hash_deterministic`]).
fn build_mv_memory(&self, _block_env: &BlockEnv, txs: &[Self::EvmTx], seed: u64) -> MvMemory {
MvMemory::new(seed, txs.len(), [], [])
}

/// Get rewards (balance increments) to beneficiary accounts, etc.
/// Get rewards (balance increments) to beneficiary accounts, etc. `seed` is
/// needed to derive the hash of any chain-specific fee recipients.
fn get_rewards(
&self,
beneficiary_location_hash: u64,
beneficiary_location_hash: MemoryLocationHash,
gas_used: U256,
gas_price: U256,
basefee: u64,
seed: u64,
tx: &Self::EvmTx,
) -> SmallVec<[(MemoryLocationHash, U256); 1]>;

Expand Down
14 changes: 10 additions & 4 deletions crates/pevm/src/chain/ethereum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,10 @@ impl PevmChain for PevmEthereum {
tx
}

fn build_mv_memory(&self, block_env: &BlockEnv, txs: &[TxEnv]) -> MvMemory {
fn build_mv_memory(&self, block_env: &BlockEnv, txs: &[TxEnv], seed: u64) -> MvMemory {
let block_size = txs.len();
let beneficiary_location_hash =
hash_deterministic(MemoryLocation::Basic(block_env.beneficiary));
hash_deterministic(MemoryLocation::Basic(block_env.beneficiary), seed);

// TODO: Estimate more locations based on sender, to, etc.
let mut estimated_locations = HashMap::with_hasher(BuildIdentityHasher::default());
Expand All @@ -175,15 +175,21 @@ impl PevmChain for PevmEthereum {
(0..block_size).collect::<Vec<TxIdx>>(),
);

MvMemory::new(block_size, estimated_locations, [block_env.beneficiary])
MvMemory::new(
seed,
block_size,
estimated_locations,
[block_env.beneficiary],
)
}

fn get_rewards(
&self,
beneficiary_location_hash: u64,
beneficiary_location_hash: MemoryLocationHash,
gas_used: U256,
gas_price: U256,
_: u64,
_: u64,
_: &Self::EvmTx,
) -> SmallVec<[(MemoryLocationHash, U256); 1]> {
smallvec::smallvec![(
Expand Down
83 changes: 39 additions & 44 deletions crates/pevm/src/chain/rise.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
//! RISE
use std::sync::LazyLock;

use alloy_consensus::Transaction;
use alloy_primitives::{Address, B256, ChainId, U256};
use alloy_rpc_types_eth::{BlockTransactions, Header};
Expand Down Expand Up @@ -30,14 +28,13 @@ use super::{CalculateReceiptRootError, PevmChain};

const RISE_CHAIN_ID: ChainId = 4153; // Mainnet

static BASE_FEE_RECIPIENT_LOCATION_HASH: LazyLock<MemoryLocationHash> =
LazyLock::new(|| hash_deterministic(MemoryLocation::Basic(BASE_FEE_RECIPIENT)));

static L1_FEE_RECIPIENT_LOCATION_HASH: LazyLock<MemoryLocationHash> =
LazyLock::new(|| hash_deterministic(MemoryLocation::Basic(L1_FEE_RECIPIENT)));

static OPERATOR_FEE_RECIPIENT_LOCATION_HASH: LazyLock<MemoryLocationHash> =
LazyLock::new(|| hash_deterministic(MemoryLocation::Basic(OPERATOR_FEE_RECIPIENT)));
// Hash a basic-account location under the per-block seed. The fee recipients are
// fixed addresses, but a per-execution seed means their hashes cannot be
// precomputed once, so we derive them per block here.
#[inline]
fn basic_location_hash(address: Address, seed: u64) -> MemoryLocationHash {
hash_deterministic(MemoryLocation::Basic(address), seed)
}

/// Implementation of [`PevmChain`] for RISE
#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -105,55 +102,50 @@ impl PevmChain for PevmRise {
.build_op()
}

fn build_mv_memory(&self, block_env: &BlockEnv, txs: &[OpTransaction<TxEnv>]) -> MvMemory {
let beneficiary_location_hash =
hash_deterministic(MemoryLocation::Basic(block_env.beneficiary));
fn build_mv_memory(
&self,
block_env: &BlockEnv,
txs: &[OpTransaction<TxEnv>],
seed: u64,
) -> MvMemory {
// The beneficiary and the three fee recipients are written by every
// non-deposit transaction, so we pre-seed them as estimates. Their hashes
// only depend on the static addresses and the block-level seed, so we
// compute them once instead of per transaction.
let recipients = [
block_env.beneficiary,
BASE_FEE_RECIPIENT,
L1_FEE_RECIPIENT,
OPERATOR_FEE_RECIPIENT,
];
let recipient_hashes = recipients.map(|address| basic_location_hash(address, seed));

// TODO: Estimate more locations based on sender, to, etc.
// TODO: Benchmark to check whether adding these estimated
// locations helps or harms the performance.
let mut estimated_locations = HashMap::with_hasher(BuildIdentityHasher::default());

for (index, tx) in txs.iter().enumerate() {
// Deposit transactions pay no fees so they write nothing to these fee recipients.
if !tx.is_deposit() {
estimated_locations
.entry(beneficiary_location_hash)
.or_insert_with(|| Vec::with_capacity(txs.len()))
.push(index);
estimated_locations
.entry(*BASE_FEE_RECIPIENT_LOCATION_HASH)
.or_insert_with(|| Vec::with_capacity(txs.len()))
.push(index);
estimated_locations
.entry(*L1_FEE_RECIPIENT_LOCATION_HASH)
.or_insert_with(|| Vec::with_capacity(txs.len()))
.push(index);
estimated_locations
.entry(*OPERATOR_FEE_RECIPIENT_LOCATION_HASH)
.or_insert_with(|| Vec::with_capacity(txs.len()))
.push(index);
for &location_hash in &recipient_hashes {
estimated_locations
.entry(location_hash)
.or_insert_with(|| Vec::with_capacity(txs.len()))
.push(index);
}
}
}
Comment on lines +115 to 137

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The hashes of the fee recipients (recipients) are constant for the entire block and only depend on the static addresses and the block-level seed. Recalculating them inside the transaction loop results in redundant hashing operations (2 FxHash passes per recipient per transaction). Precomputing these hashes once before the loop improves performance, especially for blocks with a large number of transactions.

        let recipients = [
            block_env.beneficiary,
            BASE_FEE_RECIPIENT,
            L1_FEE_RECIPIENT,
            OPERATOR_FEE_RECIPIENT,
        ];
        let recipient_hashes = [
            basic_location_hash(block_env.beneficiary, seed),
            basic_location_hash(BASE_FEE_RECIPIENT, seed),
            basic_location_hash(L1_FEE_RECIPIENT, seed),
            basic_location_hash(OPERATOR_FEE_RECIPIENT, seed),
        ];

        // TODO: Estimate more locations based on sender, to, etc.
        // TODO: Benchmark to check whether adding these estimated
        // locations helps or harms the performance.
        let mut estimated_locations = HashMap::with_hasher(BuildIdentityHasher::default());
        for (index, tx) in txs.iter().enumerate() {
            // Deposit transactions pay no fees so they write nothing to these fee recipients.
            if !tx.is_deposit() {
                for hash in recipient_hashes {
                    estimated_locations
                        .entry(hash)
                        .or_insert_with(|| Vec::with_capacity(txs.len()))
                        .push(index);
                }
            }
        }


MvMemory::new(
txs.len(),
estimated_locations,
[
block_env.beneficiary,
BASE_FEE_RECIPIENT,
L1_FEE_RECIPIENT,
OPERATOR_FEE_RECIPIENT,
],
)
MvMemory::new(seed, txs.len(), estimated_locations, recipients)
}

fn get_rewards(
&self,
beneficiary_location_hash: u64,
beneficiary_location_hash: MemoryLocationHash,
gas_used: U256,
gas_price: U256,
basefee: u64,
seed: u64,
tx: &Self::EvmTx,
) -> SmallVec<[(MemoryLocationHash, U256); 1]> {
if tx.is_deposit() {
Expand All @@ -165,14 +157,17 @@ impl PevmChain for PevmRise {
gas_price.saturating_mul(gas_used)
),
(
*BASE_FEE_RECIPIENT_LOCATION_HASH,
basic_location_hash(BASE_FEE_RECIPIENT, seed),
U256::from(basefee).saturating_mul(gas_used),
),
// RISE disables DA footprint and operator fees. Annoyingly, we still
// need to touch these to match revm's sequential execution for now.
// Will remove once we rewrite our own EVM implementation.
(*L1_FEE_RECIPIENT_LOCATION_HASH, U256::ZERO),
(*OPERATOR_FEE_RECIPIENT_LOCATION_HASH, U256::ZERO),
(basic_location_hash(L1_FEE_RECIPIENT, seed), U256::ZERO),
(
basic_location_hash(OPERATOR_FEE_RECIPIENT, seed),
U256::ZERO
),
]
}
}
Expand Down
27 changes: 22 additions & 5 deletions crates/pevm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ enum MemoryLocation {
// We then identify the locations with its hash in the multi-version
// data, write and read sets, which is much faster than rehashing
// on every single lookup & validation.
type MemoryLocationHash = u64;
//
// This is 128 bits wide so that distinct locations practically never collide
// (birthday bound ~2^64). Because this hash is also the key of the multi-version
// data, a collision would silently merge two locations into one entry; a 128-bit
// key makes the map's own key comparison a reliable identity check. See
// [hash_deterministic] for how the two halves are derived.
type MemoryLocationHash = u128;

/// This is primarily used for memory location hash, but can also be used for
/// transaction indexes, etc.
Expand All @@ -51,6 +57,11 @@ impl Hasher for IdentityHasher {
fn write_u64(&mut self, id: u64) {
self.0 = id;
}
fn write_u128(&mut self, id: u128) {
// Use the low 64 bits as the bucket hash. The full 128-bit key is still
// compared for equality by the map, so this only affects bucket spread.
self.0 = id as u64;
}
fn write_usize(&mut self, id: usize) {
self.0 = id as u64;
}
Expand All @@ -65,11 +76,17 @@ impl Hasher for IdentityHasher {
/// Build an identity hasher
pub type BuildIdentityHasher = BuildHasherDefault<IdentityHasher>;

// TODO: Ensure it's not easy to hand-craft transactions and storage slots
// that can cause a lot of collisions that destroys pevm's performance.
// Hash a memory location into a 128-bit value, mixed with a per-execution random
// [seed]. The result is built from two FxHash passes over the same location with
// decorrelated seeds (`seed` and `!seed`) forming the low and high 64 bits, so the
// two halves collide independently and the full 128-bit value only collides at
// ~2^-128. The seed also makes collisions impossible for an attacker to aim at, as
// it is unknown until the block runs.
#[inline(always)]
fn hash_deterministic<T: Hash>(x: T) -> u64 {
FxBuildHasher.hash_one(x)
fn hash_deterministic<T: Hash>(x: T, seed: u64) -> MemoryLocationHash {
let low = FxBuildHasher.hash_one((seed, &x));
let high = FxBuildHasher.hash_one((!seed, &x));
(u128::from(high) << 64) | u128::from(low)
}

// TODO: It would be nice if we could tie the different cases of
Expand Down
5 changes: 5 additions & 0 deletions crates/pevm/src/mv_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,14 @@ pub struct MvMemory {
lazy_addresses: Mutex<LazyAddresses>,
/// New bytecodes deployed in this block
pub(crate) new_bytecodes: DashMap<B256, Bytecode, BuildSuffixHasher>,
/// Per-execution random seed mixed into every location hash & tag. Unknown to
/// transaction submitters until the block runs, so collisions cannot be aimed at.
pub(crate) seed: u64,
}

impl MvMemory {
pub(crate) fn new(
seed: u64,
block_size: usize,
estimated_locations: impl IntoIterator<Item = (MemoryLocationHash, Vec<TxIdx>)>,
lazy_addresses: impl IntoIterator<Item = Address>,
Expand Down Expand Up @@ -70,6 +74,7 @@ impl MvMemory {
// TODO: Fine-tune the number of shards, like to the next number of two from the
// number of worker threads.
new_bytecodes: DashMap::default(),
seed,
}
}

Expand Down
11 changes: 9 additions & 2 deletions crates/pevm/src/pevm.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{
cell::UnsafeCell,
fmt::Debug,
hash::{BuildHasher, RandomState},
num::NonZeroUsize,
sync::{OnceLock, mpsc},
thread,
Expand Down Expand Up @@ -224,7 +225,13 @@ impl Pevm {
let block_size = txs.len();
let scheduler = Scheduler::new(block_size);

let mv_memory = chain.build_mv_memory(&block_env, &txs);
// A per-execution random seed mixed into every location hash & tag. It is
// unknown to transaction submitters until the block runs, so hash collisions
// cannot be crafted to target this block. The seed never escapes into the
// results (state is keyed by real addresses), so output is identical for any
// seed. We derive a u64 from the OS-seeded [RandomState] to avoid a dependency.
let seed = RandomState::new().hash_one(0u8);
let mv_memory = chain.build_mv_memory(&block_env, &txs, seed);

self.execution_results.grow_to(block_size);

Expand Down Expand Up @@ -288,7 +295,7 @@ impl Pevm {
// We fully evaluate (the balance and nonce of) the beneficiary account
// and raw transfer recipients that may have been atomically updated.
for address in mv_memory.consume_lazy_addresses() {
let location_hash = hash_deterministic(MemoryLocation::Basic(address));
let location_hash = hash_deterministic(MemoryLocation::Basic(address), mv_memory.seed);
if let Some(write_history) = mv_memory.data.get(&location_hash) {
let mut balance = U256::ZERO;
let mut nonce = 0;
Expand Down
Loading
Loading