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(bytes: &[u8]) -> Result<Self, JournalDecodeError> {
159 let mut cur = Cursor::new(bytes);
160
161 let version = cur.read_byte()?;
162 if version != JOURNAL_VERSION {
163 return Err(JournalDecodeError::VersionMismatch {
164 expected: JOURNAL_VERSION,
165 found: version,
166 });
167 }
168
169 let block_hash = cur.read_h256()?;
170 let parent_state_root = cur.read_h256()?;
171
172 let account_trie_diff = decode_flat_diff(&mut cur)?;
173 let storage_trie_diff = decode_flat_diff(&mut cur)?;
174 let account_flat_diff = decode_flat_diff(&mut cur)?;
175 let storage_flat_diff = decode_flat_diff(&mut cur)?;
176
177 if cur.offset != bytes.len() {
180 return Err(JournalDecodeError::TrailingBytes {
181 offset: cur.offset,
182 trailing: bytes.len() - cur.offset,
183 });
184 }
185
186 Ok(Self {
187 block_hash,
188 parent_state_root,
189 account_trie_diff,
190 storage_trie_diff,
191 account_flat_diff,
192 storage_flat_diff,
193 })
194 }
195}
196
197fn encode_varint(out: &mut Vec<u8>, mut value: u64) {
198 while value >= 0x80 {
199 out.push((value as u8 & 0x7f) | 0x80);
200 value >>= 7;
201 }
202 out.push(value as u8);
203}
204
205fn encode_flat_diff(out: &mut Vec<u8>, diff: &[ReverseDiffEntry]) {
206 encode_varint(out, diff.len() as u64);
207 for (path, value) in diff {
208 encode_varint(out, path.len() as u64);
209 out.extend_from_slice(path);
210 match value {
211 None => out.push(0),
212 Some(v) => {
213 out.push(1);
214 encode_varint(out, v.len() as u64);
215 out.extend_from_slice(v);
216 }
217 }
218 }
219}
220
221fn varint_len(value: u64) -> usize {
224 let bits = 64 - value.leading_zeros() as usize;
225 bits.div_ceil(7).max(1)
226}
227
228fn diff_byte_estimate(diff: &[ReverseDiffEntry]) -> usize {
229 diff.iter()
232 .map(|(p, v)| {
233 varint_len(p.len() as u64)
234 + p.len()
235 + 1
236 + v.as_ref()
237 .map_or(0, |v| varint_len(v.len() as u64) + v.len())
238 })
239 .sum::<usize>()
240 + varint_len(diff.len() as u64)
241}
242
243struct Cursor<'a> {
244 bytes: &'a [u8],
245 offset: usize,
246}
247
248impl<'a> Cursor<'a> {
249 fn new(bytes: &'a [u8]) -> Self {
250 Self { bytes, offset: 0 }
251 }
252
253 fn read_byte(&mut self) -> Result<u8, JournalDecodeError> {
254 if self.offset >= self.bytes.len() {
255 return Err(JournalDecodeError::Truncated {
256 offset: self.offset,
257 expected: 1,
258 });
259 }
260 let b = self.bytes[self.offset];
261 self.offset += 1;
262 Ok(b)
263 }
264
265 fn read_slice(&mut self, n: usize) -> Result<&'a [u8], JournalDecodeError> {
266 let remaining = self.bytes.len().saturating_sub(self.offset);
269 if remaining < n {
270 return Err(JournalDecodeError::Truncated {
271 offset: self.offset,
272 expected: n,
273 });
274 }
275 let s = &self.bytes[self.offset..self.offset + n];
276 self.offset += n;
277 Ok(s)
278 }
279
280 fn remaining(&self) -> usize {
282 self.bytes.len().saturating_sub(self.offset)
283 }
284
285 fn read_h256(&mut self) -> Result<H256, JournalDecodeError> {
286 let s = self.read_slice(32)?;
287 Ok(H256::from_slice(s))
288 }
289
290 fn read_varint(&mut self) -> Result<u64, JournalDecodeError> {
291 let mut result: u64 = 0;
292 let mut shift: u32 = 0;
293 loop {
294 let b = self.read_byte()?;
295 if shift >= 64 {
298 return Err(JournalDecodeError::VarintOverflow {
299 offset: self.offset - 1,
300 });
301 }
302 if shift == 63 && (b & 0x7e != 0 || b & 0x80 != 0) {
306 return Err(JournalDecodeError::VarintOverflow {
307 offset: self.offset - 1,
308 });
309 }
310 result |= ((b & 0x7f) as u64) << shift;
311 if b & 0x80 == 0 {
312 return Ok(result);
313 }
314 shift += 7;
315 }
316 }
317}
318
319const MIN_ENTRY_BYTES: usize = 2;
323
324fn decode_flat_diff(cur: &mut Cursor<'_>) -> Result<FlatDiff, JournalDecodeError> {
325 let count_offset = cur.offset;
326 let count_u64 = cur.read_varint()?;
327 let remaining = cur.remaining();
328 let max_possible = remaining / MIN_ENTRY_BYTES;
332 if count_u64 as usize > max_possible {
333 return Err(JournalDecodeError::LengthExceedsRemaining {
334 offset: count_offset,
335 claimed: count_u64,
336 remaining,
337 });
338 }
339 let count = count_u64 as usize;
340 let mut out = Vec::with_capacity(count);
341 for _ in 0..count {
342 let path_len_offset = cur.offset;
343 let path_len_u64 = cur.read_varint()?;
344 let remaining = cur.remaining();
346 if path_len_u64 > remaining as u64 {
347 return Err(JournalDecodeError::LengthExceedsRemaining {
348 offset: path_len_offset,
349 claimed: path_len_u64,
350 remaining,
351 });
352 }
353 let path_len = path_len_u64 as usize;
354 let path = cur.read_slice(path_len)?.to_vec();
355 let presence_offset = cur.offset;
356 let presence = cur.read_byte()?;
357 let value = match presence {
358 0 => None,
359 1 => {
360 let value_len_offset = cur.offset;
361 let value_len_u64 = cur.read_varint()?;
362 let remaining = cur.remaining();
363 if value_len_u64 > remaining as u64 {
364 return Err(JournalDecodeError::LengthExceedsRemaining {
365 offset: value_len_offset,
366 claimed: value_len_u64,
367 remaining,
368 });
369 }
370 let value_len = value_len_u64 as usize;
371 Some(cur.read_slice(value_len)?.to_vec())
372 }
373 other => {
374 return Err(JournalDecodeError::InvalidPresenceByte {
375 offset: presence_offset,
376 found: other,
377 });
378 }
379 };
380 out.push((path, value));
381 }
382 Ok(out)
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 fn h(b: u8) -> H256 {
390 H256::repeat_byte(b)
391 }
392
393 fn round_trip(entry: &JournalEntry) {
394 let bytes = entry.encode();
395 let decoded = JournalEntry::decode(&bytes).unwrap();
396 assert_eq!(&decoded, entry);
397 }
398
399 #[test]
400 fn empty_entry_round_trips() {
401 let entry = JournalEntry {
402 block_hash: h(0xaa),
403 parent_state_root: h(0xbb),
404 account_trie_diff: vec![],
405 storage_trie_diff: vec![],
406 account_flat_diff: vec![],
407 storage_flat_diff: vec![],
408 };
409 round_trip(&entry);
410 assert_eq!(entry.encode().len(), 69);
412 }
413
414 #[test]
415 fn typical_entry_round_trips() {
416 let entry = JournalEntry {
417 block_hash: h(0x11),
418 parent_state_root: h(0x22),
419 account_trie_diff: vec![
420 (vec![0x00, 0x01], Some(vec![0xde, 0xad, 0xbe, 0xef])),
421 (vec![0x02], None),
422 ],
423 storage_trie_diff: vec![(vec![0x0a; 67], Some(vec![0xff])), (vec![0x0b; 68], None)],
424 account_flat_diff: vec![(vec![0xaa; 65], Some(vec![0x01, 0x02, 0x03]))],
425 storage_flat_diff: vec![(vec![0xbb; 131], None)],
426 };
427 round_trip(&entry);
428 }
429
430 #[test]
431 fn entry_with_only_absences_round_trips() {
432 let entry = JournalEntry {
433 block_hash: h(0x55),
434 parent_state_root: h(0x66),
435 account_trie_diff: vec![(vec![0x00], None), (vec![0x01], None), (vec![0x02], None)],
436 storage_trie_diff: vec![],
437 account_flat_diff: vec![(vec![0xaa; 32], None)],
438 storage_flat_diff: vec![],
439 };
440 round_trip(&entry);
441 }
442
443 #[test]
444 fn large_entry_round_trips() {
445 let mut account_trie_diff = Vec::with_capacity(10_000);
446 for i in 0u32..10_000 {
447 let path = i.to_be_bytes().to_vec();
448 let value = if i % 7 == 0 {
449 None
450 } else {
451 Some(vec![(i & 0xff) as u8; (i % 200) as usize])
452 };
453 account_trie_diff.push((path, value));
454 }
455 let entry = JournalEntry {
456 block_hash: h(0xee),
457 parent_state_root: h(0xff),
458 account_trie_diff,
459 storage_trie_diff: vec![],
460 account_flat_diff: vec![],
461 storage_flat_diff: vec![],
462 };
463 round_trip(&entry);
464 }
465
466 #[test]
467 fn rejects_unknown_version() {
468 let mut bytes = vec![0xff];
469 bytes.extend_from_slice(&[0; 32]);
470 bytes.extend_from_slice(&[0; 32]);
471 bytes.extend_from_slice(&[0, 0, 0, 0]);
472 let err = JournalEntry::decode(&bytes).unwrap_err();
473 assert_eq!(
474 err,
475 JournalDecodeError::VersionMismatch {
476 expected: JOURNAL_VERSION,
477 found: 0xff,
478 }
479 );
480 }
481
482 #[test]
483 fn rejects_truncated_input() {
484 let entry = JournalEntry {
485 block_hash: h(0x77),
486 parent_state_root: h(0x88),
487 account_trie_diff: vec![(vec![0x00], Some(vec![0xff]))],
488 storage_trie_diff: vec![],
489 account_flat_diff: vec![],
490 storage_flat_diff: vec![],
491 };
492 let bytes = entry.encode();
493 let err = JournalEntry::decode(&bytes[..bytes.len() - 1]).unwrap_err();
494 assert!(matches!(err, JournalDecodeError::Truncated { .. }));
495 }
496
497 #[test]
498 fn rejects_invalid_presence_byte() {
499 let mut bytes = Vec::new();
500 bytes.push(JOURNAL_VERSION);
501 bytes.extend_from_slice(&[0; 32]);
502 bytes.extend_from_slice(&[0; 32]);
503 bytes.push(1); bytes.push(1); bytes.push(0xab); bytes.push(2); let err = JournalEntry::decode(&bytes).unwrap_err();
508 assert!(matches!(
509 err,
510 JournalDecodeError::InvalidPresenceByte { found: 2, .. }
511 ));
512 }
513
514 #[test]
515 fn varint_round_trip() {
516 for &v in &[0u64, 1, 127, 128, 16_383, 16_384, u32::MAX as u64, u64::MAX] {
517 let mut buf = Vec::new();
518 encode_varint(&mut buf, v);
519 let mut cur = Cursor::new(&buf);
520 assert_eq!(cur.read_varint().unwrap(), v);
521 }
522 }
523
524 #[test]
528 fn rejects_varint_with_truncating_10th_byte() {
529 let mut buf = vec![0x80; 9];
533 buf.push(0x7e);
534 let mut cur = Cursor::new(&buf);
535 let err = cur.read_varint().unwrap_err();
536 assert!(matches!(err, JournalDecodeError::VarintOverflow { .. }));
537 }
538
539 #[test]
541 fn rejects_varint_with_11th_byte() {
542 let mut buf = vec![0x80; 10];
543 buf.push(0x00);
544 let mut cur = Cursor::new(&buf);
545 let err = cur.read_varint().unwrap_err();
546 assert!(matches!(err, JournalDecodeError::VarintOverflow { .. }));
547 }
548
549 #[test]
552 fn rejects_varint_with_continuation_at_byte_10() {
553 let mut buf = vec![0x80; 9];
554 buf.push(0x81); let mut cur = Cursor::new(&buf);
556 let err = cur.read_varint().unwrap_err();
557 assert!(matches!(err, JournalDecodeError::VarintOverflow { .. }));
558 }
559
560 #[test]
563 fn u64_max_round_trips_after_tighter_decoder() {
564 let mut buf = Vec::new();
565 encode_varint(&mut buf, u64::MAX);
566 assert_eq!(buf.len(), 10);
567 let mut cur = Cursor::new(&buf);
568 assert_eq!(cur.read_varint().unwrap(), u64::MAX);
569 }
570
571 #[test]
574 fn rejects_oom_via_malformed_count() {
575 let mut bytes = vec![JOURNAL_VERSION];
579 bytes.extend_from_slice(&[0; 32]);
580 bytes.extend_from_slice(&[0; 32]);
581 encode_varint(&mut bytes, u64::MAX);
582 let err = JournalEntry::decode(&bytes).unwrap_err();
583 assert!(
584 matches!(err, JournalDecodeError::LengthExceedsRemaining { .. }),
585 "expected LengthExceedsRemaining, got {err:?}"
586 );
587 }
588
589 #[test]
591 fn rejects_oom_via_malformed_path_len() {
592 let mut bytes = vec![JOURNAL_VERSION];
593 bytes.extend_from_slice(&[0; 32]);
594 bytes.extend_from_slice(&[0; 32]);
595 encode_varint(&mut bytes, 1); encode_varint(&mut bytes, u64::MAX); let err = JournalEntry::decode(&bytes).unwrap_err();
598 assert!(
599 matches!(err, JournalDecodeError::LengthExceedsRemaining { .. }),
600 "expected LengthExceedsRemaining, got {err:?}"
601 );
602 }
603
604 #[test]
606 fn rejects_oom_via_malformed_value_len() {
607 let mut bytes = vec![JOURNAL_VERSION];
608 bytes.extend_from_slice(&[0; 32]);
609 bytes.extend_from_slice(&[0; 32]);
610 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();
616 assert!(
617 matches!(err, JournalDecodeError::LengthExceedsRemaining { .. }),
618 "expected LengthExceedsRemaining, got {err:?}"
619 );
620 }
621
622 #[test]
625 fn rejects_trailing_bytes() {
626 let entry = JournalEntry {
627 block_hash: h(0xaa),
628 parent_state_root: h(0xbb),
629 account_trie_diff: vec![],
630 storage_trie_diff: vec![],
631 account_flat_diff: vec![],
632 storage_flat_diff: vec![],
633 };
634 let mut bytes = entry.encode();
635 bytes.push(0xff); let err = JournalEntry::decode(&bytes).unwrap_err();
637 match err {
638 JournalDecodeError::TrailingBytes { trailing, .. } => assert_eq!(trailing, 1),
639 other => panic!("expected TrailingBytes, got {other:?}"),
640 }
641 }
642
643 #[test]
649 fn proptest_encode_decode_round_trip() {
650 use proptest::collection::vec;
651 use proptest::prelude::*;
652 let entry_strategy = (
653 any::<[u8; 32]>(),
654 any::<[u8; 32]>(),
655 vec(flat_diff_entry(), 0..16),
656 vec(flat_diff_entry(), 0..16),
657 vec(flat_diff_entry(), 0..16),
658 vec(flat_diff_entry(), 0..16),
659 )
660 .prop_map(|(bh, psr, a, b, c, d)| JournalEntry {
661 block_hash: H256::from(bh),
662 parent_state_root: H256::from(psr),
663 account_trie_diff: a,
664 storage_trie_diff: b,
665 account_flat_diff: c,
666 storage_flat_diff: d,
667 });
668 proptest!(|(entry in entry_strategy)| {
669 let bytes = entry.encode();
670 let decoded = JournalEntry::decode(&bytes).expect("encoded entry must decode");
671 prop_assert_eq!(decoded, entry);
672 });
673 }
674
675 #[test]
680 fn proptest_decoder_never_panics_on_arbitrary_bytes() {
681 use proptest::collection::vec;
682 use proptest::prelude::*;
683 proptest!(|(bytes in vec(any::<u8>(), 0..1024))| {
684 let _ = JournalEntry::decode(&bytes);
685 });
686 }
687
688 #[test]
693 fn proptest_single_byte_mutation_never_silently_accepted() {
694 use proptest::prelude::*;
695 let entry = JournalEntry {
696 block_hash: h(0x42),
697 parent_state_root: h(0x43),
698 account_trie_diff: vec![(vec![0x01, 0x02], Some(vec![0xaa, 0xbb]))],
699 storage_trie_diff: vec![(vec![0x0a; 67], None)],
700 account_flat_diff: vec![(vec![0xcc; 65], Some(vec![0xdd]))],
701 storage_flat_diff: vec![],
702 };
703 let baseline = entry.encode();
704 proptest!(|(idx in 0..baseline.len(), bit in 0u8..8)| {
705 let mut mutated = baseline.clone();
706 mutated[idx] ^= 1 << bit;
707 match JournalEntry::decode(&mutated) {
708 Err(_) => {}
709 Ok(decoded) => prop_assert_ne!(decoded, entry.clone()),
710 }
711 });
712 }
713
714 fn flat_diff_entry() -> impl proptest::strategy::Strategy<Value = ReverseDiffEntry> {
715 use proptest::collection::vec;
716 use proptest::prelude::*;
717 (
718 vec(any::<u8>(), 0..200),
719 proptest::option::of(vec(any::<u8>(), 0..200)),
720 )
721 }
722
723 #[test]
726 fn diff_byte_estimate_handles_large_lengths() {
727 let diff: Vec<ReverseDiffEntry> = vec![
728 (vec![0xaa; 200], Some(vec![0xbb; 300])), (vec![0xcc; 50], Some(vec![0xdd; 50])), ];
731 let mut buf = Vec::new();
735 encode_flat_diff(&mut buf, &diff);
736 assert_eq!(
737 diff_byte_estimate(&diff),
738 buf.len(),
739 "estimate must equal actual encoded length so encode() avoids realloc"
740 );
741 }
742}