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 = 6;
35const CHECKPOINT_LABEL: &str = "durable reactive checkpoint";
36const CHECKPOINT_CHECKSUM_BYTES: usize = 32;
37const CHECKPOINT_HEADER_BYTES: u64 =
38 CHECKPOINT_MAGIC.len() as u64 + std::mem::size_of::<u32>() as u64;
39const MAX_TEMP_CREATE_ATTEMPTS: usize = 128;
40pub const DEFAULT_MAX_DURABLE_CHECKPOINT_BYTES: u64 = 512 * 1024 * 1024;
42static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(0);
43static CHECKPOINT_COORDINATORS: OnceLock<
44 Mutex<HashMap<PathBuf, Weak<CheckpointWriteCoordinator>>>,
45> = OnceLock::new();
46
47#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
54#[non_exhaustive]
55pub struct DurableCheckpointIdentity {
56 pub chain_id: u64,
58 pub subscriber_id: String,
60 pub handler_set_id: String,
62}
63
64impl DurableCheckpointIdentity {
65 pub fn new(
67 chain_id: u64,
68 subscriber_id: impl Into<String>,
69 handler_set_id: impl Into<String>,
70 ) -> Self {
71 Self {
72 chain_id,
73 subscriber_id: subscriber_id.into(),
74 handler_set_id: handler_set_id.into(),
75 }
76 }
77}
78
79#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
81#[non_exhaustive]
82pub struct DurableCheckpointBlock {
83 pub number: u64,
85 pub hash: B256,
88 pub parent_hash: Option<B256>,
90 pub timestamp: Option<u64>,
92}
93
94impl DurableCheckpointBlock {
95 pub const fn new(number: u64, hash: B256) -> Self {
97 Self {
98 number,
99 hash,
100 parent_hash: None,
101 timestamp: None,
102 }
103 }
104
105 pub const fn with_parent_hash(mut self, parent_hash: B256) -> Self {
107 self.parent_hash = Some(parent_hash);
108 self
109 }
110
111 pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
113 self.timestamp = Some(timestamp);
114 self
115 }
116}
117
118#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
120#[non_exhaustive]
121pub struct DurableCheckpointMetadata {
122 pub identity: DurableCheckpointIdentity,
124 pub block: DurableCheckpointBlock,
126 pub delivery_token: Option<Vec<u8>>,
131 pub delivery_witness: Option<B256>,
140 pub subscriber_checkpoint: Option<Vec<u8>>,
145 pub runtime_checkpoint: Option<Vec<u8>>,
151}
152
153impl DurableCheckpointMetadata {
154 pub fn new(identity: DurableCheckpointIdentity, block: DurableCheckpointBlock) -> Self {
156 Self {
157 identity,
158 block,
159 delivery_token: None,
160 delivery_witness: None,
161 subscriber_checkpoint: None,
162 runtime_checkpoint: None,
163 }
164 }
165
166 pub fn with_delivery_token(mut self, delivery_token: impl Into<Vec<u8>>) -> Self {
168 self.delivery_token = Some(delivery_token.into());
169 self
170 }
171
172 pub fn with_delivery_witness(mut self, delivery_witness: B256) -> Self {
178 self.delivery_witness = Some(delivery_witness);
179 self
180 }
181
182 pub fn with_subscriber_checkpoint(mut self, checkpoint: impl Into<Vec<u8>>) -> Self {
184 self.subscriber_checkpoint = Some(checkpoint.into());
185 self
186 }
187
188 pub fn with_runtime_checkpoint(mut self, checkpoint: impl Into<Vec<u8>>) -> Self {
190 self.runtime_checkpoint = Some(checkpoint.into());
191 self
192 }
193}
194
195#[derive(Clone, Debug)]
205pub struct DurableCheckpointStore {
206 path: PathBuf,
207 coordinator: Arc<CheckpointWriteCoordinator>,
208 max_checkpoint_bytes: u64,
209}
210
211#[derive(Debug, Default)]
212struct CheckpointWriteCoordinator {
213 latest_generation: AtomicU64,
214 writer: Mutex<()>,
215}
216
217impl PartialEq for DurableCheckpointStore {
218 fn eq(&self, other: &Self) -> bool {
219 self.path == other.path
220 }
221}
222
223impl Eq for DurableCheckpointStore {}
224
225impl DurableCheckpointStore {
226 pub fn new(path: impl Into<PathBuf>) -> Self {
228 let path = normalized_checkpoint_path(&path.into());
229 Self {
230 coordinator: checkpoint_coordinator(&path),
231 path,
232 max_checkpoint_bytes: DEFAULT_MAX_DURABLE_CHECKPOINT_BYTES,
233 }
234 }
235
236 pub fn with_max_checkpoint_bytes(mut self, max_checkpoint_bytes: u64) -> Self {
245 self.max_checkpoint_bytes = max_checkpoint_bytes;
246 self
247 }
248
249 pub fn max_checkpoint_bytes(&self) -> u64 {
251 self.max_checkpoint_bytes
252 }
253
254 pub fn path(&self) -> &Path {
256 &self.path
257 }
258
259 pub fn save(
280 &self,
281 cache: &EvmCache,
282 metadata: DurableCheckpointMetadata,
283 ) -> Result<(), DurableCheckpointError> {
284 validate_capture_identity(cache, &metadata)?;
285 let generation = self.reserve_generation()?;
289 let snapshot = DurableCheckpointSnapshot::capture(cache, metadata);
290 persist_snapshot(
291 &self.path,
292 snapshot,
293 &self.coordinator,
294 generation,
295 self.max_checkpoint_bytes,
296 )
297 }
298
299 pub async fn save_async(
315 &self,
316 cache: &EvmCache,
317 metadata: DurableCheckpointMetadata,
318 ) -> Result<(), DurableCheckpointError> {
319 validate_capture_identity(cache, &metadata)?;
320 let generation = self.reserve_generation()?;
323 let snapshot = DurableCheckpointSnapshot::capture(cache, metadata);
324 let path = self.path.clone();
325 let coordinator = Arc::clone(&self.coordinator);
326 let max_checkpoint_bytes = self.max_checkpoint_bytes;
327 tokio::task::spawn_blocking(move || {
328 persist_snapshot(
329 &path,
330 snapshot,
331 &coordinator,
332 generation,
333 max_checkpoint_bytes,
334 )
335 })
336 .await
337 .map_err(DurableCheckpointError::TaskJoin)?
338 }
339
340 fn reserve_generation(&self) -> Result<u64, DurableCheckpointError> {
341 self.coordinator
342 .latest_generation
343 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |generation| {
344 generation.checked_add(1)
345 })
346 .map(|previous| previous + 1)
347 .map_err(|_| DurableCheckpointError::GenerationExhausted)
348 }
349
350 pub fn load(&self) -> Result<Option<LoadedDurableCheckpoint>, DurableCheckpointError> {
363 let file = match File::open(&self.path) {
364 Ok(file) => file,
365 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
366 Err(source) => {
367 return Err(DurableCheckpointError::Read {
368 path: self.path.clone(),
369 source,
370 });
371 }
372 };
373 let reported_bytes = file
374 .metadata()
375 .map_err(|source| DurableCheckpointError::Read {
376 path: self.path.clone(),
377 source,
378 })?
379 .len();
380 if reported_bytes > self.max_checkpoint_bytes {
381 return Err(DurableCheckpointError::CheckpointTooLarge {
382 path: self.path.clone(),
383 bytes: reported_bytes,
384 max_bytes: self.max_checkpoint_bytes,
385 });
386 }
387 let mut data = Vec::new();
388 file.take(self.max_checkpoint_bytes.saturating_add(1))
389 .read_to_end(&mut data)
390 .map_err(|source| DurableCheckpointError::Read {
391 path: self.path.clone(),
392 source,
393 })?;
394 if data.len() as u64 > self.max_checkpoint_bytes {
395 return Err(DurableCheckpointError::CheckpointTooLarge {
396 path: self.path.clone(),
397 bytes: data.len() as u64,
398 max_bytes: self.max_checkpoint_bytes,
399 });
400 }
401 let Some(checksum_start) = data.len().checked_sub(CHECKPOINT_CHECKSUM_BYTES) else {
402 return Err(DurableCheckpointError::InvalidFormat {
403 path: self.path.clone(),
404 });
405 };
406 let encoded = &data[..checksum_start];
407 let expected = B256::from_slice(&data[checksum_start..]);
408 let actual = keccak256(encoded);
409 if actual != expected {
410 return Err(DurableCheckpointError::ChecksumMismatch {
411 path: self.path.clone(),
412 });
413 }
414 let snapshot = versioned::decode(
415 encoded,
416 CHECKPOINT_MAGIC,
417 CHECKPOINT_VERSION,
418 CHECKPOINT_LABEL,
419 )
420 .ok_or_else(|| DurableCheckpointError::InvalidFormat {
421 path: self.path.clone(),
422 })?;
423 Ok(Some(LoadedDurableCheckpoint { snapshot }))
424 }
425}
426
427fn checkpoint_coordinator(path: &Path) -> Arc<CheckpointWriteCoordinator> {
428 let key = normalized_checkpoint_path(path);
429 let coordinators = CHECKPOINT_COORDINATORS.get_or_init(|| Mutex::new(HashMap::new()));
430 let mut coordinators = coordinators
431 .lock()
432 .unwrap_or_else(std::sync::PoisonError::into_inner);
433 if let Some(coordinator) = coordinators.get(&key).and_then(Weak::upgrade) {
434 return coordinator;
435 }
436 coordinators.retain(|_, coordinator| coordinator.strong_count() > 0);
437 let coordinator = Arc::new(CheckpointWriteCoordinator::default());
438 coordinators.insert(key, Arc::downgrade(&coordinator));
439 coordinator
440}
441
442fn normalized_checkpoint_path(path: &Path) -> PathBuf {
443 let absolute = if path.is_absolute() {
444 path.to_path_buf()
445 } else {
446 std::env::current_dir()
447 .map(|directory| directory.join(path))
448 .unwrap_or_else(|_| path.to_path_buf())
449 };
450
451 let Some(file_name) = absolute.file_name() else {
456 return normalize_existing_path_prefix(&absolute);
457 };
458 let parent = absolute.parent().unwrap_or_else(|| Path::new("."));
459 normalize_existing_path_prefix(parent).join(file_name)
460}
461
462fn normalize_existing_path_prefix(absolute: &Path) -> PathBuf {
463 let components: Vec<_> = absolute.components().collect();
468 for split in (1..=components.len()).rev() {
469 let prefix: PathBuf = components[..split]
470 .iter()
471 .map(|component| component.as_os_str())
472 .collect();
473 let Ok(mut resolved) = prefix.canonicalize() else {
474 continue;
475 };
476 for component in &components[split..] {
477 match component {
478 Component::Prefix(prefix) => resolved.push(prefix.as_os_str()),
479 Component::RootDir => resolved.push(component.as_os_str()),
480 Component::CurDir => {}
481 Component::ParentDir => {
482 let _ = resolved.pop();
483 }
484 Component::Normal(part) => resolved.push(part),
485 }
486 }
487 return resolved;
488 }
489
490 absolute.to_path_buf()
493}
494
495fn validate_capture_identity(
496 cache: &EvmCache,
497 metadata: &DurableCheckpointMetadata,
498) -> Result<(), DurableCheckpointError> {
499 if metadata.identity.chain_id != cache.chain_id {
500 return Err(DurableCheckpointError::CacheChainMismatch {
501 cache_chain_id: cache.chain_id,
502 checkpoint_chain_id: metadata.identity.chain_id,
503 });
504 }
505 Ok(())
506}
507
508fn persist_snapshot(
509 path: &Path,
510 snapshot: DurableCheckpointSnapshot,
511 coordinator: &CheckpointWriteCoordinator,
512 generation: u64,
513 max_checkpoint_bytes: u64,
514) -> Result<(), DurableCheckpointError> {
515 let _writer = coordinator
516 .writer
517 .lock()
518 .unwrap_or_else(std::sync::PoisonError::into_inner);
519 let latest = coordinator.latest_generation.load(Ordering::Acquire);
520 if generation != latest {
521 return Err(DurableCheckpointError::WriteSuperseded { generation, latest });
522 }
523 let payload_bytes = bincode::serialized_size(&snapshot).map_err(|source| {
528 DurableCheckpointError::Encode(crate::errors::PersistenceError::serialize(
529 CHECKPOINT_LABEL,
530 source,
531 ))
532 })?;
533 let encoded_bytes = payload_bytes
534 .checked_add(CHECKPOINT_HEADER_BYTES)
535 .and_then(|bytes| bytes.checked_add(CHECKPOINT_CHECKSUM_BYTES as u64))
536 .ok_or(DurableCheckpointError::CheckpointSizeOverflow {
537 path: path.to_path_buf(),
538 })?;
539 if encoded_bytes > max_checkpoint_bytes {
540 return Err(DurableCheckpointError::CheckpointTooLarge {
541 path: path.to_path_buf(),
542 bytes: encoded_bytes,
543 max_bytes: max_checkpoint_bytes,
544 });
545 }
546 let mut data = versioned::encode(
547 CHECKPOINT_MAGIC,
548 CHECKPOINT_VERSION,
549 &snapshot,
550 CHECKPOINT_LABEL,
551 )
552 .map_err(DurableCheckpointError::Encode)?;
553 let checksum = keccak256(&data);
554 data.extend_from_slice(checksum.as_slice());
555 debug_assert_eq!(data.len() as u64, encoded_bytes);
556 atomic_replace(path, &data)
557}
558
559pub struct LoadedDurableCheckpoint {
561 snapshot: DurableCheckpointSnapshot,
562}
563
564impl LoadedDurableCheckpoint {
565 pub fn metadata(&self) -> &DurableCheckpointMetadata {
567 &self.snapshot.metadata
568 }
569
570 pub fn restore_into(
583 self,
584 cache: &mut EvmCache,
585 expected: &DurableCheckpointIdentity,
586 ) -> Result<DurableCheckpointMetadata, DurableCheckpointError> {
587 if &self.snapshot.metadata.identity != expected {
588 return Err(DurableCheckpointError::IdentityMismatch {
589 expected: expected.clone(),
590 actual: self.snapshot.metadata.identity.clone(),
591 });
592 }
593 if cache.chain_id != expected.chain_id {
594 return Err(DurableCheckpointError::CacheChainMismatch {
595 cache_chain_id: cache.chain_id,
596 checkpoint_chain_id: expected.chain_id,
597 });
598 }
599 Ok(self.snapshot.restore(cache))
600 }
601}
602
603#[derive(Serialize, Deserialize)]
604struct DurableCheckpointSnapshot {
605 metadata: DurableCheckpointMetadata,
606 state: EvmCacheStateSnapshot,
607}
608
609#[derive(Clone, Serialize, Deserialize)]
612pub(crate) struct EvmCacheStateSnapshot {
613 backend_accounts: Vec<(Address, AccountInfo)>,
614 backend_storage: Vec<(Address, Vec<(U256, U256)>)>,
615 backend_block_hashes: Vec<(U256, B256)>,
616 overlay: Cache,
617 token_decimals: HashMap<Address, u8>,
618 immutable_cache: ImmutableDataCache,
619 code_seeds: HashMap<Address, CodeSeedState>,
620 erc20_balance_slots: HashMap<Address, TrackedMapping>,
621 block: PersistedBlockId,
622 block_number: Option<u64>,
623 basefee: Option<u64>,
624 coinbase: Option<Address>,
625 prevrandao: Option<B256>,
626 block_gas_limit: Option<u64>,
627 timestamp_override: Option<u64>,
628 block_env_source: Option<BlockEnvSource>,
629 spec_id: SpecId,
630 snapshot_generation: u64,
631}
632
633#[derive(Clone, Copy, Serialize, Deserialize)]
634enum PersistedBlockId {
635 Hash {
636 hash: B256,
637 require_canonical: Option<bool>,
638 },
639 Latest,
640 Finalized,
641 Safe,
642 Earliest,
643 Pending,
644 Number(u64),
645}
646
647impl From<BlockId> for PersistedBlockId {
648 fn from(block: BlockId) -> Self {
649 match block {
650 BlockId::Hash(hash) => Self::Hash {
651 hash: hash.block_hash,
652 require_canonical: hash.require_canonical,
653 },
654 BlockId::Number(BlockNumberOrTag::Latest) => Self::Latest,
655 BlockId::Number(BlockNumberOrTag::Finalized) => Self::Finalized,
656 BlockId::Number(BlockNumberOrTag::Safe) => Self::Safe,
657 BlockId::Number(BlockNumberOrTag::Earliest) => Self::Earliest,
658 BlockId::Number(BlockNumberOrTag::Pending) => Self::Pending,
659 BlockId::Number(BlockNumberOrTag::Number(number)) => Self::Number(number),
660 }
661 }
662}
663
664impl From<PersistedBlockId> for BlockId {
665 fn from(block: PersistedBlockId) -> Self {
666 match block {
667 PersistedBlockId::Hash {
668 hash,
669 require_canonical,
670 } => BlockId::Hash(RpcBlockHash::from_hash(hash, require_canonical)),
671 PersistedBlockId::Latest => BlockId::latest(),
672 PersistedBlockId::Finalized => BlockId::finalized(),
673 PersistedBlockId::Safe => BlockId::safe(),
674 PersistedBlockId::Earliest => BlockId::earliest(),
675 PersistedBlockId::Pending => BlockId::pending(),
676 PersistedBlockId::Number(number) => BlockId::number(number),
677 }
678 }
679}
680
681impl DurableCheckpointSnapshot {
682 fn capture(cache: &EvmCache, metadata: DurableCheckpointMetadata) -> Self {
683 let mut state = EvmCacheStateSnapshot::capture(cache);
684 state.align_to_checkpoint_block(&metadata.block);
685 Self { metadata, state }
686 }
687
688 fn restore(self, cache: &mut EvmCache) -> DurableCheckpointMetadata {
689 let block_hash = self.metadata.block.hash;
690 self.state.restore(cache);
691
692 let block = alloy_eips::BlockId::from((block_hash, Some(true)));
698 cache.block = block;
699 let _ = cache.backend.set_pinned_block(block);
700
701 self.metadata
702 }
703}
704
705impl EvmCacheStateSnapshot {
706 pub(crate) fn capture(cache: &EvmCache) -> Self {
707 let (backend_accounts, backend_storage, backend_block_hashes) =
708 capture_backend_maps(&cache.blockchain_db);
709
710 Self {
711 backend_accounts,
712 backend_storage,
713 backend_block_hashes,
714 overlay: cache.db.cache.clone(),
715 token_decimals: cache.token_decimals.clone(),
716 immutable_cache: cache.immutable_cache.clone(),
717 code_seeds: cache.code_seeds.clone(),
718 erc20_balance_slots: cache.erc20_balance_slots.clone(),
719 block: cache.block.into(),
720 block_number: cache.block_number,
721 basefee: cache.basefee,
722 coinbase: cache.coinbase,
723 prevrandao: cache.prevrandao,
724 block_gas_limit: cache.block_gas_limit,
725 timestamp_override: cache.timestamp_override,
726 block_env_source: cache.block_env_source,
727 spec_id: cache.spec_id,
728 snapshot_generation: cache.snapshot_generation,
729 }
730 }
731
732 fn align_to_checkpoint_block(&mut self, block: &DurableCheckpointBlock) {
743 let preserve_full_env = matches!(
744 self.block_env_source,
745 Some(BlockEnvSource::VerifiedHash { number, hash })
746 if number == block.number
747 && hash == block.hash
748 && block
749 .timestamp
750 .zip(self.timestamp_override)
751 .is_none_or(|(expected, actual)| expected == actual)
752 );
753 self.block = PersistedBlockId::Hash {
754 hash: block.hash,
755 require_canonical: Some(true),
756 };
757 self.block_number = Some(block.number);
758 if !preserve_full_env {
759 self.timestamp_override = block.timestamp;
760 self.basefee = None;
761 self.coinbase = None;
762 self.prevrandao = None;
763 self.block_gas_limit = None;
764 self.block_env_source = None;
765 }
766 }
767
768 pub(crate) fn restore(self, cache: &mut EvmCache) {
769 {
770 let mut accounts = cache.blockchain_db.accounts().write();
771 accounts.clear();
772 accounts.extend(self.backend_accounts);
773 }
774 {
775 let mut storage = cache.blockchain_db.storage().write();
776 storage.clear();
777 storage.extend(
778 self.backend_storage
779 .into_iter()
780 .map(|(address, slots)| (address, slots.into_iter().collect())),
781 );
782 }
783 {
784 let mut hashes = cache.blockchain_db.block_hashes().write();
785 hashes.clear();
786 hashes.extend(self.backend_block_hashes);
787 }
788
789 cache.db.cache = self.overlay;
790 cache.token_decimals = self.token_decimals;
791 cache.immutable_cache = self.immutable_cache;
792 cache.code_seeds = self.code_seeds;
793 cache.erc20_balance_slots = self.erc20_balance_slots;
794 let block = BlockId::from(self.block);
795 cache.block = block;
796 let _ = cache.backend.set_pinned_block(block);
797 cache.block_number = self.block_number;
798 cache.basefee = self.basefee;
799 cache.coinbase = self.coinbase;
800 cache.prevrandao = self.prevrandao;
801 cache.block_gas_limit = self.block_gas_limit;
802 cache.timestamp_override = self.timestamp_override;
803 cache.block_env_source = self.block_env_source;
804 cache.spec_id = self.spec_id;
805 cache.snapshot_generation = self.snapshot_generation;
806 cache.base = None;
807 cache.base_dirty.clear();
808 cache.base_full_rebuild = true;
809 cache.base_storage_lens.clear();
810 }
811}
812
813type BackendMapsSnapshot = (
814 Vec<(Address, AccountInfo)>,
815 Vec<(Address, Vec<(U256, U256)>)>,
816 Vec<(U256, B256)>,
817);
818
819fn capture_backend_maps(blockchain_db: &BlockchainDb) -> BackendMapsSnapshot {
820 let accounts = blockchain_db.accounts().read();
828 let storage = blockchain_db.storage().read();
829 let block_hashes = blockchain_db.block_hashes().read();
830 let backend_accounts = accounts
831 .iter()
832 .map(|(address, info)| (*address, info.clone()))
833 .collect();
834 let backend_storage = storage
835 .iter()
836 .map(|(address, slots)| {
837 (
838 *address,
839 slots.iter().map(|(key, value)| (*key, *value)).collect(),
840 )
841 })
842 .collect();
843 let backend_block_hashes = block_hashes
844 .iter()
845 .map(|(number, hash)| (*number, *hash))
846 .collect();
847 (backend_accounts, backend_storage, backend_block_hashes)
848}
849
850#[cfg(test)]
851mod tests {
852 use std::{
853 fs,
854 sync::{Arc, Barrier},
855 thread,
856 time::{Duration, Instant},
857 };
858
859 use foundry_fork_db::{BlockchainDb, cache::BlockchainDbMeta};
860
861 use super::capture_backend_maps;
862 #[cfg(unix)]
863 use super::create_unique_temp_file;
864
865 #[test]
866 fn backend_capture_retains_earlier_guards_while_waiting_for_later_maps() {
867 let blockchain_db = Arc::new(BlockchainDb::new(BlockchainDbMeta::default(), None));
868 let storage_guard = blockchain_db.storage().write();
869 let start = Arc::new(Barrier::new(2));
870 let worker_db = Arc::clone(&blockchain_db);
871 let worker_start = Arc::clone(&start);
872 let capture = thread::spawn(move || {
873 worker_start.wait();
874 capture_backend_maps(&worker_db)
875 });
876 start.wait();
877
878 let deadline = Instant::now() + Duration::from_secs(2);
882 let retained_accounts_guard = loop {
883 if blockchain_db.accounts().try_write().is_none() {
884 break true;
885 }
886 if Instant::now() >= deadline {
887 break false;
888 }
889 thread::yield_now();
890 };
891 assert!(
892 retained_accounts_guard,
893 "capture must retain the accounts guard while awaiting storage"
894 );
895
896 drop(storage_guard);
897 capture.join().expect("capture thread");
898 }
899
900 #[cfg(unix)]
901 #[test]
902 fn stale_temp_candidate_is_skipped_without_blocking_checkpoint_progress() {
903 use std::os::unix::fs::PermissionsExt;
904
905 let root = std::env::temp_dir().join(format!(
906 "evm-fork-cache-stale-temp-{}-{}",
907 std::process::id(),
908 super::NEXT_TEMP_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
909 ));
910 fs::create_dir_all(&root).expect("create test directory");
911 let destination = root.join("checkpoint.bin");
912 let stale = root.join(".checkpoint.bin.first");
913 let fresh = root.join(".checkpoint.bin.second");
914 fs::write(&stale, b"stale crash residue").expect("precreate first candidate");
915 let mut candidates = [stale.clone(), fresh.clone()].into_iter();
916
917 let (selected, file) = create_unique_temp_file(&destination, || {
918 candidates.next().expect("bounded test candidates")
919 })
920 .expect("collision must retry with the next candidate");
921 drop(file);
922
923 assert_eq!(selected, fresh);
924 assert_eq!(
925 fs::metadata(&selected)
926 .expect("fresh temp metadata")
927 .permissions()
928 .mode()
929 & 0o777,
930 0o600,
931 "checkpoint temp files contain provider cursors and must be owner-only"
932 );
933 assert_eq!(
934 fs::read(&stale).expect("stale file remains"),
935 b"stale crash residue"
936 );
937 fs::remove_dir_all(root).expect("remove test directory");
938 }
939}
940
941#[cfg(unix)]
942fn atomic_replace(path: &Path, data: &[u8]) -> Result<(), DurableCheckpointError> {
943 let parent = path.parent().unwrap_or_else(|| Path::new("."));
944 fs::create_dir_all(parent).map_err(|source| DurableCheckpointError::CreateDir {
945 path: parent.to_path_buf(),
946 source,
947 })?;
948
949 let (temp_path, mut file) = create_unique_temp_file(path, || next_temp_path(path))?;
950 let result = (|| {
951 file.write_all(data)
952 .and_then(|()| file.sync_all())
953 .map_err(|source| DurableCheckpointError::Write {
954 path: temp_path.clone(),
955 source,
956 })?;
957 fs::rename(&temp_path, path).map_err(|source| DurableCheckpointError::Rename {
958 from: temp_path.clone(),
959 to: path.to_path_buf(),
960 source,
961 })?;
962 sync_parent_directory(parent)?;
963 Ok(())
964 })();
965
966 if result.is_err() {
967 let _ = fs::remove_file(&temp_path);
968 }
969 result
970}
971
972#[cfg(not(unix))]
973fn atomic_replace(path: &Path, _data: &[u8]) -> Result<(), DurableCheckpointError> {
974 Err(DurableCheckpointError::AtomicReplaceUnsupported {
975 path: path.to_path_buf(),
976 })
977}
978
979#[cfg(unix)]
980fn create_unique_temp_file(
981 destination: &Path,
982 mut next_candidate: impl FnMut() -> PathBuf,
983) -> Result<(PathBuf, File), DurableCheckpointError> {
984 use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
985
986 for _ in 0..MAX_TEMP_CREATE_ATTEMPTS {
987 let candidate = next_candidate();
988 match OpenOptions::new()
989 .write(true)
990 .create_new(true)
991 .mode(0o600)
992 .open(&candidate)
993 {
994 Ok(file) => {
995 if let Err(source) = file.set_permissions(fs::Permissions::from_mode(0o600)) {
999 drop(file);
1000 let _ = fs::remove_file(&candidate);
1001 return Err(DurableCheckpointError::Write {
1002 path: candidate,
1003 source,
1004 });
1005 }
1006 return Ok((candidate, file));
1007 }
1008 Err(source) if source.kind() == io::ErrorKind::AlreadyExists => continue,
1009 Err(source) => {
1010 return Err(DurableCheckpointError::Write {
1011 path: candidate,
1012 source,
1013 });
1014 }
1015 }
1016 }
1017 Err(DurableCheckpointError::TemporaryPathExhausted {
1018 path: destination.to_path_buf(),
1019 attempts: MAX_TEMP_CREATE_ATTEMPTS,
1020 })
1021}
1022
1023#[cfg(unix)]
1024fn sync_parent_directory(parent: &Path) -> Result<(), DurableCheckpointError> {
1025 File::open(parent)
1026 .and_then(|directory| directory.sync_all())
1027 .map_err(|source| DurableCheckpointError::SyncDirectory {
1028 path: parent.to_path_buf(),
1029 source,
1030 })
1031}
1032
1033#[cfg(not(unix))]
1034fn sync_parent_directory(_parent: &Path) -> Result<(), DurableCheckpointError> {
1035 Ok(())
1038}
1039
1040#[cfg(unix)]
1041fn next_temp_path(path: &Path) -> PathBuf {
1042 let id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
1043 let name = path
1044 .file_name()
1045 .and_then(|name| name.to_str())
1046 .unwrap_or("checkpoint");
1047 path.with_file_name(format!(".{name}.tmp-{}-{id}", std::process::id()))
1048}
1049
1050#[derive(Debug, thiserror::Error)]
1052#[non_exhaustive]
1053pub enum DurableCheckpointError {
1054 #[error("atomic durable checkpoint replacement for {path:?} is unsupported on this platform")]
1056 AtomicReplaceUnsupported {
1057 path: PathBuf,
1059 },
1060 #[error(transparent)]
1062 Encode(#[from] crate::errors::PersistenceError),
1063 #[error("durable checkpoint writer task failed: {0}")]
1065 TaskJoin(#[source] tokio::task::JoinError),
1066 #[error("durable checkpoint writer generation exhausted")]
1068 GenerationExhausted,
1069 #[error(
1071 "durable checkpoint write generation {generation} was superseded by generation {latest}"
1072 )]
1073 WriteSuperseded {
1074 generation: u64,
1076 latest: u64,
1078 },
1079 #[error("failed to read durable checkpoint {path:?}: {source}")]
1081 Read {
1082 path: PathBuf,
1084 #[source]
1086 source: io::Error,
1087 },
1088 #[error("durable checkpoint {path:?} has an invalid or unsupported format")]
1090 InvalidFormat {
1091 path: PathBuf,
1093 },
1094 #[error("durable checkpoint {path:?} failed its integrity checksum")]
1096 ChecksumMismatch {
1097 path: PathBuf,
1099 },
1100 #[error(
1102 "durable checkpoint {path:?} is {bytes} bytes, exceeding the configured {max_bytes}-byte limit"
1103 )]
1104 CheckpointTooLarge {
1105 path: PathBuf,
1107 bytes: u64,
1109 max_bytes: u64,
1111 },
1112 #[error("durable checkpoint {path:?} size exceeds supported accounting")]
1114 CheckpointSizeOverflow {
1115 path: PathBuf,
1117 },
1118 #[error("failed to create durable checkpoint directory {path:?}: {source}")]
1120 CreateDir {
1121 path: PathBuf,
1123 #[source]
1125 source: io::Error,
1126 },
1127 #[error("failed to write durable checkpoint {path:?}: {source}")]
1129 Write {
1130 path: PathBuf,
1132 #[source]
1134 source: io::Error,
1135 },
1136 #[error(
1138 "failed to allocate a unique temporary file for durable checkpoint {path:?} after {attempts} attempts"
1139 )]
1140 TemporaryPathExhausted {
1141 path: PathBuf,
1143 attempts: usize,
1145 },
1146 #[error("failed to replace durable checkpoint {to:?} from {from:?}: {source}")]
1148 Rename {
1149 from: PathBuf,
1151 to: PathBuf,
1153 #[source]
1155 source: io::Error,
1156 },
1157 #[error("failed to sync durable checkpoint directory {path:?}: {source}")]
1161 SyncDirectory {
1162 path: PathBuf,
1164 #[source]
1166 source: io::Error,
1167 },
1168 #[error("durable checkpoint identity mismatch: expected {expected:?}, found {actual:?}")]
1170 IdentityMismatch {
1171 expected: DurableCheckpointIdentity,
1173 actual: DurableCheckpointIdentity,
1175 },
1176 #[error(
1178 "durable checkpoint chain {checkpoint_chain_id} does not match cache chain {cache_chain_id}"
1179 )]
1180 CacheChainMismatch {
1181 cache_chain_id: u64,
1183 checkpoint_chain_id: u64,
1185 },
1186}