diff --git a/Cargo.lock b/Cargo.lock index 0b3318c4ae0..1bae07a4ebf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,6 +1341,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bn254" +version = "0.1.0" +source = "git+https://github.com/scroll-tech/bn254.git?branch=master#81e1dcc92ee9a2798b13b84b24de182e9c42256e" +dependencies = [ + "ff", + "getrandom 0.2.15", + "rand 0.8.5", + "rand_core 0.6.4", + "sp1-intrinsics", + "subtle", +] + [[package]] name = "boa_ast" version = "0.19.1" @@ -3140,6 +3153,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ + "bitvec", "rand_core 0.6.4", "subtle", ] @@ -5691,6 +5705,16 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +[[package]] +name = "poseidon-bn254" +version = "0.1.0" +source = "git+https://github.com/scroll-tech/poseidon-bn254?branch=master#254baa0e3e85c0975c16fe6f33042b1fa9ae0a44" +dependencies = [ + "bn254", + "itertools 0.13.0", + "sp1-intrinsics", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -8480,8 +8504,9 @@ dependencies = [ "proptest-arbitrary-interop", "rand 0.8.5", "reth-codecs", + "reth-scroll-primitives", + "reth-scroll-revm", "reth-testing-utils", - "revm-primitives", "roaring", "serde", "serde_json", @@ -8938,6 +8963,35 @@ dependencies = [ "serde_json", ] +[[package]] +name = "reth-scroll-primitives" +version = "1.1.0" +dependencies = [ + "poseidon-bn254", + "revm", +] + +[[package]] +name = "reth-scroll-revm" +version = "1.1.0" +dependencies = [ + "reth-scroll-primitives", + "revm", +] + +[[package]] +name = "reth-scroll-storage" +version = "1.1.0" +dependencies = [ + "alloy-primitives", + "eyre", + "reth-primitives-traits", + "reth-revm", + "reth-scroll-primitives", + "reth-scroll-revm", + "reth-storage-errors", +] + [[package]] name = "reth-stages" version = "1.1.0" @@ -10231,6 +10285,14 @@ dependencies = [ "sha1", ] +[[package]] +name = "sp1-intrinsics" +version = "0.0.0" +source = "git+https://github.com/scroll-tech/sp1-intrinsics.git?branch=master#7e038e60db0b2e847f6d8f49e148ccac8c6fc394" +dependencies = [ + "cfg-if", +] + [[package]] name = "spin" version = "0.9.8" diff --git a/Cargo.toml b/Cargo.toml index 8c5cf7b0cd5..7ca4430c24e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,9 @@ members = [ "crates/rpc/rpc-testing-util/", "crates/rpc/rpc-types-compat/", "crates/rpc/rpc/", + "crates/scroll/primitives", + "crates/scroll/revm", + "crates/scroll/storage", "crates/stages/api/", "crates/stages/stages/", "crates/stages/types/", @@ -400,6 +403,9 @@ reth-rpc-eth-types = { path = "crates/rpc/rpc-eth-types", default-features = fal reth-rpc-layer = { path = "crates/rpc/rpc-layer" } reth-rpc-server-types = { path = "crates/rpc/rpc-server-types" } reth-rpc-types-compat = { path = "crates/rpc/rpc-types-compat" } +reth-scroll-primitives = { path = "crates/scroll/primitives" } +reth-scroll-revm = { path = "crates/scroll/revm" } +reth-scroll-storage = { path = "crates/scroll/storage" } reth-stages = { path = "crates/stages/stages" } reth-stages-api = { path = "crates/stages/api" } reth-stages-types = { path = "crates/stages/types" } diff --git a/crates/primitives-traits/Cargo.toml b/crates/primitives-traits/Cargo.toml index 6cafe8b8b1e..2aa438659dc 100644 --- a/crates/primitives-traits/Cargo.toml +++ b/crates/primitives-traits/Cargo.toml @@ -20,7 +20,9 @@ alloy-genesis.workspace = true alloy-primitives.workspace = true alloy-rlp.workspace = true -revm-primitives = { workspace = true, features = ["serde"] } +# revm-primitives scroll re-export +revm-primitives = { package = "reth-scroll-revm", path = "../scroll/revm", features = ["serde"] } +reth-scroll-primitives = { workspace = true, optional = true } # misc byteorder = "1" @@ -81,3 +83,4 @@ serde-bincode-compat = [ "alloy-consensus/serde-bincode-compat", "alloy-eips/serde-bincode-compat" ] +scroll = ["reth-scroll-primitives"] diff --git a/crates/primitives-traits/src/account.rs b/crates/primitives-traits/src/account.rs index ae58973edd7..dc256d5bf07 100644 --- a/crates/primitives-traits/src/account.rs +++ b/crates/primitives-traits/src/account.rs @@ -5,7 +5,9 @@ use byteorder::{BigEndian, ReadBytesExt}; use bytes::Buf; use derive_more::Deref; use reth_codecs::{add_arbitrary_tests, Compact}; -use revm_primitives::{AccountInfo, Bytecode as RevmBytecode, BytecodeDecodeError, JumpTable}; +use revm_primitives::{ + AccountInfo, Bytecode as RevmBytecode, BytecodeDecodeError, JumpTable, ScrollAccountInfo, +}; use serde::{Deserialize, Serialize}; /// Identifier for [`LegacyRaw`](RevmBytecode::LegacyRaw). @@ -34,6 +36,12 @@ pub struct Account { pub balance: U256, /// Hash of the account's bytecode. pub bytecode_hash: Option, + #[cfg(feature = "scroll")] + /// Size of the account's code in bytes. + pub code_size: u64, + #[cfg(feature = "scroll")] + /// Poseidon hash of the account's bytecode. + pub poseidon_code_hash: B256, } impl Account { @@ -158,6 +166,29 @@ impl From<&GenesisAccount> for Account { nonce: value.nonce.unwrap_or_default(), balance: value.balance, bytecode_hash: value.code.as_ref().map(keccak256), + #[cfg(feature = "scroll")] + code_size: value.code.as_ref().map(|c| c.len()).unwrap_or_default() as u64, + #[cfg(feature = "scroll")] + poseidon_code_hash: value + .code + .as_ref() + .map(|c| reth_scroll_primitives::poseidon(c)) + .unwrap_or_default(), + } + } +} + +impl From for Account { + fn from(revm_acc: ScrollAccountInfo) -> Self { + let code_hash = revm_acc.code_hash; + Self { + balance: revm_acc.balance, + nonce: revm_acc.nonce, + bytecode_hash: (code_hash != KECCAK_EMPTY).then_some(code_hash), + #[cfg(feature = "scroll")] + code_size: revm_acc.code_size, + #[cfg(feature = "scroll")] + poseidon_code_hash: revm_acc.poseidon_code_hash, } } } @@ -169,6 +200,25 @@ impl From for Account { balance: revm_acc.balance, nonce: revm_acc.nonce, bytecode_hash: (code_hash != KECCAK_EMPTY).then_some(code_hash), + #[cfg(feature = "scroll")] + code_size: 0, + #[cfg(feature = "scroll")] + poseidon_code_hash: B256::ZERO, + } + } +} + +impl From for ScrollAccountInfo { + fn from(reth_acc: Account) -> Self { + Self { + balance: reth_acc.balance, + nonce: reth_acc.nonce, + code_hash: reth_acc.bytecode_hash.unwrap_or(KECCAK_EMPTY), + code: None, + #[cfg(feature = "scroll")] + code_size: reth_acc.code_size, + #[cfg(feature = "scroll")] + poseidon_code_hash: reth_acc.poseidon_code_hash, } } } @@ -208,7 +258,8 @@ mod tests { #[test] fn test_empty_account() { - let mut acc = Account { nonce: 0, balance: U256::ZERO, bytecode_hash: None }; + let mut acc = + Account { nonce: 0, balance: U256::ZERO, bytecode_hash: None, ..Default::default() }; // Nonce 0, balance 0, and bytecode hash set to None is considered empty. assert!(acc.is_empty()); @@ -260,12 +311,21 @@ mod tests { #[test] fn test_account_has_bytecode() { // Account with no bytecode (None) - let acc_no_bytecode = Account { nonce: 1, balance: U256::from(1000), bytecode_hash: None }; + let acc_no_bytecode = Account { + nonce: 1, + balance: U256::from(1000), + bytecode_hash: None, + ..Default::default() + }; assert!(!acc_no_bytecode.has_bytecode(), "Account should not have bytecode"); // Account with bytecode hash set to KECCAK_EMPTY (should have bytecode) - let acc_empty_bytecode = - Account { nonce: 1, balance: U256::from(1000), bytecode_hash: Some(KECCAK_EMPTY) }; + let acc_empty_bytecode = Account { + nonce: 1, + balance: U256::from(1000), + bytecode_hash: Some(KECCAK_EMPTY), + ..Default::default() + }; assert!(acc_empty_bytecode.has_bytecode(), "Account should have bytecode"); // Account with a non-empty bytecode hash @@ -273,6 +333,7 @@ mod tests { nonce: 1, balance: U256::from(1000), bytecode_hash: Some(B256::from_slice(&[0x11u8; 32])), + ..Default::default() }; assert!(acc_with_bytecode.has_bytecode(), "Account should have bytecode"); } @@ -280,12 +341,17 @@ mod tests { #[test] fn test_account_get_bytecode_hash() { // Account with no bytecode (should return KECCAK_EMPTY) - let acc_no_bytecode = Account { nonce: 0, balance: U256::ZERO, bytecode_hash: None }; + let acc_no_bytecode = + Account { nonce: 0, balance: U256::ZERO, bytecode_hash: None, ..Default::default() }; assert_eq!(acc_no_bytecode.get_bytecode_hash(), KECCAK_EMPTY, "Should return KECCAK_EMPTY"); // Account with bytecode hash set to KECCAK_EMPTY - let acc_empty_bytecode = - Account { nonce: 1, balance: U256::from(1000), bytecode_hash: Some(KECCAK_EMPTY) }; + let acc_empty_bytecode = Account { + nonce: 1, + balance: U256::from(1000), + bytecode_hash: Some(KECCAK_EMPTY), + ..Default::default() + }; assert_eq!( acc_empty_bytecode.get_bytecode_hash(), KECCAK_EMPTY, @@ -294,8 +360,12 @@ mod tests { // Account with a valid bytecode hash let bytecode_hash = B256::from_slice(&[0x11u8; 32]); - let acc_with_bytecode = - Account { nonce: 1, balance: U256::from(1000), bytecode_hash: Some(bytecode_hash) }; + let acc_with_bytecode = Account { + nonce: 1, + balance: U256::from(1000), + bytecode_hash: Some(bytecode_hash), + ..Default::default() + }; assert_eq!( acc_with_bytecode.get_bytecode_hash(), bytecode_hash, diff --git a/crates/scroll/primitives/Cargo.toml b/crates/scroll/primitives/Cargo.toml new file mode 100644 index 00000000000..63ef63fce03 --- /dev/null +++ b/crates/scroll/primitives/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "reth-scroll-primitives" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +# revm +revm.workspace = true + +# scroll +poseidon-bn254 = { git = "https://github.com/scroll-tech/poseidon-bn254", branch = "master", features = ["bn254"] } diff --git a/crates/scroll/primitives/src/execution_context.rs b/crates/scroll/primitives/src/execution_context.rs new file mode 100644 index 00000000000..9afde1bc888 --- /dev/null +++ b/crates/scroll/primitives/src/execution_context.rs @@ -0,0 +1,12 @@ +use revm::primitives::{map::HashMap, B256}; + +/// A Keccak code hash. +type KeccakHash = B256; +/// A Poseidon code hash. +type PoseidonHash = B256; +/// Size of a contract's code in bytes. +type CodeSize = u64; + +/// Scroll post execution context maps a Keccak code hash of a contract's bytecode to its code size +/// and Poseidon code hash. +pub type ScrollPostExecutionContext = HashMap; diff --git a/crates/scroll/primitives/src/lib.rs b/crates/scroll/primitives/src/lib.rs new file mode 100644 index 00000000000..189f6cd26ae --- /dev/null +++ b/crates/scroll/primitives/src/lib.rs @@ -0,0 +1,7 @@ +//! Primitive types for the Scroll extension of `Reth`. + +pub use execution_context::ScrollPostExecutionContext; +mod execution_context; + +pub use poseidon::{poseidon, POSEIDON_EMPTY}; +mod poseidon; diff --git a/crates/scroll/primitives/src/poseidon.rs b/crates/scroll/primitives/src/poseidon.rs new file mode 100644 index 00000000000..9058d7db3dc --- /dev/null +++ b/crates/scroll/primitives/src/poseidon.rs @@ -0,0 +1,10 @@ +use revm::primitives::{b256, B256}; + +/// The Poseidon hash of the empty string `""`. +pub const POSEIDON_EMPTY: B256 = + b256!("2098f5fb9e239eab3ceac3f27b81e481dc3124d55ffed523a839ee8446b64864"); + +/// Poseidon code hash +pub fn poseidon(code: &[u8]) -> B256 { + poseidon_bn254::hash_code(code).into() +} diff --git a/crates/scroll/revm/Cargo.toml b/crates/scroll/revm/Cargo.toml new file mode 100644 index 00000000000..a10608d2264 --- /dev/null +++ b/crates/scroll/revm/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "reth-scroll-revm" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +# revm +revm.workspace = true + +# scroll +reth-scroll-primitives.workspace = true + +[features] +default = ["std"] + +arbitrary = ["revm/arbitrary"] + +serde = ["revm/serde"] + +std = ["revm/std"] diff --git a/crates/scroll/revm/src/lib.rs b/crates/scroll/revm/src/lib.rs new file mode 100644 index 00000000000..ac2e6a6cef0 --- /dev/null +++ b/crates/scroll/revm/src/lib.rs @@ -0,0 +1,9 @@ +//! Scroll `revm` types redefinitions. Account types are redefined with two additional fields +//! `code_size` and `poseidon_code_hash`, which are used during computation of the state root. + +pub mod states; + +pub mod primitives; + +pub use primitives::ScrollAccountInfo; +pub use revm::primitives::*; diff --git a/crates/scroll/revm/src/primitives/mod.rs b/crates/scroll/revm/src/primitives/mod.rs new file mode 100644 index 00000000000..b2481459baf --- /dev/null +++ b/crates/scroll/revm/src/primitives/mod.rs @@ -0,0 +1,139 @@ +//! Scroll `revm` primitives types redefinitions. + +use reth_scroll_primitives::{poseidon, ScrollPostExecutionContext, POSEIDON_EMPTY}; +use revm::primitives::{AccountInfo, Bytecode, B256, KECCAK_EMPTY, U256}; + +/// The Scroll account information. Code copy of [`AccountInfo`]. Provides additional `code_size` +/// and `poseidon_code_hash` fields needed in the state root computation. +#[derive(Clone, Debug, Eq)] +pub struct ScrollAccountInfo { + /// Account balance. + pub balance: U256, + /// Account nonce. + pub nonce: u64, + /// Account code keccak hash. + pub code_hash: B256, + /// code: if None, `code_by_hash` will be used to fetch it if code needs to be loaded from + /// inside `revm`. + pub code: Option, + /// Account code size. + pub code_size: u64, + /// Account code Poseidon hash. [`POSEIDON_EMPTY`] if code is None or empty. + pub poseidon_code_hash: B256, +} + +impl From<(AccountInfo, &ScrollPostExecutionContext)> for ScrollAccountInfo { + fn from((info, context): (AccountInfo, &ScrollPostExecutionContext)) -> Self { + let (code_size, poseidon_code_hash) = + context.get(&info.code_hash).copied().unwrap_or((0, POSEIDON_EMPTY)); + Self { + balance: info.balance, + nonce: info.nonce, + code_hash: info.code_hash, + code: info.code, + code_size, + poseidon_code_hash, + } + } +} + +impl Default for ScrollAccountInfo { + fn default() -> Self { + Self { + balance: U256::ZERO, + code_hash: KECCAK_EMPTY, + code: Some(Bytecode::default()), + nonce: 0, + code_size: 0, + poseidon_code_hash: POSEIDON_EMPTY, + } + } +} + +impl PartialEq for ScrollAccountInfo { + fn eq(&self, other: &Self) -> bool { + self.balance == other.balance && + self.nonce == other.nonce && + self.code_hash == other.code_hash + } +} + +impl ScrollAccountInfo { + /// Creates a new [`ScrollAccountInfo`] with the given fields. + pub fn new( + balance: U256, + nonce: u64, + code_hash: B256, + code: Bytecode, + poseidon_code_hash: B256, + ) -> Self { + let code_size = code.len() as u64; + Self { balance, nonce, code: Some(code), code_hash, code_size, poseidon_code_hash } + } + + /// Returns account info without the code. + pub fn without_code(mut self) -> Self { + self.take_bytecode(); + self + } + + /// Returns if an account is empty. + /// + /// An account is empty if the following conditions are met. + /// - code hash is zero or set to the Keccak256 hash of the empty string `""` + /// - balance is zero + /// - nonce is zero + pub fn is_empty(&self) -> bool { + let code_empty = self.is_empty_code_hash() || self.code_hash.is_zero(); + code_empty && self.balance.is_zero() && self.nonce == 0 + } + + /// Returns `true` if the account is not empty. + pub fn exists(&self) -> bool { + !self.is_empty() + } + + /// Returns `true` if account has no nonce and code. + pub fn has_no_code_and_nonce(&self) -> bool { + self.is_empty_code_hash() && self.nonce == 0 + } + + /// Return bytecode hash associated with this account. + /// If account does not have code, it returns `KECCAK_EMPTY` hash. + pub const fn code_hash(&self) -> B256 { + self.code_hash + } + + /// Returns true if the code hash is the Keccak256 hash of the empty string `""`. + #[inline] + pub fn is_empty_code_hash(&self) -> bool { + self.code_hash == KECCAK_EMPTY + } + + /// Take bytecode from account. Code will be set to None. + pub fn take_bytecode(&mut self) -> Option { + self.code.take() + } + + /// Returns a [`ScrollAccountInfo`] with only balance. + pub fn from_balance(balance: U256) -> Self { + Self { balance, ..Default::default() } + } + + /// Returns a [`ScrollAccountInfo`] with defaults for balance and nonce. + /// Computes the Keccak and Poseidon hash of the provided bytecode. + pub fn from_bytecode(bytecode: Bytecode) -> Self { + let hash = bytecode.hash_slow(); + let code_size = bytecode.len() as u64; + let poseidon_code_hash = poseidon(bytecode.bytecode()); + + Self { + balance: U256::ZERO, + nonce: 1, + code: Some(bytecode), + code_hash: hash, + code_size, + poseidon_code_hash, + } + } +} diff --git a/crates/scroll/revm/src/states/bundle.rs b/crates/scroll/revm/src/states/bundle.rs new file mode 100644 index 00000000000..f440535277b --- /dev/null +++ b/crates/scroll/revm/src/states/bundle.rs @@ -0,0 +1,171 @@ +use super::bundle_account::ScrollBundleAccount; +use crate::{ + primitives::ScrollAccountInfo, + states::{ + changes::{ScrollPlainStateReverts, ScrollStateChangeset}, + reverts::ScrollReverts, + }, +}; +use reth_scroll_primitives::{poseidon, ScrollPostExecutionContext}; +use revm::{ + db::{states::PlainStorageChangeset, BundleState, OriginalValuesKnown}, + primitives::{map::HashMap, Address, Bytecode, B256, KECCAK_EMPTY}, +}; + +/// An equivalent of the [`BundleState`] modified with Scroll compatible fields. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct ScrollBundleState { + /// Account state. + pub state: HashMap, + /// All created contracts in this block. + pub contracts: HashMap, + /// Changes to revert. + /// + /// Note: Inside vector is *not* sorted by address. + /// But it is unique by address. + pub reverts: ScrollReverts, + /// The size of the plain state in the bundle state. + pub state_size: usize, + /// The size of reverts in the bundle state. + pub reverts_size: usize, +} + +impl From<(BundleState, ScrollPostExecutionContext)> for ScrollBundleState { + fn from((bundle, mut context): (BundleState, ScrollPostExecutionContext)) -> Self { + // Iterate the newly deployed contracts from the bundle and insert them in the context. + for (hash, code) in &bundle.contracts { + context.entry(*hash).or_insert_with(|| (code.len() as u64, poseidon(code.bytecode()))); + } + + let reverts = bundle + .reverts + .iter() + .map(|reverts| { + reverts + .iter() + .map(|(add, revert)| (*add, (revert.clone(), &context).into())) + .collect() + }) + .collect(); + + let state = bundle + .state + .into_iter() + .map(|(add, account)| (add, (account, &context).into())) + .collect(); + + Self { + state, + contracts: bundle.contracts, + reverts: ScrollReverts::new(reverts), + state_size: bundle.state_size, + reverts_size: bundle.reverts_size, + } + } +} + +impl ScrollBundleState { + /// Returns the approximate size of changes in the bundle state. + /// The estimation is not precise, because the information about the number of + /// destroyed entries that need to be removed is not accessible to the bundle state. + pub fn size_hint(&self) -> usize { + self.state_size + self.reverts_size + self.contracts.len() + } + + /// Return reference to the state. + pub const fn state(&self) -> &HashMap { + &self.state + } + + /// Is bundle state empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Return number of changed accounts. + pub fn len(&self) -> usize { + self.state.len() + } + + /// Get account from state + pub fn account(&self, address: &Address) -> Option<&ScrollBundleAccount> { + self.state.get(address) + } + + /// Get bytecode from state + pub fn bytecode(&self, hash: &B256) -> Option { + self.contracts.get(hash).cloned() + } + + /// Consume the bundle state and return plain state. + pub fn into_plain_state(self, is_value_known: OriginalValuesKnown) -> ScrollStateChangeset { + // pessimistically pre-allocate assuming _all_ accounts changed. + let state_len = self.state.len(); + let mut accounts = Vec::with_capacity(state_len); + let mut storage = Vec::with_capacity(state_len); + + for (address, account) in self.state { + // append account info if it is changed. + let was_destroyed = account.was_destroyed(); + if is_value_known.is_not_known() || account.is_info_changed() { + let info = account.info.map(ScrollAccountInfo::without_code); + accounts.push((address, info)); + } + + // append storage changes + + // NOTE: Assumption is that revert is going to remove whole plain storage from + // database so we can check if plain state was wiped or not. + let mut account_storage_changed = Vec::with_capacity(account.storage.len()); + + for (key, slot) in account.storage { + // If storage was destroyed that means that storage was wiped. + // In that case we need to check if present storage value is different then ZERO. + let destroyed_and_not_zero = was_destroyed && !slot.present_value.is_zero(); + + // If account is not destroyed check if original values was changed, + // so we can update it. + let not_destroyed_and_changed = !was_destroyed && slot.is_changed(); + + if is_value_known.is_not_known() || + destroyed_and_not_zero || + not_destroyed_and_changed + { + account_storage_changed.push((key, slot.present_value)); + } + } + + if !account_storage_changed.is_empty() || was_destroyed { + // append storage changes to account. + storage.push(PlainStorageChangeset { + address, + wipe_storage: was_destroyed, + storage: account_storage_changed, + }); + } + } + let contracts = self + .contracts + .into_iter() + // remove empty bytecodes + .filter(|(b, _)| *b != KECCAK_EMPTY) + .collect::>(); + ScrollStateChangeset { accounts, storage, contracts } + } + + /// Consume the bundle state and split it into reverts and plain state. + pub fn into_plain_state_and_reverts( + mut self, + is_value_known: OriginalValuesKnown, + ) -> (ScrollStateChangeset, ScrollPlainStateReverts) { + let reverts = self.take_all_reverts(); + let plain_state = self.into_plain_state(is_value_known); + (plain_state, reverts.into_plain_state_reverts()) + } + + /// Return and clear all reverts from [`ScrollBundleState`] + pub fn take_all_reverts(&mut self) -> ScrollReverts { + self.reverts_size = 0; + core::mem::take(&mut self.reverts) + } +} diff --git a/crates/scroll/revm/src/states/bundle_account.rs b/crates/scroll/revm/src/states/bundle_account.rs new file mode 100644 index 00000000000..7564d0f90bd --- /dev/null +++ b/crates/scroll/revm/src/states/bundle_account.rs @@ -0,0 +1,85 @@ +use crate::primitives::ScrollAccountInfo; +use reth_scroll_primitives::ScrollPostExecutionContext; +use revm::{ + db::{AccountStatus, BundleAccount, StorageWithOriginalValues}, + interpreter::primitives::U256, +}; + +/// The scroll account bundle. Originally defined in [`BundleAccount`], a +/// scroll version of the bundle is needed for the [`crate::states::ScrollBundleState`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScrollBundleAccount { + /// The current account's information + pub info: Option, + /// The original account's information + pub original_info: Option, + /// Contains both original and present state. + /// When extracting changeset we compare if original value is different from present value. + /// If it is different we add it to changeset. + /// + /// If Account was destroyed we ignore original value and compare present state with + /// [`U256::ZERO`]. + pub storage: StorageWithOriginalValues, + /// Account status. + pub status: AccountStatus, +} + +impl From<(BundleAccount, &ScrollPostExecutionContext)> for ScrollBundleAccount { + fn from((account, context): (BundleAccount, &ScrollPostExecutionContext)) -> Self { + let info = account.info.map(|info| (info, context).into()); + let original_info = account.original_info.map(|info| (info, context).into()); + Self { info, original_info, storage: account.storage, status: account.status } + } +} + +impl ScrollBundleAccount { + /// Creates a [`ScrollBundleAccount`]. + pub const fn new( + original_info: Option, + present_info: Option, + storage: StorageWithOriginalValues, + status: AccountStatus, + ) -> Self { + Self { info: present_info, original_info, storage, status } + } + + /// The approximate size of changes needed to store this account. + /// `1 + storage_len` + pub fn size_hint(&self) -> usize { + 1 + self.storage.len() + } + + /// Return storage slot if it exists. + /// + /// In case we know that account is newly created or destroyed, return `Some(U256::ZERO)` + pub fn storage_slot(&self, slot: U256) -> Option { + let slot = self.storage.get(&slot).map(|s| s.present_value); + if slot.is_some() { + slot + } else if self.status.is_storage_known() { + Some(U256::ZERO) + } else { + None + } + } + + /// Fetch account info if it exists. + pub fn account_info(&self) -> Option { + self.info.clone() + } + + /// Was this account destroyed. + pub fn was_destroyed(&self) -> bool { + self.status.was_destroyed() + } + + /// Return true of account info was changed. + pub fn is_info_changed(&self) -> bool { + self.info != self.original_info + } + + /// Return true if contract was changed + pub fn is_contract_changed(&self) -> bool { + self.info.as_ref().map(|a| a.code_hash) != self.original_info.as_ref().map(|a| a.code_hash) + } +} diff --git a/crates/scroll/revm/src/states/changes.rs b/crates/scroll/revm/src/states/changes.rs new file mode 100644 index 00000000000..a5265ab15ec --- /dev/null +++ b/crates/scroll/revm/src/states/changes.rs @@ -0,0 +1,36 @@ +use crate::primitives::ScrollAccountInfo; +use revm::{ + db::states::{PlainStorageChangeset, PlainStorageRevert}, + primitives::{Address, Bytecode, B256}, +}; + +/// Code copy equivalent of the [`revm::db::states::changes::StateChangeset`] to accommodate for the +/// [`ScrollAccountInfo`]. +#[derive(Debug)] +pub struct ScrollStateChangeset { + /// Vector of **not** sorted accounts information. + pub accounts: Vec<(Address, Option)>, + /// Vector of **not** sorted storage. + pub storage: Vec, + /// Vector of contracts by bytecode hash. **not** sorted. + pub contracts: Vec<(B256, Bytecode)>, +} + +/// Code copy of the [`revm::db::states::changes::PlainStateReverts`] to accommodate for +/// [`ScrollAccountInfo`]. +#[derive(Clone, Debug, Default)] +pub struct ScrollPlainStateReverts { + /// Vector of account with removed contracts bytecode + /// + /// Note: If [`ScrollAccountInfo`] is None means that account needs to be removed. + pub accounts: Vec)>>, + /// Vector of storage with its address. + pub storage: Vec>, +} + +impl ScrollPlainStateReverts { + /// Constructs new [`ScrollPlainStateReverts`] with pre-allocated capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self { accounts: Vec::with_capacity(capacity), storage: Vec::with_capacity(capacity) } + } +} diff --git a/crates/scroll/revm/src/states/mod.rs b/crates/scroll/revm/src/states/mod.rs new file mode 100644 index 00000000000..e8ca9b3550a --- /dev/null +++ b/crates/scroll/revm/src/states/mod.rs @@ -0,0 +1,13 @@ +//! Scroll `revm` states types redefinitions. + +pub use bundle::ScrollBundleState; +mod bundle; + +pub use bundle_account::ScrollBundleAccount; +mod bundle_account; + +pub use changes::{ScrollPlainStateReverts, ScrollStateChangeset}; +mod changes; + +pub use reverts::{ScrollAccountInfoRevert, ScrollAccountRevert, ScrollReverts}; +mod reverts; diff --git a/crates/scroll/revm/src/states/reverts.rs b/crates/scroll/revm/src/states/reverts.rs new file mode 100644 index 00000000000..abb8306b6ac --- /dev/null +++ b/crates/scroll/revm/src/states/reverts.rs @@ -0,0 +1,115 @@ +use crate::{primitives::ScrollAccountInfo, states::changes::ScrollPlainStateReverts}; +use reth_scroll_primitives::ScrollPostExecutionContext; +use revm::{ + db::{ + states::{reverts::AccountInfoRevert, PlainStorageRevert}, + AccountRevert, AccountStatus, RevertToSlot, + }, + primitives::{map::HashMap, Address, U256}, +}; + +/// Code copy of a [`revm::db::states::reverts::Reverts`] compatible with the +/// [`crate::states::ScrollBundleState`]. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ScrollReverts(Vec>); + +impl ScrollReverts { + /// Create new reverts + pub const fn new(reverts: Vec>) -> Self { + Self(reverts) + } + + /// Consume reverts and create plain state reverts. + /// + /// Note that account are sorted by address. + pub fn into_plain_state_reverts(mut self) -> ScrollPlainStateReverts { + let mut state_reverts = ScrollPlainStateReverts::with_capacity(self.0.len()); + for reverts in self.0.drain(..) { + // pessimistically pre-allocate assuming _all_ accounts changed. + let mut accounts = Vec::with_capacity(reverts.len()); + let mut storage = Vec::with_capacity(reverts.len()); + for (address, revert_account) in reverts { + match revert_account.account { + ScrollAccountInfoRevert::RevertTo(acc) => accounts.push((address, Some(acc))), + ScrollAccountInfoRevert::DeleteIt => accounts.push((address, None)), + ScrollAccountInfoRevert::DoNothing => (), + } + if revert_account.wipe_storage || !revert_account.storage.is_empty() { + storage.push(PlainStorageRevert { + address, + wiped: revert_account.wipe_storage, + storage_revert: revert_account.storage.into_iter().collect::>(), + }); + } + } + state_reverts.accounts.push(accounts); + state_reverts.storage.push(storage); + } + state_reverts + } +} + +/// Code copy of a [`AccountRevert`] compatible with [`ScrollReverts`]. +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct ScrollAccountRevert { + /// Account info revert + pub account: ScrollAccountInfoRevert, + /// Storage revert + pub storage: HashMap, + /// Previous status + pub previous_status: AccountStatus, + /// If true wipes storage + pub wipe_storage: bool, +} + +impl From<(AccountRevert, &ScrollPostExecutionContext)> for ScrollAccountRevert { + fn from((account, context): (AccountRevert, &ScrollPostExecutionContext)) -> Self { + Self { + account: (account.account, context).into(), + storage: account.storage, + previous_status: account.previous_status, + wipe_storage: account.wipe_storage, + } + } +} + +impl ScrollAccountRevert { + /// The approximate size of changes needed to store this account revert. + /// `1 + storage_reverts_len` + pub fn size_hint(&self) -> usize { + 1 + self.storage.len() + } + + /// Returns `true` if there is nothing to revert, + /// by checking that: + /// * both account info and storage have been left untouched + /// * we don't need to wipe storage + pub fn is_empty(&self) -> bool { + self.account == ScrollAccountInfoRevert::DoNothing && + self.storage.is_empty() && + !self.wipe_storage + } +} + +/// Code copy of a [`AccountInfoRevert`] compatible with the +/// [`ScrollAccountInfo`]. +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub enum ScrollAccountInfoRevert { + #[default] + /// Nothing changed + DoNothing, + /// Account was created and on revert we need to remove it with all storage. + DeleteIt, + /// Account was changed and on revert we need to put old state. + RevertTo(ScrollAccountInfo), +} + +impl From<(AccountInfoRevert, &ScrollPostExecutionContext)> for ScrollAccountInfoRevert { + fn from((account, context): (AccountInfoRevert, &ScrollPostExecutionContext)) -> Self { + match account { + AccountInfoRevert::DoNothing => Self::DoNothing, + AccountInfoRevert::DeleteIt => Self::DeleteIt, + AccountInfoRevert::RevertTo(account) => Self::RevertTo((account, context).into()), + } + } +} diff --git a/crates/scroll/storage/Cargo.toml b/crates/scroll/storage/Cargo.toml new file mode 100644 index 00000000000..824b6b50cfd --- /dev/null +++ b/crates/scroll/storage/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "reth-scroll-storage" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +# alloy +alloy-primitives.workspace = true + +# reth +reth-primitives-traits = { workspace = true, features = ["scroll"] } +reth-revm = { workspace = true, features = ["scroll"] } +reth-storage-errors.workspace = true + +# scroll +reth-scroll-primitives.workspace = true +reth-scroll-revm.workspace = true + +[dev-dependencies] +eyre.workspace = true +reth-primitives-traits.workspace = true +reth-revm = { workspace = true, features = ["test-utils"] } +reth-scroll-primitives.workspace = true diff --git a/crates/scroll/storage/src/lib.rs b/crates/scroll/storage/src/lib.rs new file mode 100644 index 00000000000..72372ff2469 --- /dev/null +++ b/crates/scroll/storage/src/lib.rs @@ -0,0 +1,156 @@ +//! Scroll storage implementation. + +use alloy_primitives::{Address, B256, U256}; +use reth_revm::{ + database::EvmStateProvider, + primitives::{AccountInfo, Bytecode}, + Database, +}; +use reth_scroll_primitives::ScrollPostExecutionContext; +use reth_storage_errors::provider::ProviderError; + +/// A similar construct as `StateProviderDatabase` which captures additional Scroll context for +/// touched accounts during execution. +#[derive(Clone, Debug)] +pub struct ScrollStateProviderDatabase { + /// Scroll post execution context. + post_execution_context: ScrollPostExecutionContext, + /// The database. + pub db: DB, +} + +impl ScrollStateProviderDatabase { + /// Creates a [`ScrollStateProviderDatabase`] from the provided DB. + pub fn new(db: DB) -> Self { + Self { db, post_execution_context: Default::default() } + } + + /// Consumes the provider and returns the post execution context. + pub fn post_execution_context(self) -> ScrollPostExecutionContext { + self.post_execution_context + } +} + +impl Database for ScrollStateProviderDatabase { + type Error = ProviderError; + + /// Retrieves basic account information for a given address. + /// Caches the Scroll context for the touched account if it + /// has bytecode. + /// + /// Returns `Ok` with `Some(AccountInfo)` if the account exists, + /// `None` if it doesn't, or an error if encountered. + fn basic(&mut self, address: Address) -> Result, Self::Error> { + let Some(account) = self.db.basic_account(address)? else { return Ok(None) }; + let Some(code_hash) = account.bytecode_hash else { return Ok(Some(account.into())) }; + + self.post_execution_context + .entry(code_hash) + .or_insert_with(|| (account.code_size, account.poseidon_code_hash)); + Ok(Some(account.into())) + } + + /// Retrieves the bytecode associated with a given code hash. + /// + /// Returns `Ok` with the bytecode if found, or the default bytecode otherwise. + fn code_by_hash(&mut self, code_hash: B256) -> Result { + Ok(self.db.bytecode_by_hash(code_hash)?.unwrap_or_default().0) + } + + /// Retrieves the storage value at a specific index for a given address. + /// + /// Returns `Ok` with the storage value, or the default value if not found. + fn storage(&mut self, address: Address, index: U256) -> Result { + Ok(self.db.storage(address, B256::new(index.to_be_bytes()))?.unwrap_or_default()) + } + + /// Retrieves the block hash for a given block number. + /// + /// Returns `Ok` with the block hash if found, or the default hash otherwise. + fn block_hash(&mut self, number: u64) -> Result { + Ok(self.db.block_hash(number)?.unwrap_or_default()) + } +} + +#[cfg(test)] +mod tests { + use crate::ScrollStateProviderDatabase; + use alloy_primitives::{keccak256, Address, Bytes, B256, U256}; + use reth_primitives_traits::Account; + use reth_revm::{test_utils::StateProviderTest, Database}; + use reth_scroll_primitives::{poseidon, POSEIDON_EMPTY}; + + #[test] + fn test_scroll_post_execution_context() -> eyre::Result<()> { + let mut db = StateProviderTest::default(); + + // insert an eoa in the db + let eoa_address = Address::random(); + let eoa = Account { + nonce: 0, + balance: U256::MAX, + bytecode_hash: None, + code_size: 0, + poseidon_code_hash: B256::ZERO, + }; + db.insert_account(eoa_address, eoa, None, Default::default()); + + // insert a contract account in the db + let contract_address = Address::random(); + let bytecode = Bytes::copy_from_slice(&[0x0, 0x1, 0x2, 0x3, 0x4, 0x5]); + let bytecode_hash = keccak256(&bytecode); + let poseidon_code_hash = poseidon(&bytecode); + let contract = Account { + nonce: 0, + balance: U256::MAX, + bytecode_hash: Some(bytecode_hash), + code_size: bytecode.len() as u64, + poseidon_code_hash, + }; + db.insert_account(contract_address, contract, Some(bytecode.clone()), Default::default()); + + // insert an empty contract account in the db + let empty_contract_address = Address::random(); + let empty_bytecode = Bytes::copy_from_slice(&[]); + let empty_bytecode_hash = keccak256(&empty_bytecode); + let empty_contract = Account { + nonce: 0, + balance: U256::MAX, + bytecode_hash: None, + code_size: 0, + poseidon_code_hash: POSEIDON_EMPTY, + }; + db.insert_account( + empty_contract_address, + empty_contract, + Some(empty_bytecode), + Default::default(), + ); + + let mut provider = ScrollStateProviderDatabase::new(db); + + // check eoa is in db + let _ = provider.basic(eoa_address)?.unwrap(); + // check contract is in db + let _ = provider.basic(contract_address)?.unwrap(); + // check empty contract is in db + let _ = provider.basic(empty_contract_address)?.unwrap(); + + // check provider context contains only contract and empty contract + let post_execution_context = provider.post_execution_context(); + assert_eq!(post_execution_context.len(), 2); + + // check post execution context is correct for contract + let (code_size, poseidon_code_hash) = post_execution_context.get(&bytecode_hash).unwrap(); + assert_eq!(*code_size, 6); + assert_eq!(*poseidon_code_hash, poseidon(&bytecode)); + + // check post execution context is correct for empty contract + let (code_size, poseidon_code_hash) = + post_execution_context.get(&empty_bytecode_hash).unwrap(); + assert_eq!(*code_size, 0); + assert_eq!(*poseidon_code_hash, POSEIDON_EMPTY); + + Ok(()) + } +} diff --git a/deny.toml b/deny.toml index e5823460250..6a4a98ebbbe 100644 --- a/deny.toml +++ b/deny.toml @@ -63,6 +63,11 @@ exceptions = [ { allow = ["MPL-2.0"], name = "webpki-roots" }, ] +# Skip the poseidon-bn254 and bn254 crates for license verification. We should at some point publish a license for them. +[licenses.private] +ignore = true +ignore-sources = ["https://github.com/scroll-tech/poseidon-bn254", "https://github.com/scroll-tech/bn254"] + [[licenses.clarify]] name = "ring" expression = "LicenseRef-ring" @@ -89,4 +94,7 @@ allow-git = [ "https://github.com/foundry-rs/block-explorers", "https://github.com/bluealloy/revm", "https://github.com/paradigmxyz/revm-inspectors", + "https://github.com/scroll-tech/bn254", + "https://github.com/scroll-tech/sp1-intrinsics", + "https://github.com/scroll-tech/poseidon-bn254" ]