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