Skip to content

Commit abcf4cc

Browse files
committed
test: fat/light checkpoint
1 parent c803b4f commit abcf4cc

File tree

3 files changed

+220
-31
lines changed

3 files changed

+220
-31
lines changed

src/payload/checkpoint.rs

Lines changed: 199 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
//! start of the chain up to this checkpoint:
5959
//!
6060
//! ```text
61-
//! accumulated = squash([state1, state2, state3])
61+
//! accumulated = squash([base, state1, state2, state3])
6262
//! ```
6363
//!
6464
//! This creates a baseline-accumulated snapshot.
@@ -71,10 +71,10 @@
7171
//! base
7272
//! ├─ C1: state1
7373
//! ├─ C2: state2
74-
//! ├─ C3: state3 FAT, accumulated = squash([base,1,2,3])
74+
//! ├─ C3: state3 -> FAT, accumulated = squash([base,1,2,3])
7575
//! ├─ C4: state4
7676
//! ├─ C5: state5
77-
//! ├─ C6: state6 FAT
77+
//! ├─ C6: state6 -> FAT
7878
//! ├─ C7: state7
7979
//! ├─ C8: state8
8080
//! ```
@@ -87,10 +87,10 @@
8787
//! base
8888
//! ├─ C1: state1
8989
//! ├─ C2: state2
90-
//! ├─ C3: state3 FAT, accumulated = squash([base,1,2,3])
90+
//! ├─ C3: state3 -> FAT, accumulated = squash([base,1,2,3])
9191
//! ├─ C4: state4
9292
//! ├─ C5: state5
93-
//! ├─ C6: state6 FAT, accumulated = squash([4,5,6])
93+
//! ├─ C6: state6 -> FAT, accumulated = squash([4,5,6])
9494
//! ├─ C7: state7
9595
//! ├─ C8: state8
9696
//! ```
@@ -103,7 +103,7 @@
103103
//! 2. **Previous light checkpoints** (C8 and C7)
104104
//! 3. **Hit a fat checkpoint (C6)**
105105
//! - check only its *accumulated* state (C4–C6)
106-
//! 4. **Jump to `C6.fat_ancestor` C3**
106+
//! 4. **Jump to `C6.fat_ancestor` -> C3**
107107
//! - Check only accumulated state (C1–C3)
108108
//! 5. **Fall back to base state**
109109
//!
@@ -865,27 +865,17 @@ mod tests {
865865
crate::{
866866
payload::checkpoint::{Checkpoint, IntoExecutable, Mutation},
867867
prelude::*,
868-
reth::primitives::Recovered,
869-
test_utils::{BlockContextMocked, test_bundle, test_tx, test_txs},
868+
test_utils::{
869+
BlockContextMocked,
870+
apply_multiple,
871+
test_bundle,
872+
test_tx,
873+
test_txs,
874+
},
870875
},
871876
std::time::Instant,
872877
};
873878

874-
/// Helper test function to apply multiple transactions on a checkpoint
875-
fn apply_multiple<P: PlatformWithRpcTypes>(
876-
root: Checkpoint<P>,
877-
txs: &[Recovered<types::Transaction<P>>],
878-
) -> Vec<Checkpoint<P>> {
879-
let mut cur = root;
880-
txs
881-
.iter()
882-
.map(|tx| {
883-
cur = cur.apply(tx.clone()).unwrap();
884-
cur.clone()
885-
})
886-
.collect()
887-
}
888-
889879
mod internal {
890880
use super::*;
891881
#[test]
@@ -1056,4 +1046,190 @@ mod tests {
10561046
assert_eq!(built_payload.id(), payload.id());
10571047
assert_eq!(built_payload.block(), payload.block());
10581048
}
1049+
1050+
mod fat_checkpoints {
1051+
use {
1052+
super::*,
1053+
crate::{
1054+
alloy::primitives::{Address, B256},
1055+
reth::revm::{DatabaseRef, primitives::U256},
1056+
},
1057+
std::sync::Arc,
1058+
};
1059+
1060+
/// return depths of checkpoints produced by `iter_from_fat_ancestor`.
1061+
fn depths<P: Platform>(cp: &Checkpoint<P>) -> Vec<usize> {
1062+
cp.iter_from_fat_ancestor().map(|c| c.depth()).collect()
1063+
}
1064+
1065+
#[test]
1066+
fn fat_on_first_mutation_accumulates_from_base() {
1067+
let block = BlockContext::<Ethereum>::mocked();
1068+
let base = block.start(); // depth 0, barrier
1069+
1070+
let txs = test_txs::<Ethereum>(0, 0, 3);
1071+
let checkpoints = apply_multiple(base, &txs);
1072+
let c1 = checkpoints[0].clone();
1073+
let c2 = checkpoints[1].clone();
1074+
let c3 = checkpoints[2].clone();
1075+
1076+
// Sanity: all light, no accumulated state, no fat ancestors.
1077+
assert!(c1.inner.accumulated_state.is_none());
1078+
assert!(c2.inner.accumulated_state.is_none());
1079+
assert!(c3.inner.accumulated_state.is_none());
1080+
assert!(c1.inner.fat_ancestor.is_none());
1081+
assert!(c2.inner.fat_ancestor.is_none());
1082+
assert!(c3.inner.fat_ancestor.is_none());
1083+
1084+
// Make C3 fat: with no existing fat ancestor, we should accumulate
1085+
// the whole window from base to C3.
1086+
let c3_fat = c3.clone().fat();
1087+
assert!(c3_fat.inner.accumulated_state.is_some());
1088+
// the first fat checkpoint has no fat_ancestor
1089+
assert!(c3_fat.inner.fat_ancestor.is_none());
1090+
1091+
// iter_from_fat_ancestor should cover all diffs from base to C3:
1092+
// depths [0, 1, 2, 3] (Base, C1, C2, C3).
1093+
let window_depths = depths(&c3);
1094+
assert_eq!(window_depths, vec![0, 1, 2, 3]);
1095+
1096+
// Public API should still work the same way.
1097+
assert_eq!(c3_fat.depth(), c3.depth());
1098+
assert_eq!(c3_fat.prev(), c3.prev());
1099+
assert_eq!(c3_fat.as_transaction(), c3.as_transaction());
1100+
assert!(c3_fat.state().is_some());
1101+
}
1102+
1103+
#[test]
1104+
fn fat_with_existing_fat_ancestor_accumulates_only_last_window() {
1105+
let block = BlockContext::<Ethereum>::mocked();
1106+
let base = block.start();
1107+
1108+
// Build 6 checkpoints.
1109+
let txs = test_txs::<Ethereum>(0, 0, 6);
1110+
let checkpoints = apply_multiple(base, &txs[0..3]);
1111+
1112+
let c3 = checkpoints[2].clone(); // depth 3
1113+
// First fat checkpoint at C3: accumulates [C1, C2, C3].
1114+
let c3_fat = c3.clone().fat();
1115+
1116+
let checkpoints = apply_multiple(c3_fat.clone(), &txs[3..6]);
1117+
let c4 = checkpoints[3 - 3].clone(); // depth 4
1118+
let c5 = checkpoints[4 - 3].clone(); // depth 5
1119+
let c6 = checkpoints[5 - 3].clone(); // depth 6
1120+
1121+
assert!(c3_fat.inner.accumulated_state.is_some());
1122+
assert!(c3_fat.inner.fat_ancestor.is_none());
1123+
1124+
// Sanity: the successors C4/C5/C6 should have C3 as fat_ancestor.
1125+
assert!(Arc::ptr_eq(
1126+
c4.inner
1127+
.fat_ancestor
1128+
.as_ref()
1129+
.expect("expected fat ancestor on C4"),
1130+
&c3_fat.inner
1131+
));
1132+
assert!(Arc::ptr_eq(
1133+
c5.inner
1134+
.fat_ancestor
1135+
.as_ref()
1136+
.expect("expected fat ancestor on C5"),
1137+
&c3_fat.inner
1138+
));
1139+
assert!(Arc::ptr_eq(
1140+
c6.inner
1141+
.fat_ancestor
1142+
.as_ref()
1143+
.expect("expected fat ancestor on C6"),
1144+
&c3_fat.inner
1145+
));
1146+
1147+
// Make C6 fat: now we should accumulate only the window [C4, C5, C6].
1148+
let c6_fat = c6.clone().fat();
1149+
assert!(c6_fat.inner.accumulated_state.is_some());
1150+
1151+
// The fat ancestor of C6 must be C3.
1152+
assert!(Arc::ptr_eq(
1153+
c6_fat
1154+
.inner
1155+
.fat_ancestor
1156+
.as_ref()
1157+
.expect("fat ancestor on C6"),
1158+
&c3_fat.inner
1159+
));
1160+
1161+
// iter_from_fat_ancestor on C6 should start after C3, i.e. depths [4, 5,
1162+
// 6].
1163+
let window_depths = depths(&c6);
1164+
assert_eq!(window_depths, vec![4, 5, 6]);
1165+
}
1166+
1167+
#[test]
1168+
fn iter_from_fat_ancestor_for_light_descendants_uses_latest_fat() {
1169+
let block = BlockContext::<Ethereum>::mocked();
1170+
let base = block.start();
1171+
1172+
let txs = test_txs::<Ethereum>(0, 0, 10);
1173+
let checkpoints = apply_multiple(base, &txs[0..3]);
1174+
let c3 = checkpoints[2].clone();
1175+
let c3_fat = c3.clone().fat();
1176+
1177+
let checkpoints = apply_multiple(c3_fat.clone(), &txs[3..6]);
1178+
let c6 = checkpoints[5 - 3].clone();
1179+
let c6_fat = c6.clone().fat();
1180+
1181+
let checkpoints = apply_multiple(c6_fat.clone(), &txs[6..10]);
1182+
let c8 = checkpoints[7 - 3 - 3].clone();
1183+
1184+
// After C6 becomes fat, later checkpoints should see C6 as their
1185+
// latest fat ancestor, so the window for C8 is (C6, C8] -> depths [7, 8].
1186+
let window_depths = depths(&c8);
1187+
assert_eq!(window_depths, vec![7, 8]);
1188+
1189+
// And for C6 itself, the window is from its fat ancestor C3:
1190+
// depths [4, 5, 6].
1191+
let window_depths_c6 = depths(&c6);
1192+
assert_eq!(window_depths_c6, vec![4, 5, 6]);
1193+
1194+
// For C3 (first fat), the window covers from base: [0, 1, 2, 3].
1195+
let window_depths_c3 = depths(&c3);
1196+
assert_eq!(window_depths_c3, vec![0, 1, 2, 3]);
1197+
}
1198+
1199+
#[test]
1200+
fn database_ref_traversal_resolves_state_through_fat_windows() {
1201+
let block = BlockContext::<Ethereum>::mocked();
1202+
let base = block.start();
1203+
1204+
// Build a moderately deep chain.
1205+
let txs = test_txs::<Ethereum>(0, 0, 8);
1206+
let checkpoints = apply_multiple(base, &txs);
1207+
1208+
// Make two fat checkpoints as skip-list anchors.
1209+
let _c3_fat = checkpoints[2].clone().fat();
1210+
let _c6_fat = checkpoints[5].clone().fat();
1211+
let latest = checkpoints[7].clone(); // C8
1212+
1213+
// We just want to ensure we get the same via Checkpoint and via
1214+
// CheckpointInner.
1215+
let addr = Address::random();
1216+
let key = U256::from(0);
1217+
1218+
// basic_ref should never error
1219+
let from_cp = latest.basic_ref(addr).unwrap();
1220+
let from_inner = latest.inner.basic_ref(addr).unwrap();
1221+
assert_eq!(from_cp.is_some(), from_inner.is_some());
1222+
1223+
// storage_ref should never error as well.
1224+
let storage_from_cp = latest.storage_ref(addr, key).unwrap();
1225+
let storage_from_inner = latest.inner.storage_ref(addr, key).unwrap();
1226+
assert_eq!(storage_from_cp, storage_from_inner);
1227+
1228+
// code_by_hash_ref should be consistent.
1229+
let any_hash = B256::random();
1230+
let code_from_cp = latest.code_by_hash_ref(any_hash).unwrap();
1231+
let code_from_inner = latest.inner.code_by_hash_ref(any_hash).unwrap();
1232+
assert_eq!(code_from_cp, code_from_inner);
1233+
}
1234+
}
10591235
}

src/test_utils/mod.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,7 @@ pub use {
3939
OneStep,
4040
StringEvent,
4141
},
42-
transactions::{
43-
invalid_tx,
44-
reverting_tx,
45-
test_bundle,
46-
test_tx,
47-
test_txs,
48-
transfer_tx,
49-
},
42+
transactions::*,
5043
};
5144

5245
#[cfg(feature = "optimism")]

src/test_utils/transactions.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,23 @@ pub fn invalid_tx<P: PlatformWithRpcTypes>(
130130
let signed_tx: types::Transaction<P> = signed_tx.into();
131131
signed_tx.with_signer(signer.address())
132132
}
133+
134+
/// Helper test function to apply multiple transactions on a checkpoint
135+
///
136+
/// # Panics
137+
/// - if `apply` fails
138+
pub fn apply_multiple<P: PlatformWithRpcTypes>(
139+
root: Checkpoint<P>,
140+
txs: &[Recovered<types::Transaction<P>>],
141+
) -> Vec<Checkpoint<P>> {
142+
let mut cur = root;
143+
txs
144+
.iter()
145+
.map(|tx| {
146+
cur = cur
147+
.apply(tx.clone())
148+
.expect("test transaction should not fail");
149+
cur.clone()
150+
})
151+
.collect()
152+
}

0 commit comments

Comments
 (0)