diff --git a/Cargo.toml b/Cargo.toml index 78b3c43..25124d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,11 +38,13 @@ signet-bundle = { version = "0.10", path = "crates/bundle" } signet-constants = { version = "0.10", path = "crates/constants" } signet-evm = { version = "0.10", path = "crates/evm" } signet-extract = { version = "0.10", path = "crates/extract" } +signet-journal = { version = "0.10", path = "crates/journal" } signet-node = { version = "0.10", path = "crates/node" } signet-sim = { version = "0.10", path = "crates/sim" } signet-types = { version = "0.10", path = "crates/types" } signet-tx-cache = { version = "0.10", path = "crates/tx-cache" } signet-zenith = { version = "0.10", path = "crates/zenith" } + signet-test-utils = { version = "0.10", path = "crates/test-utils" } # ajj diff --git a/crates/journal/Cargo.toml b/crates/journal/Cargo.toml new file mode 100644 index 0000000..3415052 --- /dev/null +++ b/crates/journal/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "signet-journal" +description = "Utilities for working with trevm journals in the Signet chain." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +alloy.workspace = true +futures-util = "0.3.31" +thiserror.workspace = true +trevm.workspace = true diff --git a/crates/journal/src/host.rs b/crates/journal/src/host.rs new file mode 100644 index 0000000..49661db --- /dev/null +++ b/crates/journal/src/host.rs @@ -0,0 +1,178 @@ +use crate::JournalMeta; +use alloy::{ + consensus::Header, + primitives::{keccak256, Bytes, B256}, +}; +use std::sync::OnceLock; +use trevm::journal::{BundleStateIndex, JournalDecode, JournalDecodeError, JournalEncode}; + +/// Journal associated with a host block. The journal is an index over the EVM +/// state changes. It can be used to repopulate +#[derive(Debug, Clone)] +pub struct HostJournal<'a> { + /// The metadata + meta: JournalMeta, + + /// The changes. + journal: BundleStateIndex<'a>, + + /// The serialized journal + serialized: OnceLock, + + /// The hash of the serialized journal + hash: OnceLock, +} + +impl PartialEq for HostJournal<'_> { + fn eq(&self, other: &Self) -> bool { + self.meta == other.meta && self.journal == other.journal + } +} + +impl Eq for HostJournal<'_> {} + +impl<'a> HostJournal<'a> { + /// Create a new journal. + pub const fn new(meta: JournalMeta, journal: BundleStateIndex<'a>) -> Self { + Self { meta, journal, serialized: OnceLock::new(), hash: OnceLock::new() } + } + + /// Deconstruct the `HostJournal` into its parts. + pub fn into_parts(self) -> (JournalMeta, BundleStateIndex<'a>) { + (self.meta, self.journal) + } + + /// Get the journal meta. + pub const fn meta(&self) -> &JournalMeta { + &self.meta + } + + /// Get the journal. + pub const fn journal(&self) -> &BundleStateIndex<'a> { + &self.journal + } + + /// Get the host height. + pub const fn host_height(&self) -> u64 { + self.meta.host_height() + } + + /// Get the previous journal hash. + pub const fn prev_journal_hash(&self) -> B256 { + self.meta.prev_journal_hash() + } + + /// Get the rollup block header. + pub const fn header(&self) -> &Header { + self.meta.header() + } + + /// Get the rollup height. + pub const fn rollup_height(&self) -> u64 { + self.meta.rollup_height() + } + + /// Serialize the journal. + pub fn serialized(&self) -> &Bytes { + self.serialized.get_or_init(|| JournalEncode::encoded(self)) + } + + /// Serialize and hash the journal. + pub fn journal_hash(&self) -> B256 { + *self.hash.get_or_init(|| keccak256(self.serialized())) + } +} + +impl JournalEncode for HostJournal<'_> { + fn serialized_size(&self) -> usize { + 8 + 32 + self.journal.serialized_size() + } + + fn encode(&self, buf: &mut dyn alloy::rlp::BufMut) { + self.meta.encode(buf); + self.journal.encode(buf); + } +} + +impl JournalDecode for HostJournal<'static> { + fn decode(buf: &mut &[u8]) -> Result { + let original = *buf; + + let meta = JournalMeta::decode(buf)?; + let journal = JournalDecode::decode(buf)?; + + let bytes_read = original.len() - buf.len(); + let original = &original[..bytes_read]; + + Ok(Self { + meta, + journal, + serialized: OnceLock::from(Bytes::copy_from_slice(original)), + hash: OnceLock::from(keccak256(original)), + }) + } +} + +#[cfg(test)] +pub(crate) mod test { + use super::*; + use alloy::primitives::{Address, KECCAK256_EMPTY, U256}; + use std::{borrow::Cow, collections::BTreeMap}; + use trevm::{ + journal::{AcctDiff, InfoOutcome}, + revm::{ + database::states::StorageSlot, + state::{AccountInfo, Bytecode}, + }, + }; + + pub(crate) fn make_state_diff() -> BundleStateIndex<'static> { + let mut bsi = BundleStateIndex::default(); + + let bytecode = Bytecode::new_legacy(Bytes::from_static(b"world")); + let code_hash = bytecode.hash_slow(); + + bsi.new_contracts.insert(code_hash, Cow::Owned(bytecode)); + + bsi.state.insert( + Address::repeat_byte(0x99), + AcctDiff { + outcome: InfoOutcome::Diff { + old: Cow::Owned(AccountInfo { + balance: U256::from(38), + nonce: 7, + code_hash: KECCAK256_EMPTY, + code: None, + }), + new: Cow::Owned(AccountInfo { + balance: U256::from(23828839), + nonce: 83, + code_hash, + code: None, + }), + }, + storage_diff: BTreeMap::from_iter([( + U256::MAX, + Cow::Owned(StorageSlot { + previous_or_original_value: U256::from(123456), + present_value: U256::from(654321), + }), + )]), + }, + ); + bsi + } + + #[test] + fn roundtrip() { + let original = HostJournal::new( + JournalMeta::new(u64::MAX, B256::repeat_byte(0xff), Header::default()), + make_state_diff(), + ); + + let buf = original.encoded(); + + let decoded = HostJournal::decode(&mut &buf[..]).unwrap(); + assert_eq!(original, decoded); + } +} diff --git a/crates/journal/src/lib.rs b/crates/journal/src/lib.rs new file mode 100644 index 0000000..17fa2e2 --- /dev/null +++ b/crates/journal/src/lib.rs @@ -0,0 +1,35 @@ +//! Signet journal utilities. +//! +//! In general, it is recommended to use the [`Journal`] enum, for forwards +//! compatibility. + +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +mod host; +pub use host::HostJournal; + +mod meta; +pub use meta::JournalMeta; + +mod set; +pub use set::JournalSet; + +mod versions; +pub use versions::Journal; + +use futures_util::Stream; + +/// Any [`Stream`] that produces [`Journal`]s. +pub trait JournalStream<'a>: Stream> {} + +impl<'a, S> JournalStream<'a> for S where S: Stream> {} diff --git a/crates/journal/src/meta.rs b/crates/journal/src/meta.rs new file mode 100644 index 0000000..80d549d --- /dev/null +++ b/crates/journal/src/meta.rs @@ -0,0 +1,89 @@ +use alloy::{consensus::Header, primitives::B256}; +use trevm::journal::{JournalDecode, JournalDecodeError, JournalEncode}; + +/// Metadata for a block journal. This includes the block header, the host +/// height, and the hash of the previous journal. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JournalMeta { + /// The host height. + host_height: u64, + + /// The hash of the previous journal. + prev_journal_hash: B256, + + /// The rollup block header. + header: Header, +} + +impl JournalMeta { + /// Create a new `JournalMeta`. + pub const fn new(host_height: u64, prev_journal_hash: B256, header: Header) -> Self { + Self { host_height, prev_journal_hash, header } + } + + /// Deconstruct the `JournalMeta` into its parts. + pub fn into_parts(self) -> (u64, B256, Header) { + (self.host_height, self.prev_journal_hash, self.header) + } + + /// Get the host height. + pub const fn host_height(&self) -> u64 { + self.host_height + } + + /// Get the previous journal hash. + pub const fn prev_journal_hash(&self) -> B256 { + self.prev_journal_hash + } + + /// Get the rollup block header. + pub const fn header(&self) -> &Header { + &self.header + } + + /// Get the rollup height. + pub const fn rollup_height(&self) -> u64 { + self.header.number + } +} + +impl JournalEncode for JournalMeta { + fn serialized_size(&self) -> usize { + 8 + 32 + self.header.serialized_size() + } + + fn encode(&self, buf: &mut dyn alloy::rlp::BufMut) { + self.host_height.encode(buf); + self.prev_journal_hash.encode(buf); + self.header.encode(buf); + } +} + +impl JournalDecode for JournalMeta { + fn decode(buf: &mut &[u8]) -> Result { + let host_height = JournalDecode::decode(buf)?; + let prev_journal_hash = JournalDecode::decode(buf)?; + let header = JournalDecode::decode(buf)?; + + Ok(Self { host_height, prev_journal_hash, header }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn roundtrip() { + let original = JournalMeta { + host_height: 13871, + prev_journal_hash: B256::repeat_byte(0x7), + header: Header::default(), + }; + + let buf = original.encoded(); + + let decoded = JournalMeta::decode(&mut &buf[..]).unwrap(); + assert_eq!(original, decoded); + } +} diff --git a/crates/journal/src/set.rs b/crates/journal/src/set.rs new file mode 100644 index 0000000..a91d20f --- /dev/null +++ b/crates/journal/src/set.rs @@ -0,0 +1,410 @@ +use crate::Journal; +use alloy::primitives::B256; +use std::{collections::VecDeque, ops::RangeInclusive}; + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum JournalSetError<'a> { + /// Cannot ingest the journal because it is at the wrong height. + #[error("wrong height: actual {actual}, expected {expected}")] + WrongHeight { + /// The actual height of the journal. + actual: u64, + + /// The expected height of the journal. + expected: u64, + + /// The journal. + journal: Box>, + }, + + /// Cannot ingest the journal because it has the wrong previous hash. + #[error("wrong prev_hash: current {latest_hash}, new journal expected {in_journal}")] + WrongPrevHash { + /// The latest hash of the journal. + latest_hash: B256, + + /// The hash expected during ingestion. + in_journal: B256, + + /// The journal. + journal: Box>, + }, + + /// Attempted to append_overwrite a journal that is not in the set's range. + #[error("not in range: start {start:?}, end {end:?}, height {height}")] + NotInRange { + /// The start of the expected range. + start: Option, + + /// The end of the expected range. + end: Option, + + /// The height of the journal. + height: u64, + + /// The journal + journal: Box>, + }, +} + +impl<'a> JournalSetError<'a> { + /// Converts the error into a journal, discarding error info. + pub fn into_journal(self) -> Journal<'a> { + match self { + Self::WrongHeight { journal, .. } => *journal, + Self::WrongPrevHash { journal, .. } => *journal, + Self::NotInRange { journal, .. } => *journal, + } + } +} + +/// A set of journals, ordered by height and hash. +#[derive(Debug, Clone, Default)] +pub struct JournalSet<'a> { + /// The set of journals. + journals: VecDeque>, + + /// The latest height, recorded separately so that if the set is drained, + /// pushing more journals is still checked for consistency. + latest_height: Option, + + /// The latest journal hash. + latest_hash: Option, +} + +impl<'a> JournalSet<'a> { + /// Creates a new empty `JournalSet`. + pub const fn new() -> Self { + Self { journals: VecDeque::new(), latest_height: None, latest_hash: None } + } + + /// Creates a new `JournalSet` with the specified capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self { journals: VecDeque::with_capacity(capacity), latest_height: None, latest_hash: None } + } + + /// Creates a new `JournalSet` from a single [`Journal`]. + pub fn from_journal(journal: Journal<'a>) -> Self { + let latest_height = Some(journal.rollup_height()); + let latest_hash = Some(journal.journal_hash()); + let mut journals = VecDeque::new(); + journals.push_back(journal); + Self { journals, latest_height, latest_hash } + } + + /// Returns the number of journals in the set. + pub fn len(&self) -> usize { + self.journals.len() + } + + /// True if the set is empty. + pub fn is_empty(&self) -> bool { + self.journals.is_empty() + } + + /// Make a [`JournalSetError::NotInRange`]. + fn not_in_range(&self, journal: Journal<'a>) -> JournalSetError<'a> { + JournalSetError::NotInRange { + start: self.earliest_height(), + end: self.latest_height(), + height: journal.rollup_height(), + journal: Box::new(journal), + } + } + + /// Make a [`JournalSetError::WrongPrevHash`]. + fn wrong_prev_hash(&self, journal: Journal<'a>) -> JournalSetError<'a> { + JournalSetError::WrongPrevHash { + latest_hash: self.latest_hash().expect("condition of use"), + in_journal: journal.prev_journal_hash(), + journal: Box::new(journal), + } + } + + /// Make a [`JournalSetError::WrongHeight`]. + fn wrong_height(&self, journal: Journal<'a>) -> JournalSetError<'a> { + JournalSetError::WrongHeight { + actual: journal.rollup_height(), + expected: self.latest_height().expect("condition of use") + 1, + journal: Box::new(journal), + } + } + + /// Returns the earliest height of a journal in the set. + pub fn earliest_height(&self) -> Option { + if let Some(journal) = self.journals.front() { + return Some(journal.rollup_height()); + } + None + } + + /// Returns the latest hash of a journal in the set. + pub const fn latest_hash(&self) -> Option { + self.latest_hash + } + + /// Returns the latest height of a journal in the set. + pub const fn latest_height(&self) -> Option { + self.latest_height + } + + /// Get the index of the header with the rollup height within the inner + /// set, None if not present + fn index_of(&self, rollup_height: u64) -> Option { + let start = self.earliest_height()?; + if rollup_height < start || rollup_height > self.latest_height()? { + return None; + } + + Some((rollup_height - start) as usize) + } + + /// Get the block at that height, if it is within the set. + pub fn get_by_rollup_height(&self, rollup_height: u64) -> Option<&Journal<'a>> { + let index = self.index_of(rollup_height)?; + self.journals.get(index) + } + + /// Returns the range of heights in the set. If the set is empty, returns + /// `None`. + pub fn range(&self) -> Option> { + let start = self.earliest_height()?; + let end = self.latest_height()?; + + Some(start..=end) + } + + /// Check that the journal contains the expected next height. + fn check_last_height(&self, journal: Journal<'a>) -> Result, JournalSetError<'a>> { + // if we have initialized the last_height, the journal should be + // exactly that height + 1 + if let Some(latest_height) = self.latest_height() { + if journal.rollup_height() != latest_height + 1 { + return Err(self.wrong_height(journal)); + } + } + Ok(journal) + } + + /// Check that the journal contains the expected prev_hash + fn check_prev_hash(&self, journal: Journal<'a>) -> Result, JournalSetError<'a>> { + // if we have journals, the journal's prev hash should match the last + // journal's hash + if let Some(latest_hash) = self.latest_hash() { + if journal.prev_journal_hash() != latest_hash { + return Err(self.wrong_prev_hash(journal)); + } + } + Ok(journal) + } + + /// Unwind to the height of the journal. + /// + /// ## Condition of use: + /// + /// Height of the journal must be in range. + fn unwind_to(&mut self, journal: &Journal<'a>) { + let Some(idx) = self.index_of(journal.rollup_height()) else { + unreachable!("condition of use"); + }; + + // truncate to idx + 1, then pop the back + // e.g. if the idx is 2, we want to keep 3 items. + // this puts 2 at the back. then we use `pop_back` + // to ensure our latest_height and latest_hash are + // updated. + self.journals.truncate(idx + 1); + self.pop_back(); + } + + fn append_inner(&mut self, journal: Journal<'a>) { + self.latest_height = Some(journal.rollup_height()); + self.latest_hash = Some(journal.journal_hash()); + self.journals.push_back(journal); + } + + /// Push the journal into the set. + pub fn try_append(&mut self, journal: Journal<'a>) -> Result<(), JournalSetError<'a>> { + // Check the journal's height + let journal = self.check_last_height(journal)?; + let journal = self.check_prev_hash(journal)?; + + self.append_inner(journal); + + Ok(()) + } + + /// Appends the journal to the set, removing any journals that conflict + /// with it. + /// + /// This will only succeed if the journal is within the set's range AND + /// replacing the journal currently at that height would lead to a + /// consistent history. + pub fn append_overwrite(&mut self, journal: Journal<'a>) -> Result<(), JournalSetError<'a>> { + let Some(j) = self.get_by_rollup_height(journal.rollup_height()) else { + return Err(self.not_in_range(journal)); + }; + + // If the journals are identical, do nothin. + if j.journal_hash() == journal.journal_hash() { + return Ok(()); + } + + if j.rollup_height() != journal.rollup_height() { + return Err(self.wrong_height(journal)); + } + + // If they don't have the same prev hash, return an error. + if j.prev_journal_hash() != journal.prev_journal_hash() { + return Err(self.wrong_prev_hash(journal)); + } + + self.unwind_to(&journal); + self.append_inner(journal); + Ok(()) + } + + /// Pops the front journal from the set. + pub fn pop_front(&mut self) -> Option> { + self.journals.pop_front() + } + + /// Pops the back journal from the set. + pub fn pop_back(&mut self) -> Option> { + let journal = self.journals.pop_back(); + + // This also handles the case where the popped header had a height of + // zero. + if let Some(journal) = &journal { + self.latest_height = Some(journal.rollup_height() - 1); + self.latest_hash = Some(journal.prev_journal_hash()); + } + journal + } +} + +impl<'a> IntoIterator for JournalSet<'a> { + type Item = Journal<'a>; + type IntoIter = std::collections::vec_deque::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.journals.into_iter() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{HostJournal, JournalMeta}; + use alloy::{consensus::Header, primitives::Bytes}; + use trevm::{journal::BundleStateIndex, revm::state::Bytecode}; + + fn journal_at_heights(host: u64, rollup: u64, prev_hash: B256) -> Journal<'static> { + let meta = + JournalMeta::new(host, prev_hash, Header { number: rollup, ..Default::default() }); + let host = HostJournal::new(meta, Default::default()); + + Journal::V1(host) + } + + #[test] + fn basic_consistency() { + let mut set = JournalSet::new(); + + let j0 = journal_at_heights(100, 0, B256::repeat_byte(0)); + let j1 = journal_at_heights(101, 1, j0.journal_hash()); + let j2 = journal_at_heights(102, 2, j1.journal_hash()); + let j3 = journal_at_heights(103, 3, j2.journal_hash()); + + // empty set + assert_eq!(set.earliest_height(), None); + assert_eq!(set.latest_height(), None); + assert_eq!(set.latest_hash(), None); + assert_eq!(set.range(), None); + + // push j0 + assert_eq!(set.try_append(j0.clone()), Ok(())); + assert_eq!(set.earliest_height(), Some(0)); + assert_eq!(set.latest_height(), Some(0)); + assert_eq!(set.latest_hash(), Some(j0.journal_hash())); + assert_eq!(set.range(), Some(0..=0)); + + // pushing j2 should fail + assert!(set.try_append(j2.clone()).is_err()); + + // push j1 + assert_eq!(set.try_append(j1.clone()), Ok(())); + assert_eq!(set.earliest_height(), Some(0)); + assert_eq!(set.latest_height(), Some(1)); + assert_eq!(set.latest_hash(), Some(j1.journal_hash())); + assert_eq!(set.range(), Some(0..=1)); + + // pushing j3 should fail + assert!(set.try_append(j3.clone()).is_err()); + + // pop j0 from front + let popped = set.pop_front().expect("should pop"); + assert_eq!(popped, j0); + assert_eq!(set.earliest_height(), Some(1)); + assert_eq!(set.latest_height(), Some(1)); + assert_eq!(set.latest_hash(), Some(j1.journal_hash())); + + // push j2 + assert_eq!(set.try_append(j2.clone()), Ok(())); + assert_eq!(set.earliest_height(), Some(1)); + assert_eq!(set.latest_height(), Some(2)); + assert_eq!(set.latest_hash(), Some(j2.journal_hash())); + assert_eq!(set.range(), Some(1..=2)); + + // push j3 + assert_eq!(set.try_append(j3.clone()), Ok(())); + assert_eq!(set.earliest_height(), Some(1)); + assert_eq!(set.latest_height(), Some(3)); + assert_eq!(set.latest_hash(), Some(j3.journal_hash())); + assert_eq!(set.range(), Some(1..=3)); + + // pop j1 from front + let popped = set.pop_front().expect("should pop"); + assert_eq!(popped, j1); + assert_eq!(set.earliest_height(), Some(2)); + assert_eq!(set.latest_height(), Some(3)); + assert_eq!(set.latest_hash(), Some(j3.journal_hash())); + + // pushing front to back should fail + assert!(set.try_append(j0.clone()).is_err()); + } + + #[test] + fn append_overwrite() { + let mut set = JournalSet::new(); + + let j0 = journal_at_heights(100, 0, B256::repeat_byte(0)); + let j1 = journal_at_heights(101, 1, j0.journal_hash()); + let j2 = journal_at_heights(102, 2, j1.journal_hash()); + let j3 = journal_at_heights(103, 3, j2.journal_hash()); + + let mut j1_alt_state = BundleStateIndex::default(); + j1_alt_state.new_contracts.insert( + B256::repeat_byte(1), + std::borrow::Cow::Owned(Bytecode::new_legacy(Bytes::from_static(&[0, 1, 2, 3]))), + ); + let j1_alt = Journal::V1(HostJournal::new( + JournalMeta::new(101, j0.journal_hash(), Header { number: 1, ..Default::default() }), + j1_alt_state, + )); + + // push j0-j3 + assert!(set.try_append(j0.clone()).is_ok()); + assert!(set.try_append(j1).is_ok()); + assert!(set.try_append(j2.clone()).is_ok()); + assert!(set.try_append(j3).is_ok()); + assert_eq!(set.len(), 4); + + // overwrite + assert!(set.append_overwrite(j1_alt).is_ok()); + assert_eq!(set.len(), 2); + + // can't push j2 anymore + assert!(set.try_append(j2).is_err()); + } +} diff --git a/crates/journal/src/versions.rs b/crates/journal/src/versions.rs new file mode 100644 index 0000000..5204096 --- /dev/null +++ b/crates/journal/src/versions.rs @@ -0,0 +1,106 @@ +use alloy::{consensus::Header, primitives::B256}; +use trevm::journal::{JournalDecode, JournalEncode}; + +use crate::HostJournal; + +/// Journal versions. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum Journal<'a> { + /// Version 1 + V1(HostJournal<'a>), +} + +impl<'a> Journal<'a> { + /// Get the host height. + pub const fn host_height(&self) -> u64 { + match self { + Journal::V1(journal) => journal.host_height(), + } + } + + /// Get the previous journal hash. + pub const fn prev_journal_hash(&self) -> B256 { + match self { + Journal::V1(journal) => journal.prev_journal_hash(), + } + } + + /// Get the rollup block header. + pub const fn header(&self) -> &Header { + match self { + Journal::V1(journal) => journal.header(), + } + } + + /// Get a reference to the host journal. + pub const fn journal(&self) -> &HostJournal<'a> { + match self { + Journal::V1(journal) => journal, + } + } + + /// Get the journal hash. + pub fn journal_hash(&self) -> B256 { + match self { + Journal::V1(journal) => journal.journal_hash(), + } + } + + /// Get the rollup height. + pub const fn rollup_height(&self) -> u64 { + match self { + Journal::V1(journal) => journal.rollup_height(), + } + } +} + +impl JournalEncode for Journal<'_> { + fn serialized_size(&self) -> usize { + // 1 byte for the version + 1 + match self { + Journal::V1(journal) => journal.serialized_size(), + } + } + + fn encode(&self, buf: &mut dyn alloy::rlp::BufMut) { + match self { + Journal::V1(journal) => { + 1u8.encode(buf); + journal.encode(buf) + } + } + } +} + +impl JournalDecode for Journal<'static> { + fn decode(buf: &mut &[u8]) -> Result { + let version: u8 = JournalDecode::decode(buf)?; + match version { + 1 => JournalDecode::decode(buf).map(Journal::V1), + _ => Err(trevm::journal::JournalDecodeError::InvalidTag { + ty_name: "Journal", + tag: version, + max_expected: 1, + }), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{host::test::make_state_diff, JournalMeta}; + + #[test] + fn roundtrip() { + let journal = Journal::V1(HostJournal::new( + JournalMeta::new(42, B256::repeat_byte(0x17), Header::default()), + make_state_diff(), + )); + let mut buf = Vec::new(); + journal.encode(&mut buf); + let decoded = Journal::decode(&mut &buf[..]).unwrap(); + assert_eq!(journal, decoded); + } +}