1use std::{
13 collections::HashMap,
14 fs::{self, File, OpenOptions},
15 io::{self, Read, Write},
16 path::{Component, Path, PathBuf},
17 sync::{
18 Arc, Mutex, OnceLock, Weak,
19 atomic::{AtomicU64, Ordering},
20 },
21};
22
23use alloy_eips::{BlockId, BlockNumberOrTag, RpcBlockHash};
24use alloy_primitives::{Address, B256, U256, keccak256};
25use foundry_fork_db::BlockchainDb;
26use revm::{database::Cache, primitives::hardfork::SpecId, state::AccountInfo};
27use serde::{Deserialize, Serialize};
28
29use super::{
30 BlockEnvSource, CodeSeedState, EvmCache, ImmutableDataCache, TrackedMapping, versioned,
31};
32
33const CHECKPOINT_MAGIC: &[u8; 8] = b"EFCCKPT\0";
34const CHECKPOINT_VERSION: u32 = 7;
38const CHECKPOINT_LABEL: &str = "durable reactive checkpoint";
39const CHECKPOINT_CHECKSUM_BYTES: usize = 32;
40const CHECKPOINT_HEADER_BYTES: u64 =
41 CHECKPOINT_MAGIC.len() as u64 + std::mem::size_of::<u32>() as u64;
42const MAX_TEMP_CREATE_ATTEMPTS: usize = 128;
43pub const DEFAULT_MAX_DURABLE_CHECKPOINT_BYTES: u64 = 512 * 1024 * 1024;
45static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(0);
46static CHECKPOINT_COORDINATORS: OnceLock<
47 Mutex<HashMap<PathBuf, Weak<CheckpointWriteCoordinator>>>,
48> = OnceLock::new();
49
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
57#[non_exhaustive]
58pub struct DurableCheckpointIdentity {
59 pub chain_id: u64,
61 pub subscriber_id: String,
63 pub handler_set_id: String,
65}
66
67impl DurableCheckpointIdentity {
68 pub fn new(
70 chain_id: u64,
71 subscriber_id: impl Into<String>,
72 handler_set_id: impl Into<String>,
73 ) -> Self {
74 Self {
75 chain_id,
76 subscriber_id: subscriber_id.into(),
77 handler_set_id: handler_set_id.into(),
78 }
79 }
80}
81
82#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
84#[non_exhaustive]
85pub struct DurableCheckpointBlock {
86 pub number: u64,
88 pub hash: B256,
91 pub parent_hash: Option<B256>,
93 pub timestamp: Option<u64>,
95}
96
97impl DurableCheckpointBlock {
98 pub const fn new(number: u64, hash: B256) -> Self {
100 Self {
101 number,
102 hash,
103 parent_hash: None,
104 timestamp: None,
105 }
106 }
107
108 pub const fn with_parent_hash(mut self, parent_hash: B256) -> Self {
110 self.parent_hash = Some(parent_hash);
111 self
112 }
113
114 pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
116 self.timestamp = Some(timestamp);
117 self
118 }
119}
120
121#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
123#[non_exhaustive]
124pub struct DurableCheckpointMetadata {
125 pub identity: DurableCheckpointIdentity,
127 pub block: DurableCheckpointBlock,
129 pub delivery_token: Option<Vec<u8>>,
134 pub delivery_witness: Option<B256>,
143 pub subscriber_checkpoint: Option<Vec<u8>>,
148 pub runtime_checkpoint: Option<Vec<u8>>,
154}
155
156impl DurableCheckpointMetadata {
157 pub fn new(identity: DurableCheckpointIdentity, block: DurableCheckpointBlock) -> Self {
159 Self {
160 identity,
161 block,
162 delivery_token: None,
163 delivery_witness: None,
164 subscriber_checkpoint: None,
165 runtime_checkpoint: None,
166 }
167 }
168
169 pub fn with_delivery_token(mut self, delivery_token: impl Into<Vec<u8>>) -> Self {
171 self.delivery_token = Some(delivery_token.into());
172 self
173 }
174
175 pub fn with_delivery_witness(mut self, delivery_witness: B256) -> Self {
181 self.delivery_witness = Some(delivery_witness);
182 self
183 }
184
185 pub fn with_subscriber_checkpoint(mut self, checkpoint: impl Into<Vec<u8>>) -> Self {
187 self.subscriber_checkpoint = Some(checkpoint.into());
188 self
189 }
190
191 pub fn with_runtime_checkpoint(mut self, checkpoint: impl Into<Vec<u8>>) -> Self {
193 self.runtime_checkpoint = Some(checkpoint.into());
194 self
195 }
196}
197
198#[derive(Clone, Debug)]
208pub struct DurableCheckpointStore {
209 path: PathBuf,
210 coordinator: Arc<CheckpointWriteCoordinator>,
211 max_checkpoint_bytes: u64,
212}
213
214#[derive(Debug, Default)]
215struct CheckpointWriteCoordinator {
216 latest_generation: AtomicU64,
217 writer: Mutex<()>,
218}
219
220impl PartialEq for DurableCheckpointStore {
221 fn eq(&self, other: &Self) -> bool {
222 self.path == other.path
223 }
224}
225
226impl Eq for DurableCheckpointStore {}
227
228impl DurableCheckpointStore {
229 pub fn new(path: impl Into<PathBuf>) -> Self {
231 let path = normalized_checkpoint_path(&path.into());
232 Self {
233 coordinator: checkpoint_coordinator(&path),
234 path,
235 max_checkpoint_bytes: DEFAULT_MAX_DURABLE_CHECKPOINT_BYTES,
236 }
237 }
238
239 pub fn with_max_checkpoint_bytes(mut self, max_checkpoint_bytes: u64) -> Self {
248 self.max_checkpoint_bytes = max_checkpoint_bytes;
249 self
250 }
251
252 pub fn max_checkpoint_bytes(&self) -> u64 {
254 self.max_checkpoint_bytes
255 }
256
257 pub fn path(&self) -> &Path {
259 &self.path
260 }
261
262 pub fn save(
283 &self,
284 cache: &EvmCache,
285 metadata: DurableCheckpointMetadata,
286 ) -> Result<(), DurableCheckpointError> {
287 validate_capture_identity(cache, &metadata)?;
288 let generation = self.reserve_generation()?;
292 let snapshot = DurableCheckpointSnapshot::capture(cache, metadata);
293 persist_snapshot(
294 &self.path,
295 snapshot,
296 &self.coordinator,
297 generation,
298 self.max_checkpoint_bytes,
299 )
300 }
301
302 pub async fn save_async(
318 &self,
319 cache: &EvmCache,
320 metadata: DurableCheckpointMetadata,
321 ) -> Result<(), DurableCheckpointError> {
322 validate_capture_identity(cache, &metadata)?;
323 let generation = self.reserve_generation()?;
326 let snapshot = DurableCheckpointSnapshot::capture(cache, metadata);
327 let path = self.path.clone();
328 let coordinator = Arc::clone(&self.coordinator);
329 let max_checkpoint_bytes = self.max_checkpoint_bytes;
330 tokio::task::spawn_blocking(move || {
331 persist_snapshot(
332 &path,
333 snapshot,
334 &coordinator,
335 generation,
336 max_checkpoint_bytes,
337 )
338 })
339 .await
340 .map_err(DurableCheckpointError::TaskJoin)?
341 }
342
343 fn reserve_generation(&self) -> Result<u64, DurableCheckpointError> {
344 let generation = &self.coordinator.latest_generation;
347 let mut observed = generation.load(Ordering::Acquire);
348 loop {
349 let next = observed
350 .checked_add(1)
351 .ok_or(DurableCheckpointError::GenerationExhausted)?;
352 match generation.compare_exchange_weak(
353 observed,
354 next,
355 Ordering::AcqRel,
356 Ordering::Acquire,
357 ) {
358 Ok(_) => return Ok(next),
359 Err(current) => observed = current,
360 }
361 }
362 }
363
364 pub fn load(&self) -> Result<Option<LoadedDurableCheckpoint>, DurableCheckpointError> {
377 let file = match File::open(&self.path) {
378 Ok(file) => file,
379 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
380 Err(source) => {
381 return Err(DurableCheckpointError::Read {
382 path: self.path.clone(),
383 source,
384 });
385 }
386 };
387 let reported_bytes = file
388 .metadata()
389 .map_err(|source| DurableCheckpointError::Read {
390 path: self.path.clone(),
391 source,
392 })?
393 .len();
394 if reported_bytes > self.max_checkpoint_bytes {
395 return Err(DurableCheckpointError::CheckpointTooLarge {
396 path: self.path.clone(),
397 bytes: reported_bytes,
398 max_bytes: self.max_checkpoint_bytes,
399 });
400 }
401 let mut data = Vec::new();
402 file.take(self.max_checkpoint_bytes.saturating_add(1))
403 .read_to_end(&mut data)
404 .map_err(|source| DurableCheckpointError::Read {
405 path: self.path.clone(),
406 source,
407 })?;
408 if data.len() as u64 > self.max_checkpoint_bytes {
409 return Err(DurableCheckpointError::CheckpointTooLarge {
410 path: self.path.clone(),
411 bytes: data.len() as u64,
412 max_bytes: self.max_checkpoint_bytes,
413 });
414 }
415 let Some(checksum_start) = data.len().checked_sub(CHECKPOINT_CHECKSUM_BYTES) else {
416 return Err(DurableCheckpointError::InvalidFormat {
417 path: self.path.clone(),
418 });
419 };
420 let encoded = &data[..checksum_start];
421 let expected = B256::from_slice(&data[checksum_start..]);
422 let actual = keccak256(encoded);
423 if actual != expected {
424 return Err(DurableCheckpointError::ChecksumMismatch {
425 path: self.path.clone(),
426 });
427 }
428 let snapshot = versioned::decode(
429 encoded,
430 CHECKPOINT_MAGIC,
431 CHECKPOINT_VERSION,
432 CHECKPOINT_LABEL,
433 )
434 .ok_or_else(|| DurableCheckpointError::InvalidFormat {
435 path: self.path.clone(),
436 })?;
437 Ok(Some(LoadedDurableCheckpoint { snapshot }))
438 }
439}
440
441fn checkpoint_coordinator(path: &Path) -> Arc<CheckpointWriteCoordinator> {
442 let key = normalized_checkpoint_path(path);
443 let coordinators = CHECKPOINT_COORDINATORS.get_or_init(|| Mutex::new(HashMap::new()));
444 let mut coordinators = coordinators
445 .lock()
446 .unwrap_or_else(std::sync::PoisonError::into_inner);
447 if let Some(coordinator) = coordinators.get(&key).and_then(Weak::upgrade) {
448 return coordinator;
449 }
450 coordinators.retain(|_, coordinator| coordinator.strong_count() > 0);
451 let coordinator = Arc::new(CheckpointWriteCoordinator::default());
452 coordinators.insert(key, Arc::downgrade(&coordinator));
453 coordinator
454}
455
456fn normalized_checkpoint_path(path: &Path) -> PathBuf {
457 let absolute = if path.is_absolute() {
458 path.to_path_buf()
459 } else {
460 std::env::current_dir()
461 .map(|directory| directory.join(path))
462 .unwrap_or_else(|_| path.to_path_buf())
463 };
464
465 let Some(file_name) = absolute.file_name() else {
470 return normalize_existing_path_prefix(&absolute);
471 };
472 let parent = absolute.parent().unwrap_or_else(|| Path::new("."));
473 normalize_existing_path_prefix(parent).join(file_name)
474}
475
476fn normalize_existing_path_prefix(absolute: &Path) -> PathBuf {
477 let components: Vec<_> = absolute.components().collect();
482 for split in (1..=components.len()).rev() {
483 let prefix: PathBuf = components[..split]
484 .iter()
485 .map(|component| component.as_os_str())
486 .collect();
487 let Ok(mut resolved) = prefix.canonicalize() else {
488 continue;
489 };
490 for component in &components[split..] {
491 match component {
492 Component::Prefix(prefix) => resolved.push(prefix.as_os_str()),
493 Component::RootDir => resolved.push(component.as_os_str()),
494 Component::CurDir => {}
495 Component::ParentDir => {
496 let _ = resolved.pop();
497 }
498 Component::Normal(part) => resolved.push(part),
499 }
500 }
501 return resolved;
502 }
503
504 absolute.to_path_buf()
507}
508
509fn validate_capture_identity(
510 cache: &EvmCache,
511 metadata: &DurableCheckpointMetadata,
512) -> Result<(), DurableCheckpointError> {
513 if metadata.identity.chain_id != cache.chain_id {
514 return Err(DurableCheckpointError::CacheChainMismatch {
515 cache_chain_id: cache.chain_id,
516 checkpoint_chain_id: metadata.identity.chain_id,
517 });
518 }
519 Ok(())
520}
521
522fn persist_snapshot(
523 path: &Path,
524 snapshot: DurableCheckpointSnapshot,
525 coordinator: &CheckpointWriteCoordinator,
526 generation: u64,
527 max_checkpoint_bytes: u64,
528) -> Result<(), DurableCheckpointError> {
529 let _writer = coordinator
530 .writer
531 .lock()
532 .unwrap_or_else(std::sync::PoisonError::into_inner);
533 let latest = coordinator.latest_generation.load(Ordering::Acquire);
534 if generation != latest {
535 return Err(DurableCheckpointError::WriteSuperseded { generation, latest });
536 }
537 let payload_bytes = bincode::serialized_size(&snapshot).map_err(|source| {
542 DurableCheckpointError::Encode(crate::errors::PersistenceError::serialize(
543 CHECKPOINT_LABEL,
544 source,
545 ))
546 })?;
547 let encoded_bytes = payload_bytes
548 .checked_add(CHECKPOINT_HEADER_BYTES)
549 .and_then(|bytes| bytes.checked_add(CHECKPOINT_CHECKSUM_BYTES as u64))
550 .ok_or(DurableCheckpointError::CheckpointSizeOverflow {
551 path: path.to_path_buf(),
552 })?;
553 if encoded_bytes > max_checkpoint_bytes {
554 return Err(DurableCheckpointError::CheckpointTooLarge {
555 path: path.to_path_buf(),
556 bytes: encoded_bytes,
557 max_bytes: max_checkpoint_bytes,
558 });
559 }
560 let mut data = versioned::encode(
561 CHECKPOINT_MAGIC,
562 CHECKPOINT_VERSION,
563 &snapshot,
564 CHECKPOINT_LABEL,
565 )
566 .map_err(DurableCheckpointError::Encode)?;
567 let checksum = keccak256(&data);
568 data.extend_from_slice(checksum.as_slice());
569 debug_assert_eq!(data.len() as u64, encoded_bytes);
570 atomic_replace(path, &data)
571}
572
573pub struct LoadedDurableCheckpoint {
575 snapshot: DurableCheckpointSnapshot,
576}
577
578impl LoadedDurableCheckpoint {
579 pub fn metadata(&self) -> &DurableCheckpointMetadata {
581 &self.snapshot.metadata
582 }
583
584 pub fn restore_into(
597 self,
598 cache: &mut EvmCache,
599 expected: &DurableCheckpointIdentity,
600 ) -> Result<DurableCheckpointMetadata, DurableCheckpointError> {
601 if &self.snapshot.metadata.identity != expected {
602 return Err(DurableCheckpointError::IdentityMismatch {
603 expected: expected.clone(),
604 actual: self.snapshot.metadata.identity.clone(),
605 });
606 }
607 if cache.chain_id != expected.chain_id {
608 return Err(DurableCheckpointError::CacheChainMismatch {
609 cache_chain_id: cache.chain_id,
610 checkpoint_chain_id: expected.chain_id,
611 });
612 }
613 Ok(self.snapshot.restore(cache))
614 }
615}
616
617#[derive(Serialize, Deserialize)]
618struct DurableCheckpointSnapshot {
619 metadata: DurableCheckpointMetadata,
620 state: EvmCacheStateSnapshot,
621}
622
623#[derive(Clone, Serialize, Deserialize)]
626pub(crate) struct EvmCacheStateSnapshot {
627 backend_accounts: Vec<(Address, AccountInfo)>,
628 backend_storage: Vec<(Address, Vec<(U256, U256)>)>,
629 backend_block_hashes: Vec<(U256, B256)>,
630 overlay: Cache,
631 token_decimals: HashMap<Address, u8>,
632 immutable_cache: ImmutableDataCache,
633 code_seeds: HashMap<Address, CodeSeedState>,
634 erc20_balance_slots: HashMap<Address, TrackedMapping>,
635 block: PersistedBlockId,
636 block_number: Option<u64>,
637 basefee: Option<u64>,
638 coinbase: Option<Address>,
639 prevrandao: Option<B256>,
640 block_gas_limit: Option<u64>,
641 timestamp_override: Option<u64>,
642 block_env_source: Option<BlockEnvSource>,
643 spec_id: SpecId,
644 snapshot_generation: u64,
645}
646
647#[derive(Clone, Copy, Serialize, Deserialize)]
648enum PersistedBlockId {
649 Hash {
650 hash: B256,
651 require_canonical: Option<bool>,
652 },
653 Latest,
654 Finalized,
655 Safe,
656 Earliest,
657 Pending,
658 Number(u64),
659}
660
661impl From<BlockId> for PersistedBlockId {
662 fn from(block: BlockId) -> Self {
663 match block {
664 BlockId::Hash(hash) => Self::Hash {
665 hash: hash.block_hash,
666 require_canonical: hash.require_canonical,
667 },
668 BlockId::Number(BlockNumberOrTag::Latest) => Self::Latest,
669 BlockId::Number(BlockNumberOrTag::Finalized) => Self::Finalized,
670 BlockId::Number(BlockNumberOrTag::Safe) => Self::Safe,
671 BlockId::Number(BlockNumberOrTag::Earliest) => Self::Earliest,
672 BlockId::Number(BlockNumberOrTag::Pending) => Self::Pending,
673 BlockId::Number(BlockNumberOrTag::Number(number)) => Self::Number(number),
674 }
675 }
676}
677
678impl From<PersistedBlockId> for BlockId {
679 fn from(block: PersistedBlockId) -> Self {
680 match block {
681 PersistedBlockId::Hash {
682 hash,
683 require_canonical,
684 } => BlockId::Hash(RpcBlockHash::from_hash(hash, require_canonical)),
685 PersistedBlockId::Latest => BlockId::latest(),
686 PersistedBlockId::Finalized => BlockId::finalized(),
687 PersistedBlockId::Safe => BlockId::safe(),
688 PersistedBlockId::Earliest => BlockId::earliest(),
689 PersistedBlockId::Pending => BlockId::pending(),
690 PersistedBlockId::Number(number) => BlockId::number(number),
691 }
692 }
693}
694
695impl DurableCheckpointSnapshot {
696 fn capture(cache: &EvmCache, metadata: DurableCheckpointMetadata) -> Self {
697 let mut state = EvmCacheStateSnapshot::capture(cache);
698 state.align_to_checkpoint_block(&metadata.block);
699 Self { metadata, state }
700 }
701
702 fn restore(self, cache: &mut EvmCache) -> DurableCheckpointMetadata {
703 let block_hash = self.metadata.block.hash;
704 self.state.restore(cache);
705
706 let block = alloy_eips::BlockId::from((block_hash, Some(true)));
712 cache.block = block;
713 let _ = cache.backend.set_pinned_block(block);
714
715 self.metadata
716 }
717}
718
719impl EvmCacheStateSnapshot {
720 pub(crate) fn capture(cache: &EvmCache) -> Self {
721 let (backend_accounts, backend_storage, backend_block_hashes) =
722 capture_backend_maps(&cache.blockchain_db);
723
724 Self {
725 backend_accounts,
726 backend_storage,
727 backend_block_hashes,
728 overlay: cache.db.cache.clone(),
729 token_decimals: cache.token_decimals.clone(),
730 immutable_cache: cache.immutable_cache.clone(),
731 code_seeds: cache.code_seeds.clone(),
732 erc20_balance_slots: cache.erc20_balance_slots.clone(),
733 block: cache.block.into(),
734 block_number: cache.block_number,
735 basefee: cache.basefee,
736 coinbase: cache.coinbase,
737 prevrandao: cache.prevrandao,
738 block_gas_limit: cache.block_gas_limit,
739 timestamp_override: cache.timestamp_override,
740 block_env_source: cache.block_env_source,
741 spec_id: cache.spec_id,
742 snapshot_generation: cache.snapshot_generation,
743 }
744 }
745
746 fn align_to_checkpoint_block(&mut self, block: &DurableCheckpointBlock) {
757 let preserve_full_env = matches!(
758 self.block_env_source,
759 Some(BlockEnvSource::VerifiedHash { number, hash })
760 if number == block.number
761 && hash == block.hash
762 && block
763 .timestamp
764 .zip(self.timestamp_override)
765 .is_none_or(|(expected, actual)| expected == actual)
766 );
767 self.block = PersistedBlockId::Hash {
768 hash: block.hash,
769 require_canonical: Some(true),
770 };
771 self.block_number = Some(block.number);
772 if !preserve_full_env {
773 self.timestamp_override = block.timestamp;
774 self.basefee = None;
775 self.coinbase = None;
776 self.prevrandao = None;
777 self.block_gas_limit = None;
778 self.block_env_source = None;
779 }
780 }
781
782 pub(crate) fn restore(self, cache: &mut EvmCache) {
783 {
784 let mut accounts = cache.blockchain_db.accounts().write();
785 accounts.clear();
786 accounts.extend(self.backend_accounts);
787 }
788 {
789 let mut storage = cache.blockchain_db.storage().write();
790 storage.clear();
791 storage.extend(
792 self.backend_storage
793 .into_iter()
794 .map(|(address, slots)| (address, slots.into_iter().collect())),
795 );
796 }
797 {
798 let mut hashes = cache.blockchain_db.block_hashes().write();
799 hashes.clear();
800 hashes.extend(self.backend_block_hashes);
801 }
802
803 cache.db.cache = self.overlay;
804 cache.token_decimals = self.token_decimals;
805 cache.immutable_cache = self.immutable_cache;
806 cache.code_seeds = self.code_seeds;
807 cache.erc20_balance_slots = self.erc20_balance_slots;
808 let block = BlockId::from(self.block);
809 cache.block = block;
810 let _ = cache.backend.set_pinned_block(block);
811 cache.block_number = self.block_number;
812 cache.basefee = self.basefee;
813 cache.coinbase = self.coinbase;
814 cache.prevrandao = self.prevrandao;
815 cache.block_gas_limit = self.block_gas_limit;
816 cache.timestamp_override = self.timestamp_override;
817 cache.block_env_source = self.block_env_source;
818 cache.spec_id = self.spec_id;
819 cache.snapshot_generation = self.snapshot_generation;
820 cache.base = None;
821 cache.base_dirty.clear();
822 cache.base_full_rebuild = true;
823 cache.base_storage_lens.clear();
824 }
825}
826
827type BackendMapsSnapshot = (
828 Vec<(Address, AccountInfo)>,
829 Vec<(Address, Vec<(U256, U256)>)>,
830 Vec<(U256, B256)>,
831);
832
833fn capture_backend_maps(blockchain_db: &BlockchainDb) -> BackendMapsSnapshot {
834 let accounts = blockchain_db.accounts().read();
842 let storage = blockchain_db.storage().read();
843 let block_hashes = blockchain_db.block_hashes().read();
844 let backend_accounts = accounts
845 .iter()
846 .map(|(address, info)| (*address, info.clone()))
847 .collect();
848 let backend_storage = storage
849 .iter()
850 .map(|(address, slots)| {
851 (
852 *address,
853 slots.iter().map(|(key, value)| (*key, *value)).collect(),
854 )
855 })
856 .collect();
857 let backend_block_hashes = block_hashes
858 .iter()
859 .map(|(number, hash)| (*number, *hash))
860 .collect();
861 (backend_accounts, backend_storage, backend_block_hashes)
862}
863
864#[cfg(test)]
865mod tests {
866 use std::{
867 fs,
868 sync::{Arc, Barrier},
869 thread,
870 time::{Duration, Instant},
871 };
872
873 use foundry_fork_db::{BlockchainDb, cache::BlockchainDbMeta};
874
875 use super::capture_backend_maps;
876 #[cfg(unix)]
877 use super::create_unique_temp_file;
878
879 #[test]
880 fn generation_reservation_never_wraps_at_exhaustion() {
881 let store = super::DurableCheckpointStore {
882 path: "unused-checkpoint-test".into(),
883 coordinator: Arc::new(super::CheckpointWriteCoordinator::default()),
884 max_checkpoint_bytes: 1,
885 };
886 assert_eq!(store.reserve_generation().unwrap(), 1);
887 store
888 .coordinator
889 .latest_generation
890 .store(u64::MAX - 1, std::sync::atomic::Ordering::Release);
891 assert_eq!(store.reserve_generation().unwrap(), u64::MAX);
892 assert!(matches!(
893 store.reserve_generation(),
894 Err(super::DurableCheckpointError::GenerationExhausted)
895 ));
896 assert_eq!(
897 store
898 .coordinator
899 .latest_generation
900 .load(std::sync::atomic::Ordering::Acquire),
901 u64::MAX
902 );
903 }
904
905 #[test]
906 fn backend_capture_retains_earlier_guards_while_waiting_for_later_maps() {
907 let blockchain_db = Arc::new(BlockchainDb::new(BlockchainDbMeta::default(), None));
908 let storage_guard = blockchain_db.storage().write();
909 let start = Arc::new(Barrier::new(2));
910 let worker_db = Arc::clone(&blockchain_db);
911 let worker_start = Arc::clone(&start);
912 let capture = thread::spawn(move || {
913 worker_start.wait();
914 capture_backend_maps(&worker_db)
915 });
916 start.wait();
917
918 let deadline = Instant::now() + Duration::from_secs(2);
922 let retained_accounts_guard = loop {
923 if blockchain_db.accounts().try_write().is_none() {
924 break true;
925 }
926 if Instant::now() >= deadline {
927 break false;
928 }
929 thread::yield_now();
930 };
931 assert!(
932 retained_accounts_guard,
933 "capture must retain the accounts guard while awaiting storage"
934 );
935
936 drop(storage_guard);
937 capture.join().expect("capture thread");
938 }
939
940 #[cfg(unix)]
941 #[test]
942 fn stale_temp_candidate_is_skipped_without_blocking_checkpoint_progress() {
943 use std::os::unix::fs::PermissionsExt;
944
945 let root = std::env::temp_dir().join(format!(
946 "evm-fork-cache-stale-temp-{}-{}",
947 std::process::id(),
948 super::NEXT_TEMP_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
949 ));
950 fs::create_dir_all(&root).expect("create test directory");
951 let destination = root.join("checkpoint.bin");
952 let stale = root.join(".checkpoint.bin.first");
953 let fresh = root.join(".checkpoint.bin.second");
954 fs::write(&stale, b"stale crash residue").expect("precreate first candidate");
955 let mut candidates = [stale.clone(), fresh.clone()].into_iter();
956
957 let (selected, file) = create_unique_temp_file(&destination, || {
958 candidates.next().expect("bounded test candidates")
959 })
960 .expect("collision must retry with the next candidate");
961 drop(file);
962
963 assert_eq!(selected, fresh);
964 assert_eq!(
965 fs::metadata(&selected)
966 .expect("fresh temp metadata")
967 .permissions()
968 .mode()
969 & 0o777,
970 0o600,
971 "checkpoint temp files contain provider cursors and must be owner-only"
972 );
973 assert_eq!(
974 fs::read(&stale).expect("stale file remains"),
975 b"stale crash residue"
976 );
977 fs::remove_dir_all(root).expect("remove test directory");
978 }
979}
980
981#[cfg(unix)]
982fn atomic_replace(path: &Path, data: &[u8]) -> Result<(), DurableCheckpointError> {
983 let parent = path.parent().unwrap_or_else(|| Path::new("."));
984 fs::create_dir_all(parent).map_err(|source| DurableCheckpointError::CreateDir {
985 path: parent.to_path_buf(),
986 source,
987 })?;
988
989 let (temp_path, mut file) = create_unique_temp_file(path, || next_temp_path(path))?;
990 let result = (|| {
991 file.write_all(data)
992 .and_then(|()| file.sync_all())
993 .map_err(|source| DurableCheckpointError::Write {
994 path: temp_path.clone(),
995 source,
996 })?;
997 fs::rename(&temp_path, path).map_err(|source| DurableCheckpointError::Rename {
998 from: temp_path.clone(),
999 to: path.to_path_buf(),
1000 source,
1001 })?;
1002 sync_parent_directory(parent)?;
1003 Ok(())
1004 })();
1005
1006 if result.is_err() {
1007 let _ = fs::remove_file(&temp_path);
1008 }
1009 result
1010}
1011
1012#[cfg(not(unix))]
1013fn atomic_replace(path: &Path, _data: &[u8]) -> Result<(), DurableCheckpointError> {
1014 Err(DurableCheckpointError::AtomicReplaceUnsupported {
1015 path: path.to_path_buf(),
1016 })
1017}
1018
1019#[cfg(unix)]
1020fn create_unique_temp_file(
1021 destination: &Path,
1022 mut next_candidate: impl FnMut() -> PathBuf,
1023) -> Result<(PathBuf, File), DurableCheckpointError> {
1024 use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
1025
1026 for _ in 0..MAX_TEMP_CREATE_ATTEMPTS {
1027 let candidate = next_candidate();
1028 match OpenOptions::new()
1029 .write(true)
1030 .create_new(true)
1031 .mode(0o600)
1032 .open(&candidate)
1033 {
1034 Ok(file) => {
1035 if let Err(source) = file.set_permissions(fs::Permissions::from_mode(0o600)) {
1039 drop(file);
1040 let _ = fs::remove_file(&candidate);
1041 return Err(DurableCheckpointError::Write {
1042 path: candidate,
1043 source,
1044 });
1045 }
1046 return Ok((candidate, file));
1047 }
1048 Err(source) if source.kind() == io::ErrorKind::AlreadyExists => continue,
1049 Err(source) => {
1050 return Err(DurableCheckpointError::Write {
1051 path: candidate,
1052 source,
1053 });
1054 }
1055 }
1056 }
1057 Err(DurableCheckpointError::TemporaryPathExhausted {
1058 path: destination.to_path_buf(),
1059 attempts: MAX_TEMP_CREATE_ATTEMPTS,
1060 })
1061}
1062
1063#[cfg(unix)]
1064fn sync_parent_directory(parent: &Path) -> Result<(), DurableCheckpointError> {
1065 File::open(parent)
1066 .and_then(|directory| directory.sync_all())
1067 .map_err(|source| DurableCheckpointError::SyncDirectory {
1068 path: parent.to_path_buf(),
1069 source,
1070 })
1071}
1072
1073#[cfg(not(unix))]
1074fn sync_parent_directory(_parent: &Path) -> Result<(), DurableCheckpointError> {
1075 Ok(())
1078}
1079
1080#[cfg(unix)]
1081fn next_temp_path(path: &Path) -> PathBuf {
1082 let id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
1083 let name = path
1084 .file_name()
1085 .and_then(|name| name.to_str())
1086 .unwrap_or("checkpoint");
1087 path.with_file_name(format!(".{name}.tmp-{}-{id}", std::process::id()))
1088}
1089
1090#[derive(Debug, thiserror::Error)]
1092#[non_exhaustive]
1093pub enum DurableCheckpointError {
1094 #[error("atomic durable checkpoint replacement for {path:?} is unsupported on this platform")]
1096 AtomicReplaceUnsupported {
1097 path: PathBuf,
1099 },
1100 #[error(transparent)]
1102 Encode(#[from] crate::errors::PersistenceError),
1103 #[error("durable checkpoint writer task failed: {0}")]
1105 TaskJoin(#[source] tokio::task::JoinError),
1106 #[error("durable checkpoint writer generation exhausted")]
1108 GenerationExhausted,
1109 #[error(
1111 "durable checkpoint write generation {generation} was superseded by generation {latest}"
1112 )]
1113 WriteSuperseded {
1114 generation: u64,
1116 latest: u64,
1118 },
1119 #[error("failed to read durable checkpoint {path:?}: {source}")]
1121 Read {
1122 path: PathBuf,
1124 #[source]
1126 source: io::Error,
1127 },
1128 #[error("durable checkpoint {path:?} has an invalid or unsupported format")]
1130 InvalidFormat {
1131 path: PathBuf,
1133 },
1134 #[error("durable checkpoint {path:?} failed its integrity checksum")]
1136 ChecksumMismatch {
1137 path: PathBuf,
1139 },
1140 #[error(
1142 "durable checkpoint {path:?} is {bytes} bytes, exceeding the configured {max_bytes}-byte limit"
1143 )]
1144 CheckpointTooLarge {
1145 path: PathBuf,
1147 bytes: u64,
1149 max_bytes: u64,
1151 },
1152 #[error("durable checkpoint {path:?} size exceeds supported accounting")]
1154 CheckpointSizeOverflow {
1155 path: PathBuf,
1157 },
1158 #[error("failed to create durable checkpoint directory {path:?}: {source}")]
1160 CreateDir {
1161 path: PathBuf,
1163 #[source]
1165 source: io::Error,
1166 },
1167 #[error("failed to write durable checkpoint {path:?}: {source}")]
1169 Write {
1170 path: PathBuf,
1172 #[source]
1174 source: io::Error,
1175 },
1176 #[error(
1178 "failed to allocate a unique temporary file for durable checkpoint {path:?} after {attempts} attempts"
1179 )]
1180 TemporaryPathExhausted {
1181 path: PathBuf,
1183 attempts: usize,
1185 },
1186 #[error("failed to replace durable checkpoint {to:?} from {from:?}: {source}")]
1188 Rename {
1189 from: PathBuf,
1191 to: PathBuf,
1193 #[source]
1195 source: io::Error,
1196 },
1197 #[error("failed to sync durable checkpoint directory {path:?}: {source}")]
1201 SyncDirectory {
1202 path: PathBuf,
1204 #[source]
1206 source: io::Error,
1207 },
1208 #[error("durable checkpoint identity mismatch: expected {expected:?}, found {actual:?}")]
1210 IdentityMismatch {
1211 expected: DurableCheckpointIdentity,
1213 actual: DurableCheckpointIdentity,
1215 },
1216 #[error(
1218 "durable checkpoint chain {checkpoint_chain_id} does not match cache chain {cache_chain_id}"
1219 )]
1220 CacheChainMismatch {
1221 cache_chain_id: u64,
1223 checkpoint_chain_id: u64,
1225 },
1226}