1use ethrex_common::H256;
53
54pub const JOURNAL_VERSION: u8 = 1;
66
67pub type ReverseDiffEntry = (Vec<u8>, Option<Vec<u8>>);
75
76pub type FlatDiff = Vec<ReverseDiffEntry>;
78
79#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct JournalEntry {
87 pub block_hash: H256,
89 pub parent_state_root: H256,
91 pub account_trie_diff: FlatDiff,
93 pub storage_trie_diff: FlatDiff,
96 pub account_flat_diff: FlatDiff,
98 pub storage_flat_diff: FlatDiff,
101}
102
103#[derive(Debug, thiserror::Error, PartialEq, Eq)]
105pub enum JournalDecodeError {
106 #[error("journal entry truncated: expected {expected} more bytes at offset {offset}")]
107 Truncated { offset: usize, expected: usize },
108 #[error("journal entry version mismatch: expected {expected}, found {found}")]
109 VersionMismatch { expected: u8, found: u8 },
110 #[error("journal entry varint overflow at offset {offset}")]
111 VarintOverflow { offset: usize },
112 #[error(
113 "journal entry presence byte invalid: expected 0 or 1, found {found} at offset {offset}"
114 )]
115 InvalidPresenceByte { offset: usize, found: u8 },
116 #[error("journal entry has {trailing} trailing bytes after offset {offset}")]
117 TrailingBytes { offset: usize, trailing: usize },
118 #[error(
119 "journal entry length prefix {claimed} at offset {offset} exceeds remaining bytes {remaining}"
120 )]
121 LengthExceedsRemaining {
122 offset: usize,
123 claimed: u64,
124 remaining: usize,
125 },
126}
127
128impl JournalEntry {
129 pub fn encode(&self) -> Vec<u8> {
131 let approx = 1
132 + 32
133 + 32
134 + diff_byte_estimate(&self.account_trie_diff)
135 + diff_byte_estimate(&self.storage_trie_diff)
136 + diff_byte_estimate(&self.account_flat_diff)
137 + diff_byte_estimate(&self.storage_flat_diff);
138 let mut out = Vec::with_capacity(approx);
139
140 out.push(JOURNAL_VERSION);
141 out.extend_from_slice(self.block_hash.as_bytes());
142 out.extend_from_slice(self.parent_state_root.as_bytes());
143
144 encode_flat_diff(&mut out, &self.account_trie_diff);
145 encode_flat_diff(&mut out, &self.storage_trie_diff);
146 encode_flat_diff(&mut out, &self.account_flat_diff);
147 encode_flat_diff(&mut out, &self.storage_flat_diff);
148
149 out
150 }
151
152 pub fn decode_block_hash(bytes: &[u8]) -> Result<H256, JournalDecodeError> {
157 let Some(&version) = bytes.first() else {
158 return Err(JournalDecodeError::Truncated {
159 offset: 0,
160 expected: 1,
161 });
162 };
163 if version != JOURNAL_VERSION {
164 return Err(JournalDecodeError::VersionMismatch {
165 expected: JOURNAL_VERSION,
166 found: version,
167 });
168 }
169 let Some(hash) = bytes.get(1..33) else {
170 return Err(JournalDecodeError::Truncated {
171 offset: 1,
172 expected: 32,
173 });
174 };
175 Ok(H256::from_slice(hash))
176 }
177
178 pub fn decode(bytes: &[u8]) -> Result<Self, JournalDecodeError> {
185 let mut cur = Cursor::new(bytes);
186
187 let version = cur.read_byte()?;
188 if version != JOURNAL_VERSION {
189 return Err(JournalDecodeError::VersionMismatch {
190 expected: JOURNAL_VERSION,
191 found: version,
192 });
193 }
194
195 let block_hash = cur.read_h256()?;
196 let parent_state_root = cur.read_h256()?;
197
198 let account_trie_diff = decode_flat_diff(&mut cur)?;
199 let storage_trie_diff = decode_flat_diff(&mut cur)?;
200 let account_flat_diff = decode_flat_diff(&mut cur)?;
201 let storage_flat_diff = decode_flat_diff(&mut cur)?;
202
203 if cur.offset != bytes.len() {
206 return Err(JournalDecodeError::TrailingBytes {
207 offset: cur.offset,
208 trailing: bytes.len() - cur.offset,
209 });
210 }
211
212 Ok(Self {
213 block_hash,
214 parent_state_root,
215 account_trie_diff,
216 storage_trie_diff,
217 account_flat_diff,
218 storage_flat_diff,
219 })
220 }
221}
222
223fn encode_varint(out: &mut Vec<u8>, mut value: u64) {
224 while value >= 0x80 {
225 out.push((value as u8 & 0x7f) | 0x80);
226 value >>= 7;
227 }
228 out.push(value as u8);
229}
230
231fn encode_flat_diff(out: &mut Vec<u8>, diff: &[ReverseDiffEntry]) {
232 encode_varint(out, diff.len() as u64);
233 for (path, value) in diff {
234 encode_varint(out, path.len() as u64);
235 out.extend_from_slice(path);
236 match value {
237 None => out.push(0),
238 Some(v) => {
239 out.push(1);
240 encode_varint(out, v.len() as u64);
241 out.extend_from_slice(v);
242 }
243 }
244 }
245}
246
247fn varint_len(value: u64) -> usize {
250 let bits = 64 - value.leading_zeros() as usize;
251 bits.div_ceil(7).max(1)
252}
253
254fn diff_byte_estimate(diff: &[ReverseDiffEntry]) -> usize {
255 diff.iter()
258 .map(|(p, v)| {
259 varint_len(p.len() as u64)
260 + p.len()
261 + 1
262 + v.as_ref()
263 .map_or(0, |v| varint_len(v.len() as u64) + v.len())
264 })
265 .sum::<usize>()
266 + varint_len(diff.len() as u64)
267}
268
269struct Cursor<'a> {
270 bytes: &'a [u8],
271 offset: usize,
272}
273
274impl<'a> Cursor<'a> {
275 fn new(bytes: &'a [u8]) -> Self {
276 Self { bytes, offset: 0 }
277 }
278
279 fn read_byte(&mut self) -> Result<u8, JournalDecodeError> {
280 if self.offset >= self.bytes.len() {
281 return Err(JournalDecodeError::Truncated {
282 offset: self.offset,
283 expected: 1,
284 });
285 }
286 let b = self.bytes[self.offset];
287 self.offset += 1;
288 Ok(b)
289 }
290
291 fn read_slice(&mut self, n: usize) -> Result<&'a [u8], JournalDecodeError> {
292 let remaining = self.bytes.len().saturating_sub(self.offset);
295 if remaining < n {
296 return Err(JournalDecodeError::Truncated {
297 offset: self.offset,
298 expected: n,
299 });
300 }
301 let s = &self.bytes[self.offset..self.offset + n];
302 self.offset += n;
303 Ok(s)
304 }
305
306 fn remaining(&self) -> usize {
308 self.bytes.len().saturating_sub(self.offset)
309 }
310
311 fn read_h256(&mut self) -> Result<H256, JournalDecodeError> {
312 let s = self.read_slice(32)?;
313 Ok(H256::from_slice(s))
314 }
315
316 fn read_varint(&mut self) -> Result<u64, JournalDecodeError> {
317 let mut result: u64 = 0;
318 let mut shift: u32 = 0;
319 loop {
320 let b = self.read_byte()?;
321 if shift >= 64 {
324 return Err(JournalDecodeError::VarintOverflow {
325 offset: self.offset - 1,
326 });
327 }
328 if shift == 63 && (b & 0x7e != 0 || b & 0x80 != 0) {
332 return Err(JournalDecodeError::VarintOverflow {
333 offset: self.offset - 1,
334 });
335 }
336 result |= ((b & 0x7f) as u64) << shift;
337 if b & 0x80 == 0 {
338 return Ok(result);
339 }
340 shift += 7;
341 }
342 }
343}
344
345const MIN_ENTRY_BYTES: usize = 2;
349
350fn decode_flat_diff(cur: &mut Cursor<'_>) -> Result<FlatDiff, JournalDecodeError> {
351 let count_offset = cur.offset;
352 let count_u64 = cur.read_varint()?;
353 let remaining = cur.remaining();
354 let max_possible = remaining / MIN_ENTRY_BYTES;
358 if count_u64 as usize > max_possible {
359 return Err(JournalDecodeError::LengthExceedsRemaining {
360 offset: count_offset,
361 claimed: count_u64,
362 remaining,
363 });
364 }
365 let count = count_u64 as usize;
366 let mut out = Vec::with_capacity(count);
367 for _ in 0..count {
368 let path_len_offset = cur.offset;
369 let path_len_u64 = cur.read_varint()?;
370 let remaining = cur.remaining();
372 if path_len_u64 > remaining as u64 {
373 return Err(JournalDecodeError::LengthExceedsRemaining {
374 offset: path_len_offset,
375 claimed: path_len_u64,
376 remaining,
377 });
378 }
379 let path_len = path_len_u64 as usize;
380 let path = cur.read_slice(path_len)?.to_vec();
381 let presence_offset = cur.offset;
382 let presence = cur.read_byte()?;
383 let value = match presence {
384 0 => None,
385 1 => {
386 let value_len_offset = cur.offset;
387 let value_len_u64 = cur.read_varint()?;
388 let remaining = cur.remaining();
389 if value_len_u64 > remaining as u64 {
390 return Err(JournalDecodeError::LengthExceedsRemaining {
391 offset: value_len_offset,
392 claimed: value_len_u64,
393 remaining,
394 });
395 }
396 let value_len = value_len_u64 as usize;
397 Some(cur.read_slice(value_len)?.to_vec())
398 }
399 other => {
400 return Err(JournalDecodeError::InvalidPresenceByte {
401 offset: presence_offset,
402 found: other,
403 });
404 }
405 };
406 out.push((path, value));
407 }
408 Ok(out)
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 fn h(b: u8) -> H256 {
416 H256::repeat_byte(b)
417 }
418
419 fn round_trip(entry: &JournalEntry) {
420 let bytes = entry.encode();
421 let decoded = JournalEntry::decode(&bytes).unwrap();
422 assert_eq!(&decoded, entry);
423 }
424
425 #[test]
426 fn empty_entry_round_trips() {
427 let entry = JournalEntry {
428 block_hash: h(0xaa),
429 parent_state_root: h(0xbb),
430 account_trie_diff: vec![],
431 storage_trie_diff: vec![],
432 account_flat_diff: vec![],
433 storage_flat_diff: vec![],
434 };
435 round_trip(&entry);
436 assert_eq!(entry.encode().len(), 69);
438 }
439
440 #[test]
441 fn typical_entry_round_trips() {
442 let entry = JournalEntry {
443 block_hash: h(0x11),
444 parent_state_root: h(0x22),
445 account_trie_diff: vec![
446 (vec![0x00, 0x01], Some(vec![0xde, 0xad, 0xbe, 0xef])),
447 (vec![0x02], None),
448 ],
449 storage_trie_diff: vec![(vec![0x0a; 67], Some(vec![0xff])), (vec![0x0b; 68], None)],
450 account_flat_diff: vec![(vec![0xaa; 65], Some(vec![0x01, 0x02, 0x03]))],
451 storage_flat_diff: vec![(vec![0xbb; 131], None)],
452 };
453 round_trip(&entry);
454 }
455
456 #[test]
457 fn entry_with_only_absences_round_trips() {
458 let entry = JournalEntry {
459 block_hash: h(0x55),
460 parent_state_root: h(0x66),
461 account_trie_diff: vec![(vec![0x00], None), (vec![0x01], None), (vec![0x02], None)],
462 storage_trie_diff: vec![],
463 account_flat_diff: vec![(vec![0xaa; 32], None)],
464 storage_flat_diff: vec![],
465 };
466 round_trip(&entry);
467 }
468
469 #[test]
470 fn large_entry_round_trips() {
471 let mut account_trie_diff = Vec::with_capacity(10_000);
472 for i in 0u32..10_000 {
473 let path = i.to_be_bytes().to_vec();
474 let value = if i % 7 == 0 {
475 None
476 } else {
477 Some(vec![(i & 0xff) as u8; (i % 200) as usize])
478 };
479 account_trie_diff.push((path, value));
480 }
481 let entry = JournalEntry {
482 block_hash: h(0xee),
483 parent_state_root: h(0xff),
484 account_trie_diff,
485 storage_trie_diff: vec![],
486 account_flat_diff: vec![],
487 storage_flat_diff: vec![],
488 };
489 round_trip(&entry);
490 }
491
492 #[test]
493 fn rejects_unknown_version() {
494 let mut bytes = vec![0xff];
495 bytes.extend_from_slice(&[0; 32]);
496 bytes.extend_from_slice(&[0; 32]);
497 bytes.extend_from_slice(&[0, 0, 0, 0]);
498 let err = JournalEntry::decode(&bytes).unwrap_err();
499 assert_eq!(
500 err,
501 JournalDecodeError::VersionMismatch {
502 expected: JOURNAL_VERSION,
503 found: 0xff,
504 }
505 );
506 }
507
508 #[test]
509 fn rejects_truncated_input() {
510 let entry = JournalEntry {
511 block_hash: h(0x77),
512 parent_state_root: h(0x88),
513 account_trie_diff: vec![(vec![0x00], Some(vec![0xff]))],
514 storage_trie_diff: vec![],
515 account_flat_diff: vec![],
516 storage_flat_diff: vec![],
517 };
518 let bytes = entry.encode();
519 let err = JournalEntry::decode(&bytes[..bytes.len() - 1]).unwrap_err();
520 assert!(matches!(err, JournalDecodeError::Truncated { .. }));
521 }
522
523 #[test]
524 fn rejects_invalid_presence_byte() {
525 let mut bytes = Vec::new();
526 bytes.push(JOURNAL_VERSION);
527 bytes.extend_from_slice(&[0; 32]);
528 bytes.extend_from_slice(&[0; 32]);
529 bytes.push(1); bytes.push(1); bytes.push(0xab); bytes.push(2); let err = JournalEntry::decode(&bytes).unwrap_err();
534 assert!(matches!(
535 err,
536 JournalDecodeError::InvalidPresenceByte { found: 2, .. }
537 ));
538 }
539
540 #[test]
541 fn varint_round_trip() {
542 for &v in &[0u64, 1, 127, 128, 16_383, 16_384, u32::MAX as u64, u64::MAX] {
543 let mut buf = Vec::new();
544 encode_varint(&mut buf, v);
545 let mut cur = Cursor::new(&buf);
546 assert_eq!(cur.read_varint().unwrap(), v);
547 }
548 }
549
550 #[test]
554 fn rejects_varint_with_truncating_10th_byte() {
555 let mut buf = vec![0x80; 9];
559 buf.push(0x7e);
560 let mut cur = Cursor::new(&buf);
561 let err = cur.read_varint().unwrap_err();
562 assert!(matches!(err, JournalDecodeError::VarintOverflow { .. }));
563 }
564
565 #[test]
567 fn rejects_varint_with_11th_byte() {
568 let mut buf = vec![0x80; 10];
569 buf.push(0x00);
570 let mut cur = Cursor::new(&buf);
571 let err = cur.read_varint().unwrap_err();
572 assert!(matches!(err, JournalDecodeError::VarintOverflow { .. }));
573 }
574
575 #[test]
578 fn rejects_varint_with_continuation_at_byte_10() {
579 let mut buf = vec![0x80; 9];
580 buf.push(0x81); let mut cur = Cursor::new(&buf);
582 let err = cur.read_varint().unwrap_err();
583 assert!(matches!(err, JournalDecodeError::VarintOverflow { .. }));
584 }
585
586 #[test]
589 fn u64_max_round_trips_after_tighter_decoder() {
590 let mut buf = Vec::new();
591 encode_varint(&mut buf, u64::MAX);
592 assert_eq!(buf.len(), 10);
593 let mut cur = Cursor::new(&buf);
594 assert_eq!(cur.read_varint().unwrap(), u64::MAX);
595 }
596
597 #[test]
600 fn rejects_oom_via_malformed_count() {
601 let mut bytes = vec![JOURNAL_VERSION];
605 bytes.extend_from_slice(&[0; 32]);
606 bytes.extend_from_slice(&[0; 32]);
607 encode_varint(&mut bytes, u64::MAX);
608 let err = JournalEntry::decode(&bytes).unwrap_err();
609 assert!(
610 matches!(err, JournalDecodeError::LengthExceedsRemaining { .. }),
611 "expected LengthExceedsRemaining, got {err:?}"
612 );
613 }
614
615 #[test]
617 fn rejects_oom_via_malformed_path_len() {
618 let mut bytes = vec![JOURNAL_VERSION];
619 bytes.extend_from_slice(&[0; 32]);
620 bytes.extend_from_slice(&[0; 32]);
621 encode_varint(&mut bytes, 1); encode_varint(&mut bytes, u64::MAX); let err = JournalEntry::decode(&bytes).unwrap_err();
624 assert!(
625 matches!(err, JournalDecodeError::LengthExceedsRemaining { .. }),
626 "expected LengthExceedsRemaining, got {err:?}"
627 );
628 }
629
630 #[test]
632 fn rejects_oom_via_malformed_value_len() {
633 let mut bytes = vec![JOURNAL_VERSION];
634 bytes.extend_from_slice(&[0; 32]);
635 bytes.extend_from_slice(&[0; 32]);
636 encode_varint(&mut bytes, 1); encode_varint(&mut bytes, 1); bytes.push(0xaa); bytes.push(1); encode_varint(&mut bytes, u64::MAX); let err = JournalEntry::decode(&bytes).unwrap_err();
642 assert!(
643 matches!(err, JournalDecodeError::LengthExceedsRemaining { .. }),
644 "expected LengthExceedsRemaining, got {err:?}"
645 );
646 }
647
648 #[test]
651 fn rejects_trailing_bytes() {
652 let entry = JournalEntry {
653 block_hash: h(0xaa),
654 parent_state_root: h(0xbb),
655 account_trie_diff: vec![],
656 storage_trie_diff: vec![],
657 account_flat_diff: vec![],
658 storage_flat_diff: vec![],
659 };
660 let mut bytes = entry.encode();
661 bytes.push(0xff); let err = JournalEntry::decode(&bytes).unwrap_err();
663 match err {
664 JournalDecodeError::TrailingBytes { trailing, .. } => assert_eq!(trailing, 1),
665 other => panic!("expected TrailingBytes, got {other:?}"),
666 }
667 }
668
669 #[test]
675 fn proptest_encode_decode_round_trip() {
676 use proptest::collection::vec;
677 use proptest::prelude::*;
678 let entry_strategy = (
679 any::<[u8; 32]>(),
680 any::<[u8; 32]>(),
681 vec(flat_diff_entry(), 0..16),
682 vec(flat_diff_entry(), 0..16),
683 vec(flat_diff_entry(), 0..16),
684 vec(flat_diff_entry(), 0..16),
685 )
686 .prop_map(|(bh, psr, a, b, c, d)| JournalEntry {
687 block_hash: H256::from(bh),
688 parent_state_root: H256::from(psr),
689 account_trie_diff: a,
690 storage_trie_diff: b,
691 account_flat_diff: c,
692 storage_flat_diff: d,
693 });
694 proptest!(|(entry in entry_strategy)| {
695 let bytes = entry.encode();
696 let decoded = JournalEntry::decode(&bytes).expect("encoded entry must decode");
697 prop_assert_eq!(decoded, entry);
698 });
699 }
700
701 #[test]
706 fn proptest_decoder_never_panics_on_arbitrary_bytes() {
707 use proptest::collection::vec;
708 use proptest::prelude::*;
709 proptest!(|(bytes in vec(any::<u8>(), 0..1024))| {
710 let _ = JournalEntry::decode(&bytes);
711 });
712 }
713
714 #[test]
719 fn proptest_single_byte_mutation_never_silently_accepted() {
720 use proptest::prelude::*;
721 let entry = JournalEntry {
722 block_hash: h(0x42),
723 parent_state_root: h(0x43),
724 account_trie_diff: vec![(vec![0x01, 0x02], Some(vec![0xaa, 0xbb]))],
725 storage_trie_diff: vec![(vec![0x0a; 67], None)],
726 account_flat_diff: vec![(vec![0xcc; 65], Some(vec![0xdd]))],
727 storage_flat_diff: vec![],
728 };
729 let baseline = entry.encode();
730 proptest!(|(idx in 0..baseline.len(), bit in 0u8..8)| {
731 let mut mutated = baseline.clone();
732 mutated[idx] ^= 1 << bit;
733 match JournalEntry::decode(&mutated) {
734 Err(_) => {}
735 Ok(decoded) => prop_assert_ne!(decoded, entry.clone()),
736 }
737 });
738 }
739
740 fn flat_diff_entry() -> impl proptest::strategy::Strategy<Value = ReverseDiffEntry> {
741 use proptest::collection::vec;
742 use proptest::prelude::*;
743 (
744 vec(any::<u8>(), 0..200),
745 proptest::option::of(vec(any::<u8>(), 0..200)),
746 )
747 }
748
749 #[test]
752 fn diff_byte_estimate_handles_large_lengths() {
753 let diff: Vec<ReverseDiffEntry> = vec![
754 (vec![0xaa; 200], Some(vec![0xbb; 300])), (vec![0xcc; 50], Some(vec![0xdd; 50])), ];
757 let mut buf = Vec::new();
761 encode_flat_diff(&mut buf, &diff);
762 assert_eq!(
763 diff_byte_estimate(&diff),
764 buf.len(),
765 "estimate must equal actual encoded length so encode() avoids realloc"
766 );
767 }
768 #[test]
769 fn decode_block_hash_reads_the_hash_without_the_diffs() {
770 let entry = JournalEntry {
771 block_hash: H256::repeat_byte(0xab),
772 parent_state_root: H256::repeat_byte(0xcd),
773 account_trie_diff: vec![(vec![1, 2, 3], Some(vec![4]))],
774 storage_trie_diff: vec![],
775 account_flat_diff: vec![(vec![9], None)],
776 storage_flat_diff: vec![],
777 };
778 let bytes = entry.encode();
779 assert_eq!(
780 JournalEntry::decode_block_hash(&bytes).unwrap(),
781 H256::repeat_byte(0xab)
782 );
783 assert_eq!(
785 JournalEntry::decode_block_hash(&bytes[..40]).unwrap(),
786 H256::repeat_byte(0xab)
787 );
788 let mut foreign = bytes.clone();
790 foreign[0] = JOURNAL_VERSION + 1;
791 assert!(matches!(
792 JournalEntry::decode_block_hash(&foreign),
793 Err(JournalDecodeError::VersionMismatch { .. })
794 ));
795 assert!(matches!(
796 JournalEntry::decode_block_hash(&bytes[..20]),
797 Err(JournalDecodeError::Truncated { .. })
798 ));
799 }
800}