|
58 | 58 | //! start of the chain up to this checkpoint: |
59 | 59 | //! |
60 | 60 | //! ```text |
61 | | -//! accumulated = squash([state1, state2, state3]) |
| 61 | +//! accumulated = squash([base, state1, state2, state3]) |
62 | 62 | //! ``` |
63 | 63 | //! |
64 | 64 | //! This creates a baseline-accumulated snapshot. |
|
71 | 71 | //! base |
72 | 72 | //! ├─ C1: state1 |
73 | 73 | //! ├─ C2: state2 |
74 | | -//! ├─ C3: state3 → FAT, accumulated = squash([base,1,2,3]) |
| 74 | +//! ├─ C3: state3 -> FAT, accumulated = squash([base,1,2,3]) |
75 | 75 | //! ├─ C4: state4 |
76 | 76 | //! ├─ C5: state5 |
77 | | -//! ├─ C6: state6 → FAT |
| 77 | +//! ├─ C6: state6 -> FAT |
78 | 78 | //! ├─ C7: state7 |
79 | 79 | //! ├─ C8: state8 |
80 | 80 | //! ``` |
|
87 | 87 | //! base |
88 | 88 | //! ├─ C1: state1 |
89 | 89 | //! ├─ C2: state2 |
90 | | -//! ├─ C3: state3 → FAT, accumulated = squash([base,1,2,3]) |
| 90 | +//! ├─ C3: state3 -> FAT, accumulated = squash([base,1,2,3]) |
91 | 91 | //! ├─ C4: state4 |
92 | 92 | //! ├─ C5: state5 |
93 | | -//! ├─ C6: state6 → FAT, accumulated = squash([4,5,6]) |
| 93 | +//! ├─ C6: state6 -> FAT, accumulated = squash([4,5,6]) |
94 | 94 | //! ├─ C7: state7 |
95 | 95 | //! ├─ C8: state8 |
96 | 96 | //! ``` |
|
103 | 103 | //! 2. **Previous light checkpoints** (C8 and C7) |
104 | 104 | //! 3. **Hit a fat checkpoint (C6)** |
105 | 105 | //! - check only its *accumulated* state (C4–C6) |
106 | | -//! 4. **Jump to `C6.fat_ancestor` → C3** |
| 106 | +//! 4. **Jump to `C6.fat_ancestor` -> C3** |
107 | 107 | //! - Check only accumulated state (C1–C3) |
108 | 108 | //! 5. **Fall back to base state** |
109 | 109 | //! |
@@ -865,27 +865,17 @@ mod tests { |
865 | 865 | crate::{ |
866 | 866 | payload::checkpoint::{Checkpoint, IntoExecutable, Mutation}, |
867 | 867 | 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 | + }, |
870 | 875 | }, |
871 | 876 | std::time::Instant, |
872 | 877 | }; |
873 | 878 |
|
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 | | - |
889 | 879 | mod internal { |
890 | 880 | use super::*; |
891 | 881 | #[test] |
@@ -1056,4 +1046,191 @@ mod tests { |
1056 | 1046 | assert_eq!(built_payload.id(), payload.id()); |
1057 | 1047 | assert_eq!(built_payload.block(), payload.block()); |
1058 | 1048 | } |
| 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.into()).unwrap(); |
| 1225 | + let storage_from_inner = |
| 1226 | + latest.inner.storage_ref(addr, key.into()).unwrap(); |
| 1227 | + assert_eq!(storage_from_cp, storage_from_inner); |
| 1228 | + |
| 1229 | + // code_by_hash_ref should be consistent. |
| 1230 | + let any_hash = B256::random(); |
| 1231 | + let code_from_cp = latest.code_by_hash_ref(any_hash).unwrap(); |
| 1232 | + let code_from_inner = latest.inner.code_by_hash_ref(any_hash).unwrap(); |
| 1233 | + assert_eq!(code_from_cp, code_from_inner); |
| 1234 | + } |
| 1235 | + } |
1059 | 1236 | } |
0 commit comments