Skip to main content

signet_journal/
set.rs

1use crate::Journal;
2use alloy::primitives::B256;
3use std::{collections::VecDeque, ops::RangeInclusive};
4
5#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
6pub enum JournalSetError<'a> {
7    /// Cannot ingest the journal because it is at the wrong height.
8    #[error("wrong height: actual {actual}, expected {expected}")]
9    WrongHeight {
10        /// The actual height of the journal.
11        actual: u64,
12
13        /// The expected height of the journal.
14        expected: u64,
15
16        /// The journal.
17        journal: Box<Journal<'a>>,
18    },
19
20    /// Cannot ingest the journal because it has the wrong previous hash.
21    #[error("wrong prev_hash: current {latest_hash}, new journal expected {in_journal}")]
22    WrongPrevHash {
23        /// The latest hash of the journal.
24        latest_hash: B256,
25
26        /// The hash expected during ingestion.
27        in_journal: B256,
28
29        /// The journal.
30        journal: Box<Journal<'a>>,
31    },
32
33    /// Attempted to append_overwrite a journal that is not in the set's range.
34    #[error("not in range: start {start:?}, end {end:?}, height {height}")]
35    NotInRange {
36        /// The start of the expected range.
37        start: Option<u64>,
38
39        /// The end of the expected range.
40        end: Option<u64>,
41
42        /// The height of the journal.
43        height: u64,
44
45        /// The journal
46        journal: Box<Journal<'a>>,
47    },
48}
49
50impl<'a> JournalSetError<'a> {
51    /// Converts the error into a journal, discarding error info.
52    pub fn into_journal(self) -> Journal<'a> {
53        match self {
54            Self::WrongHeight { journal, .. } => *journal,
55            Self::WrongPrevHash { journal, .. } => *journal,
56            Self::NotInRange { journal, .. } => *journal,
57        }
58    }
59}
60
61/// A set of journals, ordered by height and hash.
62#[derive(Debug, Clone, Default)]
63pub struct JournalSet<'a> {
64    /// The set of journals.
65    journals: VecDeque<Journal<'a>>,
66
67    /// The latest height, recorded separately so that if the set is drained,
68    /// pushing more journals is still checked for consistency.
69    latest_height: Option<u64>,
70
71    /// The latest journal hash.
72    latest_hash: Option<B256>,
73}
74
75impl<'a> JournalSet<'a> {
76    /// Creates a new empty `JournalSet`.
77    pub const fn new() -> Self {
78        Self { journals: VecDeque::new(), latest_height: None, latest_hash: None }
79    }
80
81    /// Creates a new `JournalSet` with the specified capacity.
82    pub fn with_capacity(capacity: usize) -> Self {
83        Self { journals: VecDeque::with_capacity(capacity), latest_height: None, latest_hash: None }
84    }
85
86    /// Creates a new `JournalSet` from a single [`Journal`].
87    pub fn from_journal(journal: Journal<'a>) -> Self {
88        let latest_height = Some(journal.rollup_height());
89        let latest_hash = Some(journal.journal_hash());
90        let mut journals = VecDeque::new();
91        journals.push_back(journal);
92        Self { journals, latest_height, latest_hash }
93    }
94
95    /// Returns the number of journals in the set.
96    pub fn len(&self) -> usize {
97        self.journals.len()
98    }
99
100    /// True if the set is empty.
101    pub fn is_empty(&self) -> bool {
102        self.journals.is_empty()
103    }
104
105    /// Make a [`JournalSetError::NotInRange`].
106    fn not_in_range(&self, journal: Journal<'a>) -> JournalSetError<'a> {
107        JournalSetError::NotInRange {
108            start: self.earliest_height(),
109            end: self.latest_height(),
110            height: journal.rollup_height(),
111            journal: Box::new(journal),
112        }
113    }
114
115    /// Make a [`JournalSetError::WrongPrevHash`].
116    fn wrong_prev_hash(&self, journal: Journal<'a>) -> JournalSetError<'a> {
117        JournalSetError::WrongPrevHash {
118            latest_hash: self.latest_hash().expect("condition of use"),
119            in_journal: journal.prev_journal_hash(),
120            journal: Box::new(journal),
121        }
122    }
123
124    /// Make a [`JournalSetError::WrongHeight`].
125    fn wrong_height(&self, journal: Journal<'a>) -> JournalSetError<'a> {
126        JournalSetError::WrongHeight {
127            actual: journal.rollup_height(),
128            expected: self.latest_height().expect("condition of use") + 1,
129            journal: Box::new(journal),
130        }
131    }
132
133    /// Returns the earliest height of a journal in the set.
134    pub fn earliest_height(&self) -> Option<u64> {
135        if let Some(journal) = self.journals.front() {
136            return Some(journal.rollup_height());
137        }
138        None
139    }
140
141    /// Returns the latest hash of a journal in the set.
142    pub const fn latest_hash(&self) -> Option<B256> {
143        self.latest_hash
144    }
145
146    /// Returns the latest height of a journal in the set.
147    pub const fn latest_height(&self) -> Option<u64> {
148        self.latest_height
149    }
150
151    /// Get the index of the header with the rollup height within the inner
152    /// set, None if not present
153    fn index_of(&self, rollup_height: u64) -> Option<usize> {
154        let start = self.earliest_height()?;
155        if rollup_height < start || rollup_height > self.latest_height()? {
156            return None;
157        }
158
159        Some((rollup_height - start) as usize)
160    }
161
162    /// Get the block at that height, if it is within the set.
163    pub fn get_by_rollup_height(&self, rollup_height: u64) -> Option<&Journal<'a>> {
164        let index = self.index_of(rollup_height)?;
165        self.journals.get(index)
166    }
167
168    /// Returns the range of heights in the set. If the set is empty, returns
169    /// `None`.
170    pub fn range(&self) -> Option<RangeInclusive<u64>> {
171        let start = self.earliest_height()?;
172        let end = self.latest_height()?;
173
174        Some(start..=end)
175    }
176
177    /// Check that the journal contains the expected next height.
178    fn check_last_height(&self, journal: Journal<'a>) -> Result<Journal<'a>, JournalSetError<'a>> {
179        // if we have initialized the last_height, the journal should be
180        // exactly that height + 1
181        if let Some(latest_height) = self.latest_height() {
182            if journal.rollup_height() != latest_height + 1 {
183                return Err(self.wrong_height(journal));
184            }
185        }
186        Ok(journal)
187    }
188
189    /// Check that the journal contains the expected prev_hash
190    fn check_prev_hash(&self, journal: Journal<'a>) -> Result<Journal<'a>, JournalSetError<'a>> {
191        // if we have journals, the journal's prev hash should match the last
192        // journal's hash
193        if let Some(latest_hash) = self.latest_hash() {
194            if journal.prev_journal_hash() != latest_hash {
195                return Err(self.wrong_prev_hash(journal));
196            }
197        }
198        Ok(journal)
199    }
200
201    /// Unwind to the height of the journal.
202    ///
203    /// ## Condition of use:
204    ///
205    /// Height of the journal must be in range.
206    fn unwind_to(&mut self, journal: &Journal<'a>) {
207        let Some(idx) = self.index_of(journal.rollup_height()) else {
208            unreachable!("condition of use");
209        };
210
211        // truncate to idx + 1, then pop the back
212        // e.g. if the idx is 2, we want to keep 3 items.
213        //      this puts 2 at the back. then we use `pop_back`
214        //      to ensure our latest_height and latest_hash are
215        //      updated.
216        self.journals.truncate(idx + 1);
217        self.pop_back();
218    }
219
220    fn append_inner(&mut self, journal: Journal<'a>) {
221        self.latest_height = Some(journal.rollup_height());
222        self.latest_hash = Some(journal.journal_hash());
223        self.journals.push_back(journal);
224    }
225
226    /// Push the journal into the set.
227    pub fn try_append(&mut self, journal: Journal<'a>) -> Result<(), JournalSetError<'a>> {
228        // Check the journal's height
229        let journal = self.check_last_height(journal)?;
230        let journal = self.check_prev_hash(journal)?;
231
232        self.append_inner(journal);
233
234        Ok(())
235    }
236
237    /// Appends the journal to the set, removing any journals that conflict
238    /// with it.
239    ///
240    /// This will only succeed if the journal is within the set's range AND
241    /// replacing the journal currently at that height would lead to a
242    /// consistent history.
243    pub fn append_overwrite(&mut self, journal: Journal<'a>) -> Result<(), JournalSetError<'a>> {
244        let Some(j) = self.get_by_rollup_height(journal.rollup_height()) else {
245            return Err(self.not_in_range(journal));
246        };
247
248        // If the journals are identical, do nothin.
249        if j.journal_hash() == journal.journal_hash() {
250            return Ok(());
251        }
252
253        if j.rollup_height() != journal.rollup_height() {
254            return Err(self.wrong_height(journal));
255        }
256
257        // If they don't have the same prev hash, return an error.
258        if j.prev_journal_hash() != journal.prev_journal_hash() {
259            return Err(self.wrong_prev_hash(journal));
260        }
261
262        self.unwind_to(&journal);
263        self.append_inner(journal);
264        Ok(())
265    }
266
267    /// Pops the front journal from the set.
268    pub fn pop_front(&mut self) -> Option<Journal<'a>> {
269        self.journals.pop_front()
270    }
271
272    /// Pops the back journal from the set.
273    pub fn pop_back(&mut self) -> Option<Journal<'a>> {
274        let journal = self.journals.pop_back();
275
276        // This also handles the case where the popped header had a height of
277        // zero.
278        if let Some(journal) = &journal {
279            self.latest_height = Some(journal.rollup_height() - 1);
280            self.latest_hash = Some(journal.prev_journal_hash());
281        }
282        journal
283    }
284}
285
286impl<'a> IntoIterator for JournalSet<'a> {
287    type Item = Journal<'a>;
288    type IntoIter = std::collections::vec_deque::IntoIter<Journal<'a>>;
289
290    fn into_iter(self) -> Self::IntoIter {
291        self.journals.into_iter()
292    }
293}
294
295#[cfg(test)]
296mod test {
297    use super::*;
298    use crate::{HostJournal, JournalMeta};
299    use alloy::{consensus::Header, primitives::Bytes};
300    use std::borrow::Cow;
301    use trevm::{journal::BundleStateIndex, revm::state::Bytecode};
302
303    fn journal_at_heights(host: u64, rollup: u64, prev_hash: B256) -> Journal<'static> {
304        let meta = JournalMeta::new(
305            host,
306            prev_hash,
307            Cow::Owned(Header { number: rollup, ..Default::default() }),
308        );
309        let host = HostJournal::new(meta, Default::default());
310
311        Journal::V1(host)
312    }
313
314    #[test]
315    fn basic_consistency() {
316        let mut set = JournalSet::new();
317
318        let j0 = journal_at_heights(100, 0, B256::repeat_byte(0));
319        let j1 = journal_at_heights(101, 1, j0.journal_hash());
320        let j2 = journal_at_heights(102, 2, j1.journal_hash());
321        let j3 = journal_at_heights(103, 3, j2.journal_hash());
322
323        // empty set
324        assert_eq!(set.earliest_height(), None);
325        assert_eq!(set.latest_height(), None);
326        assert_eq!(set.latest_hash(), None);
327        assert_eq!(set.range(), None);
328
329        // push j0
330        assert_eq!(set.try_append(j0.clone()), Ok(()));
331        assert_eq!(set.earliest_height(), Some(0));
332        assert_eq!(set.latest_height(), Some(0));
333        assert_eq!(set.latest_hash(), Some(j0.journal_hash()));
334        assert_eq!(set.range(), Some(0..=0));
335
336        // pushing j2 should fail
337        assert!(set.try_append(j2.clone()).is_err());
338
339        // push j1
340        assert_eq!(set.try_append(j1.clone()), Ok(()));
341        assert_eq!(set.earliest_height(), Some(0));
342        assert_eq!(set.latest_height(), Some(1));
343        assert_eq!(set.latest_hash(), Some(j1.journal_hash()));
344        assert_eq!(set.range(), Some(0..=1));
345
346        // pushing j3 should fail
347        assert!(set.try_append(j3.clone()).is_err());
348
349        // pop j0 from front
350        let popped = set.pop_front().expect("should pop");
351        assert_eq!(popped, j0);
352        assert_eq!(set.earliest_height(), Some(1));
353        assert_eq!(set.latest_height(), Some(1));
354        assert_eq!(set.latest_hash(), Some(j1.journal_hash()));
355
356        // push j2
357        assert_eq!(set.try_append(j2.clone()), Ok(()));
358        assert_eq!(set.earliest_height(), Some(1));
359        assert_eq!(set.latest_height(), Some(2));
360        assert_eq!(set.latest_hash(), Some(j2.journal_hash()));
361        assert_eq!(set.range(), Some(1..=2));
362
363        // push j3
364        assert_eq!(set.try_append(j3.clone()), Ok(()));
365        assert_eq!(set.earliest_height(), Some(1));
366        assert_eq!(set.latest_height(), Some(3));
367        assert_eq!(set.latest_hash(), Some(j3.journal_hash()));
368        assert_eq!(set.range(), Some(1..=3));
369
370        // pop j1 from front
371        let popped = set.pop_front().expect("should pop");
372        assert_eq!(popped, j1);
373        assert_eq!(set.earliest_height(), Some(2));
374        assert_eq!(set.latest_height(), Some(3));
375        assert_eq!(set.latest_hash(), Some(j3.journal_hash()));
376
377        // pushing front to back should fail
378        assert!(set.try_append(j0.clone()).is_err());
379    }
380
381    #[test]
382    fn append_overwrite() {
383        let mut set = JournalSet::new();
384
385        let j0 = journal_at_heights(100, 0, B256::repeat_byte(0));
386        let j1 = journal_at_heights(101, 1, j0.journal_hash());
387        let j2 = journal_at_heights(102, 2, j1.journal_hash());
388        let j3 = journal_at_heights(103, 3, j2.journal_hash());
389
390        let mut j1_alt_state = BundleStateIndex::default();
391        j1_alt_state.new_contracts.insert(
392            B256::repeat_byte(1),
393            std::borrow::Cow::Owned(Bytecode::new_legacy(Bytes::from_static(&[0, 1, 2, 3]))),
394        );
395        let j1_alt = Journal::V1(HostJournal::new(
396            JournalMeta::new(
397                101,
398                j0.journal_hash(),
399                Cow::Owned(Header { number: 1, ..Default::default() }),
400            ),
401            j1_alt_state,
402        ));
403
404        // push j0-j3
405        assert!(set.try_append(j0.clone()).is_ok());
406        assert!(set.try_append(j1).is_ok());
407        assert!(set.try_append(j2.clone()).is_ok());
408        assert!(set.try_append(j3).is_ok());
409        assert_eq!(set.len(), 4);
410
411        // overwrite
412        assert!(set.append_overwrite(j1_alt).is_ok());
413        assert_eq!(set.len(), 2);
414
415        // can't push j2 anymore
416        assert!(set.try_append(j2).is_err());
417    }
418}