From d4626c73626f459fb77de81a24440674e9b04898 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 18 Sep 2024 13:06:45 -0400 Subject: [PATCH 1/5] feat: serializable journal for bundlestate --- Cargo.toml | 3 +- src/journal/coder.rs | 698 +++++++++++++++++++++++++++++++++++++++++++ src/journal/index.rs | 184 ++++++++++++ src/journal/mod.rs | 32 ++ src/lib.rs | 2 + 5 files changed, 918 insertions(+), 1 deletion(-) create mode 100644 src/journal/coder.rs create mode 100644 src/journal/index.rs create mode 100644 src/journal/mod.rs diff --git a/Cargo.toml b/Cargo.toml index ebf0394..81f46a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trevm" -version = "0.10.0" +version = "0.10.1" rust-version = "1.79.0" edition = "2021" authors = ["init4"] @@ -40,6 +40,7 @@ revm = { version = "14.0.0", default-features = false, features = ["std"] } zenith-types = "0.7" thiserror = "1.0" +alloy-rlp = "0.3" [dev-dependencies] revm = { version = "14.0.0", features = ["test-utils", "serde-json", "std", "alloydb"] } diff --git a/src/journal/coder.rs b/src/journal/coder.rs new file mode 100644 index 0000000..ccf888f --- /dev/null +++ b/src/journal/coder.rs @@ -0,0 +1,698 @@ +use crate::journal::{AcctDiff, BundleStateIndex, InfoOutcome}; +use alloy_primitives::{Address, Bytes, B256, U256}; +use alloy_rlp::{Buf, BufMut}; +use revm::{ + db::{states::StorageSlot, BundleState}, + primitives::{ + eof::EofDecodeError, AccountInfo, Bytecode, Eip7702Bytecode, Eip7702DecodeError, Eof, + }, +}; +use std::{borrow::Cow, collections::BTreeMap, fmt::Debug, sync::Arc}; +use zenith_types::Zenith; + +type Result = std::result::Result; + +// Account Diff encoding +const TAG_ACCT_CREATED: u8 = 0; +const TAG_ACCT_DIFF: u8 = 1; +const TAG_ACCT_DESTROYED: u8 = 2; + +// Storage Diff encoding +const TAG_STORAGE_DELETED: u8 = 0; +const TAG_STORAGE_CREATED: u8 = 1; +const TAG_STORAGE_CHANGED: u8 = 2; +const TAG_STORAGE_UNCHANGED: u8 = 3; + +// Bytecode encoding +const TAG_BYTECODE_RAW: u8 = 0; +const TAG_BYTECODE_EOF: u8 = 1; +const TAG_BYTECODE_7702: u8 = 2; + +// Option encoding +const TAG_OPTION_NONE: u8 = 0; +const TAG_OPTION_SOME: u8 = 1; + +// Sizes +const ZENITH_HEADER_BYTES: usize = 32 + 32 + 32 + 20 + 32; +const ACCOUNT_INFO_BYTES: usize = 8 + 32 + 32; +const INFO_OUTCOME_MIN_BYTES: usize = 1 + ACCOUNT_INFO_BYTES; +const ACCT_DIFF_MIN_BYTES: usize = 4 + INFO_OUTCOME_MIN_BYTES; + +/// Error decoding journal types. +#[derive(thiserror::Error, Debug, Copy, Clone, PartialEq, Eq)] +pub enum JournalDecodeError { + /// The buffer does not contain enough data to decode the type. + #[error("Buffer overrun while decoding {ty_name}. Expected {expected} bytes, but only {remaining} bytes remain.")] + Overrun { + /// The name of the type being decoded. + ty_name: &'static str, + /// The number of bytes required to decode the type. + expected: usize, + /// The number of bytes remaining in the buffer. + remaining: usize, + }, + + /// Invalid tag while decoding a type. + #[error("Invalid tag while decoding {ty_name}. Expected a tag in range 0..={max_expected}, got {tag}.")] + InvalidTag { + /// The name of the type being decoded. + ty_name: &'static str, + /// The tag that was decoded. + tag: u8, + /// The maximum expected tag value. + max_expected: u8, + }, + + /// Storage slot is unchanged, journal should not contain unchanged slots. + #[error("Storage slot is unchanged. Unchanged items should never be in the journal.")] + UnchangedStorage, + + /// Error decoding an EOF bytecode. + #[error("Error decoding EOF bytecode: {0}")] + EofDecode(#[from] EofDecodeError), + + /// Error decoding an EIP-7702 bytecode. + #[error("Error decoding EIP-7702 bytecode: {0}")] + Eip7702Decode(#[from] Eip7702DecodeError), +} + +macro_rules! check_len { + ($buf:ident, $ty_name:literal, $len:expr) => { + let rem = $buf.remaining(); + if rem < $len { + return Err(JournalDecodeError::Overrun { + ty_name: $ty_name, + expected: $len, + remaining: rem, + }); + } + }; +} + +/// Trait for encoding journal types to a buffer. +pub trait JournalEncode: Debug { + /// Return the serialized size of the type, in bytes. + fn serialized_size(&self) -> usize; + + /// Encode the type into the buffer. + fn encode(&self, buf: &mut dyn BufMut); + + /// Shortcut to encode the type into a new vec. + fn encoded(&self) -> Vec { + let mut buf = Vec::new(); + self.encode(&mut buf); + buf + } +} + +impl JournalEncode for Cow<'_, T> +where + T: JournalEncode + ToOwned, + T::Owned: Debug, +{ + fn serialized_size(&self) -> usize { + self.as_ref().serialized_size() + } + + fn encode(&self, buf: &mut dyn BufMut) { + self.as_ref().encode(buf); + } +} + +impl JournalEncode for Option +where + T: JournalEncode, +{ + fn serialized_size(&self) -> usize { + self.as_ref().map(|v| 1 + v.serialized_size()).unwrap_or(1) + } + + fn encode(&self, buf: &mut dyn BufMut) { + match self { + Some(value) => { + buf.put_u8(TAG_OPTION_SOME); + value.encode(buf); + } + None => { + buf.put_u8(TAG_OPTION_NONE); + } + } + } +} + +impl JournalEncode for u8 { + fn serialized_size(&self) -> usize { + 1 + } + + fn encode(&self, buf: &mut dyn BufMut) { + buf.put_u8(*self); + } +} + +impl JournalEncode for u32 { + fn serialized_size(&self) -> usize { + 4 + } + + fn encode(&self, buf: &mut dyn BufMut) { + buf.put_u32(*self); + } +} + +impl JournalEncode for u64 { + fn serialized_size(&self) -> usize { + 8 + } + + fn encode(&self, buf: &mut dyn BufMut) { + buf.put_u64(*self); + } +} + +impl JournalEncode for B256 { + fn serialized_size(&self) -> usize { + 32 + } + + fn encode(&self, buf: &mut dyn BufMut) { + buf.put_slice(&self.0); + } +} + +impl JournalEncode for Address { + fn serialized_size(&self) -> usize { + 20 + } + + fn encode(&self, buf: &mut dyn BufMut) { + buf.put_slice(self.0.as_ref()); + } +} + +impl JournalEncode for U256 { + fn serialized_size(&self) -> usize { + 32 + } + + fn encode(&self, buf: &mut dyn BufMut) { + buf.put_slice(&self.to_be_bytes::<32>()); + } +} + +impl JournalEncode for AccountInfo { + fn serialized_size(&self) -> usize { + 32 + 8 + 32 + } + + fn encode(&self, buf: &mut dyn BufMut) { + self.balance.encode(buf); + self.nonce.encode(buf); + self.code_hash.encode(buf); + } +} + +impl JournalEncode for InfoOutcome<'_> { + fn serialized_size(&self) -> usize { + // tag + 32 per account + match self { + Self::Diff { .. } => 1 + (32 + 8 + 32) * 2, + _ => 1 + (32 + 8 + 32), + } + } + + fn encode(&self, buf: &mut dyn BufMut) { + match self { + Self::Created(info) => { + buf.put_u8(TAG_ACCT_CREATED); + info.as_ref().encode(buf); + } + Self::Diff { old, new } => { + buf.put_u8(TAG_ACCT_DIFF); + old.as_ref().encode(buf); + new.as_ref().encode(buf); + } + Self::Destroyed(old) => { + buf.put_u8(TAG_ACCT_DESTROYED); + old.as_ref().encode(buf); + } + } + } +} + +impl JournalEncode for StorageSlot { + fn serialized_size(&self) -> usize { + if self.original_value().is_zero() || self.present_value().is_zero() { + // tag + 32 for present value + 33 + } else { + // tag + 32 for present value + 32 for previous value + 1 + 32 + 32 + } + } + + fn encode(&self, buf: &mut dyn BufMut) { + if !self.is_changed() { + panic!("StorageSlot is unchanged. Unchanged items should never be in the journal. Enforced by filter in AcctDiff From impl, and in AcctDiff JournalEncode impl."); + } else if self.original_value().is_zero() { + buf.put_u8(TAG_STORAGE_CREATED); + self.present_value.encode(buf); + } else if self.present_value().is_zero() { + buf.put_u8(TAG_STORAGE_DELETED); + self.original_value().encode(buf); + } else { + buf.put_u8(TAG_STORAGE_CHANGED); + // DO NOT REORDER + self.present_value.encode(buf); + self.previous_or_original_value.encode(buf); + } + } +} + +impl JournalEncode for AcctDiff<'_> { + fn serialized_size(&self) -> usize { + // outcome size + u32 for storage diff len + storage diff size + self.outcome.serialized_size() + + 4 + + self + .storage_diff + .values() + .filter(|s| s.is_changed()) + .fold(0, |acc, v| acc + 32 + v.serialized_size()) + } + + fn encode(&self, buf: &mut dyn BufMut) { + self.outcome.encode(buf); + buf.put_u32(self.storage_diff.len() as u32); + for (slot, value) in &self.storage_diff { + if value.is_changed() { + slot.encode(buf); + value.encode(buf); + } + } + } +} + +impl JournalEncode for Bytecode { + fn serialized_size(&self) -> usize { + // tag + u32 for len + len of raw + 1 + 4 + self.bytes().len() + } + + fn encode(&self, buf: &mut dyn BufMut) { + match self { + Self::LegacyRaw(_) | Self::LegacyAnalyzed(_) => buf.put_u8(TAG_BYTECODE_RAW), + Self::Eof(_) => buf.put_u8(TAG_BYTECODE_EOF), + Self::Eip7702(_) => buf.put_u8(TAG_BYTECODE_7702), + } + + let raw = self.bytes(); + buf.put_u32(raw.len() as u32); + buf.put_slice(raw.as_ref()); + } +} + +impl JournalEncode for BundleStateIndex<'_> { + fn serialized_size(&self) -> usize { + // u32 for len + 4 + // 20 for key, then size of value + + self.state_index.values().fold(0, |acc, v| + acc + 20 + v.serialized_size()) + // u32 for len of contracts + + 4 + // 32 for key, then size of value + + self.new_contracts.values().fold(0, |acc, v| + acc + 32 + v.serialized_size() + ) + } + + fn encode(&self, buf: &mut dyn BufMut) { + buf.put_u32(self.state_index.len() as u32); + for (address, diff) in &self.state_index { + address.encode(buf); + diff.encode(buf); + } + buf.put_u32(self.new_contracts.len() as u32); + for (code_hash, code) in &self.new_contracts { + code_hash.encode(buf); + code.encode(buf); + } + } +} + +impl JournalEncode for BundleState { + fn serialized_size(&self) -> usize { + BundleStateIndex::from(self).serialized_size() + } + + fn encode(&self, buf: &mut dyn BufMut) { + BundleStateIndex::from(self).encode(buf); + } +} + +impl JournalEncode for Zenith::BlockHeader { + fn serialized_size(&self) -> usize { + ZENITH_HEADER_BYTES + } + + fn encode(&self, buf: &mut dyn BufMut) { + let Self { rollupChainId, hostBlockNumber, gasLimit, rewardAddress, blockDataHash } = self; + + rollupChainId.encode(buf); + hostBlockNumber.encode(buf); + gasLimit.encode(buf); + rewardAddress.encode(buf); + blockDataHash.encode(buf); + } +} + +/// Trait for decoding journal types from a buffer. +pub trait JournalDecode: JournalEncode + Sized + 'static { + /// Decode the type from the buffer. + fn decode(buf: &mut &[u8]) -> Result; +} + +impl JournalDecode for Cow<'static, T> +where + T: JournalEncode + ToOwned, + T::Owned: JournalEncode + JournalDecode, +{ + fn decode(buf: &mut &[u8]) -> Result { + JournalDecode::decode(buf).map(Cow::Owned) + } +} + +impl JournalDecode for Option +where + T: JournalDecode, +{ + fn decode(buf: &mut &[u8]) -> Result { + let tag: u8 = JournalDecode::decode(buf)?; + match tag { + TAG_OPTION_NONE => Ok(None), + TAG_OPTION_SOME => Ok(Some(JournalDecode::decode(buf)?)), + _ => Err(JournalDecodeError::InvalidTag { ty_name: "Option", tag, max_expected: 1 }), + } + } +} + +impl JournalDecode for u8 { + fn decode(buf: &mut &[u8]) -> Result { + check_len!(buf, "u8", 1); + + Ok(buf.get_u8()) + } +} + +impl JournalDecode for u32 { + fn decode(buf: &mut &[u8]) -> Result { + check_len!(buf, "u32", 4); + + Ok(buf.get_u32()) + } +} + +impl JournalDecode for u64 { + fn decode(buf: &mut &[u8]) -> Result { + check_len!(buf, "u64", 8); + + Ok(buf.get_u64()) + } +} + +impl JournalDecode for B256 { + fn decode(buf: &mut &[u8]) -> Result { + check_len!(buf, "B256", 32); + + let mut b = Self::default(); + buf.copy_to_slice(b.as_mut()); + Ok(b) + } +} + +impl JournalDecode for Address { + fn decode(buf: &mut &[u8]) -> Result { + check_len!(buf, "Address", 20); + + let mut a = Self::default(); + buf.copy_to_slice(a.as_mut()); + Ok(a) + } +} + +impl JournalDecode for U256 { + fn decode(buf: &mut &[u8]) -> Result { + check_len!(buf, "U256", 32); + + let mut bytes = [0u8; 32]; + buf.copy_to_slice(&mut bytes); + Ok(Self::from_be_bytes(bytes)) + } +} + +impl JournalDecode for AccountInfo { + fn decode(buf: &mut &[u8]) -> Result { + check_len!(buf, "AccountInfo", ACCOUNT_INFO_BYTES); + + Ok(Self { + balance: JournalDecode::decode(buf)?, + nonce: JournalDecode::decode(buf)?, + code_hash: JournalDecode::decode(buf)?, + code: None, + }) + } +} + +impl JournalDecode for InfoOutcome<'static> { + fn decode(buf: &mut &[u8]) -> Result { + let tag = JournalDecode::decode(buf)?; + + match tag { + TAG_ACCT_CREATED => { + let info = JournalDecode::decode(buf)?; + Ok(Self::Created(Cow::Owned(info))) + } + TAG_ACCT_DIFF => { + let old = JournalDecode::decode(buf)?; + let new = JournalDecode::decode(buf)?; + Ok(Self::Diff { old: Cow::Owned(old), new: Cow::Owned(new) }) + } + TAG_ACCT_DESTROYED => { + let info = JournalDecode::decode(buf)?; + Ok(Self::Destroyed(Cow::Owned(info))) + } + _ => { + Err(JournalDecodeError::InvalidTag { ty_name: "InfoOutcome", tag, max_expected: 2 }) + } + } + } +} + +impl JournalDecode for StorageSlot { + fn decode(buf: &mut &[u8]) -> Result { + let tag = JournalDecode::decode(buf)?; + + let present_value = JournalDecode::decode(buf)?; + + match tag { + TAG_STORAGE_DELETED => Ok(Self::new_changed(present_value, U256::ZERO)), + TAG_STORAGE_CREATED => Ok(Self::new_changed(U256::ZERO, present_value)), + TAG_STORAGE_CHANGED => { + let previous_or_original_value = JournalDecode::decode(buf)?; + Ok(Self::new_changed(previous_or_original_value, present_value)) + } + TAG_STORAGE_UNCHANGED => Err(JournalDecodeError::UnchangedStorage), + _ => { + Err(JournalDecodeError::InvalidTag { ty_name: "StorageSlot", tag, max_expected: 3 }) + } + } + } +} + +impl JournalDecode for AcctDiff<'static> { + fn decode(buf: &mut &[u8]) -> Result { + let outcome = JournalDecode::decode(buf)?; + + check_len!(buf, "StorageDiffLen", ACCT_DIFF_MIN_BYTES); + let storage_diff_len: u32 = JournalDecode::decode(buf)?; + + let mut storage_diff = BTreeMap::new(); + for _ in 0..storage_diff_len { + let slot = JournalDecode::decode(buf)?; + let value = JournalDecode::decode(buf)?; + storage_diff.insert(slot, Cow::Owned(value)); + } + + Ok(AcctDiff { outcome, storage_diff }) + } +} + +impl JournalDecode for Bytecode { + fn decode(buf: &mut &[u8]) -> Result { + let tag = JournalDecode::decode(buf)?; + let len: u32 = JournalDecode::decode(buf)?; + check_len!(buf, "BytecodeBody", len as usize); + + let raw: Bytes = buf.copy_to_bytes(len as usize).into(); + + match tag { + TAG_BYTECODE_RAW => Ok(Self::new_raw(raw)), + TAG_BYTECODE_EOF => Ok(Self::Eof(Arc::new(Eof::decode(raw)?))), + TAG_BYTECODE_7702 => Ok(Self::Eip7702(Eip7702Bytecode::new_raw(raw)?)), + _ => Err(JournalDecodeError::InvalidTag { ty_name: "Bytecode", tag, max_expected: 2 }), + } + } +} + +impl JournalDecode for BundleStateIndex<'static> { + fn decode(buf: &mut &[u8]) -> Result { + let state_index_len: u32 = JournalDecode::decode(buf)?; + let mut state_index = BTreeMap::new(); + for _ in 0..state_index_len { + let address = JournalDecode::decode(buf)?; + let diff = JournalDecode::decode(buf)?; + state_index.insert(address, diff); + } + + let new_contracts_len: u32 = JournalDecode::decode(buf)?; + let mut new_contracts = BTreeMap::new(); + for _ in 0..new_contracts_len { + let address = JournalDecode::decode(buf)?; + let code = JournalDecode::decode(buf)?; + new_contracts.insert(address, Cow::Owned(code)); + } + + Ok(BundleStateIndex { state_index, new_contracts }) + } +} + +impl JournalDecode for BundleState { + // TODO(perf): we can manually implemnt the decoding here in order to avoid + // allocating the btrees in the index + + fn decode(buf: &mut &[u8]) -> Result { + BundleStateIndex::decode(buf).map(Self::from) + } +} + +impl JournalDecode for Zenith::BlockHeader { + fn decode(buf: &mut &[u8]) -> Result { + Ok(Self { + rollupChainId: JournalDecode::decode(buf)?, + hostBlockNumber: JournalDecode::decode(buf)?, + gasLimit: JournalDecode::decode(buf)?, + rewardAddress: JournalDecode::decode(buf)?, + blockDataHash: JournalDecode::decode(buf)?, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn roundtrip(expected: &T) { + let enc = JournalEncode::encoded(expected); + assert_eq!(enc.len(), expected.serialized_size(), "{}", std::any::type_name::()); + let dec = T::decode(&mut enc.as_slice()).expect("decoding failed"); + assert_eq!(&dec, expected); + } + + #[test] + fn roundtrips() { + roundtrip(&Cow::<'static, u8>::Owned(1u8)); + roundtrip(&Cow::<'static, u32>::Owned(1u32)); + roundtrip(&Cow::<'static, u64>::Owned(1u64)); + roundtrip(&B256::repeat_byte(0xa)); + roundtrip(&Address::repeat_byte(0xa)); + roundtrip(&U256::from(38238923)); + + let acc_info = AccountInfo { + balance: U256::from(38238923), + nonce: 38238923, + code_hash: B256::repeat_byte(0xa), + code: None, + }; + roundtrip(&acc_info); + let created_outcome = InfoOutcome::Created(Cow::Owned(acc_info)); + roundtrip(&created_outcome); + + let diff_outcome = InfoOutcome::Diff { + old: Cow::Owned(AccountInfo { + balance: U256::from(38), + nonce: 38, + code_hash: B256::repeat_byte(0xab), + code: None, + }), + new: Cow::Owned(AccountInfo { + balance: U256::from(38238923), + nonce: 38238923, + code_hash: B256::repeat_byte(0xa), + code: None, + }), + }; + roundtrip(&diff_outcome); + + let new_slot = StorageSlot::new_changed(U256::ZERO, U256::from(38238923)); + let changed_slot = StorageSlot::new_changed(U256::from(38238923), U256::from(3)); + let deleted_slot = StorageSlot::new_changed(U256::from(17), U256::ZERO); + + roundtrip(&new_slot); + roundtrip(&changed_slot); + roundtrip(&deleted_slot); + + let created_acc = AcctDiff { + outcome: created_outcome, + storage_diff: vec![ + (U256::from(3), Cow::Owned(new_slot.clone())), + (U256::from(4), Cow::Owned(changed_slot.clone())), + (U256::from(5), Cow::Owned(deleted_slot.clone())), + ] + .into_iter() + .collect(), + }; + roundtrip(&created_acc); + + let changed_acc = AcctDiff { + outcome: diff_outcome, + storage_diff: vec![ + (U256::from(3), Cow::Owned(new_slot)), + (U256::from(4), Cow::Owned(changed_slot)), + (U256::from(5), Cow::Owned(deleted_slot)), + ] + .into_iter() + .collect(), + }; + roundtrip(&changed_acc); + + let bytecode = Bytecode::new_raw(Bytes::from(vec![1, 2, 3])); + let eof_bytes = Bytecode::Eof(Arc::new(Eof::default())); + roundtrip(&bytecode); + roundtrip(&eof_bytes); + + let bsi = BundleStateIndex { + state_index: vec![ + (Address::repeat_byte(0xa), created_acc), + (Address::repeat_byte(0xb), changed_acc), + ] + .into_iter() + .collect(), + new_contracts: vec![ + (B256::repeat_byte(0xa), Cow::Owned(bytecode)), + (B256::repeat_byte(0xb), Cow::Owned(eof_bytes)), + ] + .into_iter() + .collect(), + }; + roundtrip(&bsi); + + roundtrip(&Zenith::BlockHeader { + rollupChainId: U256::from(1), + hostBlockNumber: U256::from(1), + gasLimit: U256::from(1), + rewardAddress: Address::repeat_byte(0xa), + blockDataHash: B256::repeat_byte(0xa), + }); + } +} diff --git a/src/journal/index.rs b/src/journal/index.rs new file mode 100644 index 0000000..f5c07a8 --- /dev/null +++ b/src/journal/index.rs @@ -0,0 +1,184 @@ +use alloy_primitives::{Address, B256, U256}; +use revm::{ + db::{states::StorageSlot, AccountStatus, BundleAccount, BundleState}, + primitives::{AccountInfo, Bytecode}, +}; +use std::{ + borrow::Cow, + collections::{BTreeMap, HashMap}, +}; + +/// Outcome of an account info after block execution. Post-6780, accounts +/// cannot be destroyed, only created or modified. In either case, the new and +/// old states are contained in this object. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InfoOutcome<'a> { + /// Account was created after block execution. + /// + /// Reverting this means deleting the account. + Created(Cow<'a, AccountInfo>), + /// Account was modified after block execution. This object contains the + /// new and previous states. + /// + /// Reverting this means restoring the previous state. + Diff { + /// The original account info before block execution. + old: Cow<'a, AccountInfo>, + /// The updated account info after block execution. + new: Cow<'a, AccountInfo>, + }, + /// Account was destroyed after block execution. Restoring this state means + /// restoring the account. + Destroyed(Cow<'a, AccountInfo>), +} + +impl InfoOutcome<'_> { + /// Get the original account info. This is `None` if the account was + /// created. + pub fn original(&self) -> Option> { + match self { + Self::Created(_) => None, + Self::Diff { old, .. } => Some(Cow::Borrowed(old)), + Self::Destroyed(info) => Some(Cow::Borrowed(info)), + } + } + + /// Get the updated account info. This is the account info at the end of + /// block execution. + pub fn updated(&self) -> Cow<'_, AccountInfo> { + match self { + Self::Created(info) => Cow::Borrowed(info), + Self::Diff { new, .. } => Cow::Borrowed(new), + Self::Destroyed(_) => Cow::Owned(Default::default()), + } + } +} + +impl<'a> From<&'a BundleAccount> for InfoOutcome<'a> { + fn from(value: &'a BundleAccount) -> Self { + match (&value.original_info, &value.info) { + (None, Some(new)) => Self::Created(Cow::Borrowed(new)), + (Some(old), Some(new)) => { + Self::Diff { old: Cow::Borrowed(old), new: Cow::Borrowed(new) } + } + (Some(old), None) => { + Self::Destroyed(Cow::Borrowed(old)) + } + _ => unreachable!("revm will never output a bundle account that went from not-existing to not-existing"), + } + } +} + +/// Contains the diff of an account after block execution. This includes the +/// account info and the storage diff. This type ensures that the storage +/// updates are sorted by slot. +/// +/// Reverting this means: +/// - Write the original value for the account info (deleting the account if it +/// was created) +/// - Write the original value for each storage slot +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AcctDiff<'a> { + /// Outcome of an account info after block execution. + pub outcome: InfoOutcome<'a>, + /// The storage diff for the account. This is a map of storage slot to the + /// old and new values + pub storage_diff: BTreeMap>, +} + +impl AcctDiff<'_> { + /// Get the original account info. This is `None` if the account was + /// created. + pub fn original(&self) -> Option> { + self.outcome.original() + } + + /// Get the updated account info. This is the account info at the end of + /// block execution. + pub fn updated(&self) -> Cow<'_, AccountInfo> { + self.outcome.updated() + } +} + +impl<'a> From<&'a BundleAccount> for AcctDiff<'a> { + fn from(value: &'a BundleAccount) -> Self { + let outcome = InfoOutcome::from(value); + let storage_diff = value + .storage + .iter() + .filter(|(_, v)| v.is_changed()) + .map(|(k, v)| (*k, Cow::Borrowed(v))) + .collect(); + AcctDiff { outcome, storage_diff } + } +} + +impl From> for BundleAccount { + fn from(value: AcctDiff<'_>) -> Self { + let original_info = value.outcome.original().map(|info| info.into_owned()); + let info = Some(value.outcome.updated().into_owned()); + let storage = value.storage_diff.into_iter().map(|(k, v)| (k, v.into_owned())).collect(); + + Self { original_info, info, storage, status: AccountStatus::Changed } + } +} + +/// A state index contains the diffs for a single block. The primary purpose of +/// this type is to iterate over the information in a [`BundleState`], making a +/// [`BTreeMap`] containing the changed addresses. This ensures that the +/// state updates are sorted by address, and the bytecodes are sorted by +/// contract address. +/// +/// Reverting this type means reverting +/// - Reverting each account state +/// - Deleting each new contract +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct BundleStateIndex<'a> { + /// The state index contains the account and storage diffs for a single + /// block. + pub state_index: BTreeMap>, + /// The new contracts created in this block. + pub new_contracts: BTreeMap>, +} + +impl<'a> From<&'a BundleState> for BundleStateIndex<'a> { + fn from(value: &'a BundleState) -> Self { + let state_index = value + .state + .iter() + .map(|(address, account)| (*address, AcctDiff::from(account))) + .collect(); + + let new_contracts = value.contracts.iter().map(|(k, v)| (*k, Cow::Borrowed(v))).collect(); + BundleStateIndex { state_index, new_contracts } + } +} + +impl From> for BundleState { + // much of this implementation adapted from revm: + // revm/src/db/states/bundle_state.rs + fn from(value: BundleStateIndex<'_>) -> Self { + let mut state_size = 0; + let state: HashMap<_, _> = value + .state_index + .into_iter() + .map(|(address, info)| { + let original = info.original().map(Cow::into_owned); + let present = Some(info.updated().into_owned()); + + let storage = + info.storage_diff.into_iter().map(|(k, v)| (k, v.into_owned())).collect(); + + let account: BundleAccount = + BundleAccount::new(original, present, storage, AccountStatus::Changed); + + state_size += account.size_hint(); + (address, account) + }) + .collect(); + + let contracts = value.new_contracts.into_iter().map(|(a, c)| (a, c.into_owned())).collect(); + + Self { state, reverts: Default::default(), contracts, state_size, reverts_size: 0 } + } +} diff --git a/src/journal/mod.rs b/src/journal/mod.rs new file mode 100644 index 0000000..8a2d83f --- /dev/null +++ b/src/journal/mod.rs @@ -0,0 +1,32 @@ +//! Utilities for serializing revm's [`BundleState`] into a canonical format. +//! +//! The [`BundleState`] represents the accumulated state changes of one or more +//! transactions. It is produced when revm is run with a [`State`] and the +//! [`StateBuilder::with_bundle_update`] option. It is useful for aggregating +//! state changes across multiple transactions, so that those changes may be +//! stored in a DB, or otherwise processed in a batch. +//! +//! This module contains utilities for serializing the [`BundleState`] in a +//! canonical format that can be stored to disk or sent over the wire. The core +//! type is the [`BundleStateIndex`], which is a sorted list of [`AcctDiff`]s +//! along with new contract bytecode. +//! +//! Each [`AcctDiff`] represents the state changes for a single account, and +//! contains the pre- and post-state of the account, along with sorted changes +//! to the account's storage. +//! +//! The coding scheme is captured by the [`JournalEncode`] and [`JournalDecode`] +//! traits. These traits provide a very simple binary encoding. The encoding +//! prioritizes legibility and simplicity over compactness. We assume that it +//! will be compressed before being stored or sent. +//! +//! [`StateBuilder::with_bundle_update`]: revm::db::StateBuilder::with_bundle_update +//! [`State`]: revm::db::State +//! [`BundleState`]: revm::db::BundleState +//! [reth]: https://github.com/paradigmxyz/reth + +mod coder; +pub use coder::{JournalDecode, JournalDecodeError, JournalEncode}; + +mod index; +pub use index::{AcctDiff, BundleStateIndex, InfoOutcome}; diff --git a/src/lib.rs b/src/lib.rs index cffc44b..8f81739 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -377,6 +377,8 @@ pub use ext::EvmExtUnchecked; mod fill; pub use fill::{Block, Cfg, NoopBlock, NoopCfg, Tx}; +pub mod journal; + mod lifecycle; pub use lifecycle::{ethereum_receipt, BlockOutput, PostTx, PostflightResult}; From ecf010b2bc91b542c273d2bde3974ae436827fb4 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 18 Sep 2024 13:08:28 -0400 Subject: [PATCH 2/5] fix: add license to index --- src/journal/index.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/journal/index.rs b/src/journal/index.rs index f5c07a8..b077cef 100644 --- a/src/journal/index.rs +++ b/src/journal/index.rs @@ -182,3 +182,28 @@ impl From> for BundleState { Self { state, reverts: Default::default(), contracts, state_size, reverts_size: 0 } } } + +// Some code above and documentation is adapted from the revm crate, and is +// reproduced here under the terms of the MIT license. +// +// MIT License +// +// Copyright (c) 2021-2024 draganrakita +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. From 47ece8446b68fa3507fed1b73a162b78d17a1050 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 18 Sep 2024 13:34:04 -0400 Subject: [PATCH 3/5] doc: usage example #1 --- src/journal/index.rs | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/journal/index.rs b/src/journal/index.rs index b077cef..5d55939 100644 --- a/src/journal/index.rs +++ b/src/journal/index.rs @@ -8,9 +8,13 @@ use std::{ collections::{BTreeMap, HashMap}, }; -/// Outcome of an account info after block execution. Post-6780, accounts -/// cannot be destroyed, only created or modified. In either case, the new and -/// old states are contained in this object. +/// Outcome of an account info after block execution. +/// +/// Post-6780, accounts cannot be destroyed, only created or modified. In +/// either case, the new and old states are contained in this object. +/// +/// In general, this should not be instantiated directly. Instead, use the +/// [`BundleStateIndex`] to index a [`BundleState`]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum InfoOutcome<'a> { /// Account was created after block execution. @@ -69,14 +73,18 @@ impl<'a> From<&'a BundleAccount> for InfoOutcome<'a> { } } -/// Contains the diff of an account after block execution. This includes the -/// account info and the storage diff. This type ensures that the storage -/// updates are sorted by slot. +/// Contains the diff of an account after block execution. +/// +/// This includes the account info and the storage diff. This type ensures that +/// the storage updates are sorted by slot. /// /// Reverting this means: /// - Write the original value for the account info (deleting the account if it /// was created) /// - Write the original value for each storage slot +/// +/// In general, this should not be instantiated directly. Instead, use the +/// [`BundleStateIndex`] to index a [`BundleState`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AcctDiff<'a> { /// Outcome of an account info after block execution. @@ -132,6 +140,19 @@ impl From> for BundleAccount { /// Reverting this type means reverting /// - Reverting each account state /// - Deleting each new contract +/// +/// ``` +/// # use revm::db::BundleState; +/// # use trevm::journal::{BundleStateIndex, JournalEncode, JournalDecode, JournalDecodeError}; +/// # fn make_index(bundle_state: &BundleState) -> Result<(), JournalDecodeError> { +/// let index = BundleStateIndex::from(bundle_state); +/// let serialized_index = index.encoded(); +/// let decoded = BundleStateIndex::decode(&mut serialized_index.as_slice())?; +/// assert_eq!(index, decoded); +/// # Ok(()) +/// # } +/// ``` +/// #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct BundleStateIndex<'a> { /// The state index contains the account and storage diffs for a single From 8565475b07637e6b3ce53585b3a8d0489356f7cc Mon Sep 17 00:00:00 2001 From: James Date: Wed, 18 Sep 2024 13:44:50 -0400 Subject: [PATCH 4/5] doc: more usage examples --- src/journal/coder.rs | 18 +++++++++--------- src/journal/index.rs | 31 +++++++++++++++++++++---------- src/journal/mod.rs | 26 ++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/journal/coder.rs b/src/journal/coder.rs index ccf888f..ec3d728 100644 --- a/src/journal/coder.rs +++ b/src/journal/coder.rs @@ -317,7 +317,7 @@ impl JournalEncode for BundleStateIndex<'_> { // u32 for len 4 // 20 for key, then size of value - + self.state_index.values().fold(0, |acc, v| + + self.state.values().fold(0, |acc, v| acc + 20 + v.serialized_size()) // u32 for len of contracts + 4 @@ -328,8 +328,8 @@ impl JournalEncode for BundleStateIndex<'_> { } fn encode(&self, buf: &mut dyn BufMut) { - buf.put_u32(self.state_index.len() as u32); - for (address, diff) in &self.state_index { + buf.put_u32(self.state.len() as u32); + for (address, diff) in &self.state { address.encode(buf); diff.encode(buf); } @@ -547,12 +547,12 @@ impl JournalDecode for Bytecode { impl JournalDecode for BundleStateIndex<'static> { fn decode(buf: &mut &[u8]) -> Result { - let state_index_len: u32 = JournalDecode::decode(buf)?; - let mut state_index = BTreeMap::new(); - for _ in 0..state_index_len { + let state_len: u32 = JournalDecode::decode(buf)?; + let mut state = BTreeMap::new(); + for _ in 0..state_len { let address = JournalDecode::decode(buf)?; let diff = JournalDecode::decode(buf)?; - state_index.insert(address, diff); + state.insert(address, diff); } let new_contracts_len: u32 = JournalDecode::decode(buf)?; @@ -563,7 +563,7 @@ impl JournalDecode for BundleStateIndex<'static> { new_contracts.insert(address, Cow::Owned(code)); } - Ok(BundleStateIndex { state_index, new_contracts }) + Ok(BundleStateIndex { state, new_contracts }) } } @@ -672,7 +672,7 @@ mod test { roundtrip(&eof_bytes); let bsi = BundleStateIndex { - state_index: vec![ + state: vec![ (Address::repeat_byte(0xa), created_acc), (Address::repeat_byte(0xb), changed_acc), ] diff --git a/src/journal/index.rs b/src/journal/index.rs index 5d55939..6fd6b45 100644 --- a/src/journal/index.rs +++ b/src/journal/index.rs @@ -1,4 +1,4 @@ -use alloy_primitives::{Address, B256, U256}; +use alloy_primitives::{Address, Sign, B256, I256, U256}; use revm::{ db::{states::StorageSlot, AccountStatus, BundleAccount, BundleState}, primitives::{AccountInfo, Bytecode}, @@ -106,6 +106,16 @@ impl AcctDiff<'_> { pub fn updated(&self) -> Cow<'_, AccountInfo> { self.outcome.updated() } + + /// Get the change in balance for the account. + pub fn balance_change(&self) -> I256 { + let old = self.original().map(|info| info.balance).unwrap_or_default(); + let new = self.updated().balance; + + let abs = std::cmp::max(new, old) - std::cmp::min(new, old); + let sign = if new > old { Sign::Positive } else { Sign::Negative }; + I256::checked_from_sign_and_abs(sign, abs).expect("balance diff overflow") + } } impl<'a> From<&'a BundleAccount> for AcctDiff<'a> { @@ -141,37 +151,38 @@ impl From> for BundleAccount { /// - Reverting each account state /// - Deleting each new contract /// +/// # Example +/// /// ``` /// # use revm::db::BundleState; /// # use trevm::journal::{BundleStateIndex, JournalEncode, JournalDecode, JournalDecodeError}; /// # fn make_index(bundle_state: &BundleState) -> Result<(), JournalDecodeError> { -/// let index = BundleStateIndex::from(bundle_state); -/// let serialized_index = index.encoded(); -/// let decoded = BundleStateIndex::decode(&mut serialized_index.as_slice())?; -/// assert_eq!(index, decoded); +/// let index = BundleStateIndex::from(bundle_state); +/// let serialized_index = index.encoded(); +/// let decoded = BundleStateIndex::decode(&mut serialized_index.as_slice())?; +/// assert_eq!(index, decoded); /// # Ok(()) /// # } /// ``` -/// #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct BundleStateIndex<'a> { /// The state index contains the account and storage diffs for a single /// block. - pub state_index: BTreeMap>, + pub state: BTreeMap>, /// The new contracts created in this block. pub new_contracts: BTreeMap>, } impl<'a> From<&'a BundleState> for BundleStateIndex<'a> { fn from(value: &'a BundleState) -> Self { - let state_index = value + let state = value .state .iter() .map(|(address, account)| (*address, AcctDiff::from(account))) .collect(); let new_contracts = value.contracts.iter().map(|(k, v)| (*k, Cow::Borrowed(v))).collect(); - BundleStateIndex { state_index, new_contracts } + BundleStateIndex { state, new_contracts } } } @@ -181,7 +192,7 @@ impl From> for BundleState { fn from(value: BundleStateIndex<'_>) -> Self { let mut state_size = 0; let state: HashMap<_, _> = value - .state_index + .state .into_iter() .map(|(address, info)| { let original = info.original().map(Cow::into_owned); diff --git a/src/journal/mod.rs b/src/journal/mod.rs index 8a2d83f..37c9d0b 100644 --- a/src/journal/mod.rs +++ b/src/journal/mod.rs @@ -20,6 +20,32 @@ //! prioritizes legibility and simplicity over compactness. We assume that it //! will be compressed before being stored or sent. //! +//! # Usage Example +//! +//! ``` +//! # use revm::db::BundleState; +//! # use trevm::journal::{BundleStateIndex, JournalEncode, JournalDecode, JournalDecodeError}; +//! # fn make_index(bundle_state: &BundleState) -> Result<(), JournalDecodeError> { +//! // Make an index over a bundle state. +//! let index = BundleStateIndex::from(bundle_state); +//! +//! // We can serialize it and deserialize it :) +//! let serialized_index = index.encoded(); +//! let decoded = BundleStateIndex::decode(&mut serialized_index.as_slice())?; +//! assert_eq!(index, decoded); +//! +//! // It contains information about accounts +//! for (addr, diff) in index.state { +//! println!("Balance of {addr} changed by {}", diff.balance_change()); +//! } +//! +//! // And about bytecode +//! let contract_count = index.new_contracts.len(); +//! println!("{contract_count} new contracts deployed!"); +//! # Ok(()) +//! # } +//! ``` +//! //! [`StateBuilder::with_bundle_update`]: revm::db::StateBuilder::with_bundle_update //! [`State`]: revm::db::State //! [`BundleState`]: revm::db::BundleState From 629ae8d485490eb991d7a8ea9535015111fadb9c Mon Sep 17 00:00:00 2001 From: James Date: Wed, 18 Sep 2024 15:52:15 -0400 Subject: [PATCH 5/5] nit: error fmt --- src/journal/coder.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/journal/coder.rs b/src/journal/coder.rs index ec3d728..d73baa3 100644 --- a/src/journal/coder.rs +++ b/src/journal/coder.rs @@ -42,7 +42,7 @@ const ACCT_DIFF_MIN_BYTES: usize = 4 + INFO_OUTCOME_MIN_BYTES; #[derive(thiserror::Error, Debug, Copy, Clone, PartialEq, Eq)] pub enum JournalDecodeError { /// The buffer does not contain enough data to decode the type. - #[error("Buffer overrun while decoding {ty_name}. Expected {expected} bytes, but only {remaining} bytes remain.")] + #[error("buffer overrun while decoding {ty_name}. Expected {expected} bytes, but only {remaining} bytes remain")] Overrun { /// The name of the type being decoded. ty_name: &'static str, @@ -53,7 +53,7 @@ pub enum JournalDecodeError { }, /// Invalid tag while decoding a type. - #[error("Invalid tag while decoding {ty_name}. Expected a tag in range 0..={max_expected}, got {tag}.")] + #[error("invalid tag while decoding {ty_name}. Expected a tag in range 0..={max_expected}, got {tag}")] InvalidTag { /// The name of the type being decoded. ty_name: &'static str, @@ -64,15 +64,15 @@ pub enum JournalDecodeError { }, /// Storage slot is unchanged, journal should not contain unchanged slots. - #[error("Storage slot is unchanged. Unchanged items should never be in the journal.")] + #[error("storage slot is unchanged. Unchanged items should never be in the journal")] UnchangedStorage, /// Error decoding an EOF bytecode. - #[error("Error decoding EOF bytecode: {0}")] + #[error("error decoding EOF bytecode: {0}")] EofDecode(#[from] EofDecodeError), /// Error decoding an EIP-7702 bytecode. - #[error("Error decoding EIP-7702 bytecode: {0}")] + #[error("error decoding EIP-7702 bytecode: {0}")] Eip7702Decode(#[from] Eip7702DecodeError), }