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 self.coordinator
345 .latest_generation
346 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |generation| {
347 generation.checked_add(1)
348 })
349 .map(|previous| previous + 1)
350 .map_err(|_| DurableCheckpointError::GenerationExhausted)
351 }
352
353 pub fn load(&self) -> Result<Option<LoadedDurableCheckpoint>, DurableCheckpointError> {
366 let file = match File::open(&self.path) {
367 Ok(file) => file,
368 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
369 Err(source) => {
370 return Err(DurableCheckpointError::Read {
371 path: self.path.clone(),
372 source,
373 });
374 }
375 };
376 let reported_bytes = file
377 .metadata()
378 .map_err(|source| DurableCheckpointError::Read {
379 path: self.path.clone(),
380 source,
381 })?
382 .len();
383 if reported_bytes > self.max_checkpoint_bytes {
384 return Err(DurableCheckpointError::CheckpointTooLarge {
385 path: self.path.clone(),
386 bytes: reported_bytes,
387 max_bytes: self.max_checkpoint_bytes,
388 });
389 }
390 let mut data = Vec::new();
391 file.take(self.max_checkpoint_bytes.saturating_add(1))
392 .read_to_end(&mut data)
393 .map_err(|source| DurableCheckpointError::Read {
394 path: self.path.clone(),
395 source,
396 })?;
397 if data.len() as u64 > self.max_checkpoint_bytes {
398 return Err(DurableCheckpointError::CheckpointTooLarge {
399 path: self.path.clone(),
400 bytes: data.len() as u64,
401 max_bytes: self.max_checkpoint_bytes,
402 });
403 }
404 let Some(checksum_start) = data.len().checked_sub(CHECKPOINT_CHECKSUM_BYTES) else {
405 return Err(DurableCheckpointError::InvalidFormat {
406 path: self.path.clone(),
407 });
408 };
409 let encoded = &data[..checksum_start];
410 let expected = B256::from_slice(&data[checksum_start..]);
411 let actual = keccak256(encoded);
412 if actual != expected {
413 return Err(DurableCheckpointError::ChecksumMismatch {
414 path: self.path.clone(),
415 });
416 }
417 let snapshot = versioned::decode(
418 encoded,
419 CHECKPOINT_MAGIC,
420 CHECKPOINT_VERSION,
421 CHECKPOINT_LABEL,
422 )
423 .ok_or_else(|| DurableCheckpointError::InvalidFormat {
424 path: self.path.clone(),
425 })?;
426 Ok(Some(LoadedDurableCheckpoint { snapshot }))
427 }
428}
429
430fn checkpoint_coordinator(path: &Path) -> Arc<CheckpointWriteCoordinator> {
431 let key = normalized_checkpoint_path(path);
432 let coordinators = CHECKPOINT_COORDINATORS.get_or_init(|| Mutex::new(HashMap::new()));
433 let mut coordinators = coordinators
434 .lock()
435 .unwrap_or_else(std::sync::PoisonError::into_inner);
436 if let Some(coordinator) = coordinators.get(&key).and_then(Weak::upgrade) {
437 return coordinator;
438 }
439 coordinators.retain(|_, coordinator| coordinator.strong_count() > 0);
440 let coordinator = Arc::new(CheckpointWriteCoordinator::default());
441 coordinators.insert(key, Arc::downgrade(&coordinator));
442 coordinator
443}
444
445fn normalized_checkpoint_path(path: &Path) -> PathBuf {
446 let absolute = if path.is_absolute() {
447 path.to_path_buf()
448 } else {
449 std::env::current_dir()
450 .map(|directory| directory.join(path))
451 .unwrap_or_else(|_| path.to_path_buf())
452 };
453
454 let Some(file_name) = absolute.file_name() else {
459 return normalize_existing_path_prefix(&absolute);
460 };
461 let parent = absolute.parent().unwrap_or_else(|| Path::new("."));
462 normalize_existing_path_prefix(parent).join(file_name)
463}
464
465fn normalize_existing_path_prefix(absolute: &Path) -> PathBuf {
466 let components: Vec<_> = absolute.components().collect();
471 for split in (1..=components.len()).rev() {
472 let prefix: PathBuf = components[..split]
473 .iter()
474 .map(|component| component.as_os_str())
475 .collect();
476 let Ok(mut resolved) = prefix.canonicalize() else {
477 continue;
478 };
479 for component in &components[split..] {
480 match component {
481 Component::Prefix(prefix) => resolved.push(prefix.as_os_str()),
482 Component::RootDir => resolved.push(component.as_os_str()),
483 Component::CurDir => {}
484 Component::ParentDir => {
485 let _ = resolved.pop();
486 }
487 Component::Normal(part) => resolved.push(part),
488 }
489 }
490 return resolved;
491 }
492
493 absolute.to_path_buf()
496}
497
498fn validate_capture_identity(
499 cache: &EvmCache,
500 metadata: &DurableCheckpointMetadata,
501) -> Result<(), DurableCheckpointError> {
502 if metadata.identity.chain_id != cache.chain_id {
503 return Err(DurableCheckpointError::CacheChainMismatch {
504 cache_chain_id: cache.chain_id,
505 checkpoint_chain_id: metadata.identity.chain_id,
506 });
507 }
508 Ok(())
509}
510
511fn persist_snapshot(
512 path: &Path,
513 snapshot: DurableCheckpointSnapshot,
514 coordinator: &CheckpointWriteCoordinator,
515 generation: u64,
516 max_checkpoint_bytes: u64,
517) -> Result<(), DurableCheckpointError> {
518 let _writer = coordinator
519 .writer
520 .lock()
521 .unwrap_or_else(std::sync::PoisonError::into_inner);
522 let latest = coordinator.latest_generation.load(Ordering::Acquire);
523 if generation != latest {
524 return Err(DurableCheckpointError::WriteSuperseded { generation, latest });
525 }
526 let payload_bytes = bincode::serialized_size(&snapshot).map_err(|source| {
531 DurableCheckpointError::Encode(crate::errors::PersistenceError::serialize(
532 CHECKPOINT_LABEL,
533 source,
534 ))
535 })?;
536 let encoded_bytes = payload_bytes
537 .checked_add(CHECKPOINT_HEADER_BYTES)
538 .and_then(|bytes| bytes.checked_add(CHECKPOINT_CHECKSUM_BYTES as u64))
539 .ok_or(DurableCheckpointError::CheckpointSizeOverflow {
540 path: path.to_path_buf(),
541 })?;
542 if encoded_bytes > max_checkpoint_bytes {
543 return Err(DurableCheckpointError::CheckpointTooLarge {
544 path: path.to_path_buf(),
545 bytes: encoded_bytes,
546 max_bytes: max_checkpoint_bytes,
547 });
548 }
549 let mut data = versioned::encode(
550 CHECKPOINT_MAGIC,
551 CHECKPOINT_VERSION,
552 &snapshot,
553 CHECKPOINT_LABEL,
554 )
555 .map_err(DurableCheckpointError::Encode)?;
556 let checksum = keccak256(&data);
557 data.extend_from_slice(checksum.as_slice());
558 debug_assert_eq!(data.len() as u64, encoded_bytes);
559 atomic_replace(path, &data)
560}
561
562pub struct LoadedDurableCheckpoint {
564 snapshot: DurableCheckpointSnapshot,
565}
566
567impl LoadedDurableCheckpoint {
568 pub fn metadata(&self) -> &DurableCheckpointMetadata {
570 &self.snapshot.metadata
571 }
572
573 pub fn restore_into(
586 self,
587 cache: &mut EvmCache,
588 expected: &DurableCheckpointIdentity,
589 ) -> Result<DurableCheckpointMetadata, DurableCheckpointError> {
590 if &self.snapshot.metadata.identity != expected {
591 return Err(DurableCheckpointError::IdentityMismatch {
592 expected: expected.clone(),
593 actual: self.snapshot.metadata.identity.clone(),
594 });
595 }
596 if cache.chain_id != expected.chain_id {
597 return Err(DurableCheckpointError::CacheChainMismatch {
598 cache_chain_id: cache.chain_id,
599 checkpoint_chain_id: expected.chain_id,
600 });
601 }
602 Ok(self.snapshot.restore(cache))
603 }
604}
605
606#[derive(Serialize, Deserialize)]
607struct DurableCheckpointSnapshot {
608 metadata: DurableCheckpointMetadata,
609 state: EvmCacheStateSnapshot,
610}
611
612#[derive(Clone, Serialize, Deserialize)]
615pub(crate) struct EvmCacheStateSnapshot {
616 backend_accounts: Vec<(Address, AccountInfo)>,
617 backend_storage: Vec<(Address, Vec<(U256, U256)>)>,
618 backend_block_hashes: Vec<(U256, B256)>,
619 overlay: Cache,
620 token_decimals: HashMap<Address, u8>,
621 immutable_cache: ImmutableDataCache,
622 code_seeds: HashMap<Address, CodeSeedState>,
623 erc20_balance_slots: HashMap<Address, TrackedMapping>,
624 block: PersistedBlockId,
625 block_number: Option<u64>,
626 basefee: Option<u64>,
627 coinbase: Option<Address>,
628 prevrandao: Option<B256>,
629 block_gas_limit: Option<u64>,
630 timestamp_override: Option<u64>,
631 block_env_source: Option<BlockEnvSource>,
632 spec_id: SpecId,
633 snapshot_generation: u64,
634}
635
636#[derive(Clone, Copy, Serialize, Deserialize)]
637enum PersistedBlockId {
638 Hash {
639 hash: B256,
640 require_canonical: Option<bool>,
641 },
642 Latest,
643 Finalized,
644 Safe,
645 Earliest,
646 Pending,
647 Number(u64),
648}
649
650impl From<BlockId> for PersistedBlockId {
651 fn from(block: BlockId) -> Self {
652 match block {
653 BlockId::Hash(hash) => Self::Hash {
654 hash: hash.block_hash,
655 require_canonical: hash.require_canonical,
656 },
657 BlockId::Number(BlockNumberOrTag::Latest) => Self::Latest,
658 BlockId::Number(BlockNumberOrTag::Finalized) => Self::Finalized,
659 BlockId::Number(BlockNumberOrTag::Safe) => Self::Safe,
660 BlockId::Number(BlockNumberOrTag::Earliest) => Self::Earliest,
661 BlockId::Number(BlockNumberOrTag::Pending) => Self::Pending,
662 BlockId::Number(BlockNumberOrTag::Number(number)) => Self::Number(number),
663 }
664 }
665}
666
667impl From<PersistedBlockId> for BlockId {
668 fn from(block: PersistedBlockId) -> Self {
669 match block {
670 PersistedBlockId::Hash {
671 hash,
672 require_canonical,
673 } => BlockId::Hash(RpcBlockHash::from_hash(hash, require_canonical)),
674 PersistedBlockId::Latest => BlockId::latest(),
675 PersistedBlockId::Finalized => BlockId::finalized(),
676 PersistedBlockId::Safe => BlockId::safe(),
677 PersistedBlockId::Earliest => BlockId::earliest(),
678 PersistedBlockId::Pending => BlockId::pending(),
679 PersistedBlockId::Number(number) => BlockId::number(number),
680 }
681 }
682}
683
684impl DurableCheckpointSnapshot {
685 fn capture(cache: &EvmCache, metadata: DurableCheckpointMetadata) -> Self {
686 let mut state = EvmCacheStateSnapshot::capture(cache);
687 state.align_to_checkpoint_block(&metadata.block);
688 Self { metadata, state }
689 }
690
691 fn restore(self, cache: &mut EvmCache) -> DurableCheckpointMetadata {
692 let block_hash = self.metadata.block.hash;
693 self.state.restore(cache);
694
695 let block = alloy_eips::BlockId::from((block_hash, Some(true)));
701 cache.block = block;
702 let _ = cache.backend.set_pinned_block(block);
703
704 self.metadata
705 }
706}
707
708impl EvmCacheStateSnapshot {
709 pub(crate) fn capture(cache: &EvmCache) -> Self {
710 let (backend_accounts, backend_storage, backend_block_hashes) =
711 capture_backend_maps(&cache.blockchain_db);
712
713 Self {
714 backend_accounts,
715 backend_storage,
716 backend_block_hashes,
717 overlay: cache.db.cache.clone(),
718 token_decimals: cache.token_decimals.clone(),
719 immutable_cache: cache.immutable_cache.clone(),
720 code_seeds: cache.code_seeds.clone(),
721 erc20_balance_slots: cache.erc20_balance_slots.clone(),
722 block: cache.block.into(),
723 block_number: cache.block_number,
724 basefee: cache.basefee,
725 coinbase: cache.coinbase,
726 prevrandao: cache.prevrandao,
727 block_gas_limit: cache.block_gas_limit,
728 timestamp_override: cache.timestamp_override,
729 block_env_source: cache.block_env_source,
730 spec_id: cache.spec_id,
731 snapshot_generation: cache.snapshot_generation,
732 }
733 }
734
735 fn align_to_checkpoint_block(&mut self, block: &DurableCheckpointBlock) {
746 let preserve_full_env = matches!(
747 self.block_env_source,
748 Some(BlockEnvSource::VerifiedHash { number, hash })
749 if number == block.number
750 && hash == block.hash
751 && block
752 .timestamp
753 .zip(self.timestamp_override)
754 .is_none_or(|(expected, actual)| expected == actual)
755 );
756 self.block = PersistedBlockId::Hash {
757 hash: block.hash,
758 require_canonical: Some(true),
759 };
760 self.block_number = Some(block.number);
761 if !preserve_full_env {
762 self.timestamp_override = block.timestamp;
763 self.basefee = None;
764 self.coinbase = None;
765 self.prevrandao = None;
766 self.block_gas_limit = None;
767 self.block_env_source = None;
768 }
769 }
770
771 pub(crate) fn restore(self, cache: &mut EvmCache) {
772 {
773 let mut accounts = cache.blockchain_db.accounts().write();
774 accounts.clear();
775 accounts.extend(self.backend_accounts);
776 }
777 {
778 let mut storage = cache.blockchain_db.storage().write();
779 storage.clear();
780 storage.extend(
781 self.backend_storage
782 .into_iter()
783 .map(|(address, slots)| (address, slots.into_iter().collect())),
784 );
785 }
786 {
787 let mut hashes = cache.blockchain_db.block_hashes().write();
788 hashes.clear();
789 hashes.extend(self.backend_block_hashes);
790 }
791
792 cache.db.cache = self.overlay;
793 cache.token_decimals = self.token_decimals;
794 cache.immutable_cache = self.immutable_cache;
795 cache.code_seeds = self.code_seeds;
796 cache.erc20_balance_slots = self.erc20_balance_slots;
797 let block = BlockId::from(self.block);
798 cache.block = block;
799 let _ = cache.backend.set_pinned_block(block);
800 cache.block_number = self.block_number;
801 cache.basefee = self.basefee;
802 cache.coinbase = self.coinbase;
803 cache.prevrandao = self.prevrandao;
804 cache.block_gas_limit = self.block_gas_limit;
805 cache.timestamp_override = self.timestamp_override;
806 cache.block_env_source = self.block_env_source;
807 cache.spec_id = self.spec_id;
808 cache.snapshot_generation = self.snapshot_generation;
809 cache.base = None;
810 cache.base_dirty.clear();
811 cache.base_full_rebuild = true;
812 cache.base_storage_lens.clear();
813 }
814}
815
816type BackendMapsSnapshot = (
817 Vec<(Address, AccountInfo)>,
818 Vec<(Address, Vec<(U256, U256)>)>,
819 Vec<(U256, B256)>,
820);
821
822fn capture_backend_maps(blockchain_db: &BlockchainDb) -> BackendMapsSnapshot {
823 let accounts = blockchain_db.accounts().read();
831 let storage = blockchain_db.storage().read();
832 let block_hashes = blockchain_db.block_hashes().read();
833 let backend_accounts = accounts
834 .iter()
835 .map(|(address, info)| (*address, info.clone()))
836 .collect();
837 let backend_storage = storage
838 .iter()
839 .map(|(address, slots)| {
840 (
841 *address,
842 slots.iter().map(|(key, value)| (*key, *value)).collect(),
843 )
844 })
845 .collect();
846 let backend_block_hashes = block_hashes
847 .iter()
848 .map(|(number, hash)| (*number, *hash))
849 .collect();
850 (backend_accounts, backend_storage, backend_block_hashes)
851}
852
853#[cfg(test)]
854mod tests {
855 use std::{
856 fs,
857 sync::{Arc, Barrier},
858 thread,
859 time::{Duration, Instant},
860 };
861
862 use foundry_fork_db::{BlockchainDb, cache::BlockchainDbMeta};
863
864 use super::capture_backend_maps;
865 #[cfg(unix)]
866 use super::create_unique_temp_file;
867
868 #[test]
869 fn backend_capture_retains_earlier_guards_while_waiting_for_later_maps() {
870 let blockchain_db = Arc::new(BlockchainDb::new(BlockchainDbMeta::default(), None));
871 let storage_guard = blockchain_db.storage().write();
872 let start = Arc::new(Barrier::new(2));
873 let worker_db = Arc::clone(&blockchain_db);
874 let worker_start = Arc::clone(&start);
875 let capture = thread::spawn(move || {
876 worker_start.wait();
877 capture_backend_maps(&worker_db)
878 });
879 start.wait();
880
881 let deadline = Instant::now() + Duration::from_secs(2);
885 let retained_accounts_guard = loop {
886 if blockchain_db.accounts().try_write().is_none() {
887 break true;
888 }
889 if Instant::now() >= deadline {
890 break false;
891 }
892 thread::yield_now();
893 };
894 assert!(
895 retained_accounts_guard,
896 "capture must retain the accounts guard while awaiting storage"
897 );
898
899 drop(storage_guard);
900 capture.join().expect("capture thread");
901 }
902
903 #[cfg(unix)]
904 #[test]
905 fn stale_temp_candidate_is_skipped_without_blocking_checkpoint_progress() {
906 use std::os::unix::fs::PermissionsExt;
907
908 let root = std::env::temp_dir().join(format!(
909 "evm-fork-cache-stale-temp-{}-{}",
910 std::process::id(),
911 super::NEXT_TEMP_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
912 ));
913 fs::create_dir_all(&root).expect("create test directory");
914 let destination = root.join("checkpoint.bin");
915 let stale = root.join(".checkpoint.bin.first");
916 let fresh = root.join(".checkpoint.bin.second");
917 fs::write(&stale, b"stale crash residue").expect("precreate first candidate");
918 let mut candidates = [stale.clone(), fresh.clone()].into_iter();
919
920 let (selected, file) = create_unique_temp_file(&destination, || {
921 candidates.next().expect("bounded test candidates")
922 })
923 .expect("collision must retry with the next candidate");
924 drop(file);
925
926 assert_eq!(selected, fresh);
927 assert_eq!(
928 fs::metadata(&selected)
929 .expect("fresh temp metadata")
930 .permissions()
931 .mode()
932 & 0o777,
933 0o600,
934 "checkpoint temp files contain provider cursors and must be owner-only"
935 );
936 assert_eq!(
937 fs::read(&stale).expect("stale file remains"),
938 b"stale crash residue"
939 );
940 fs::remove_dir_all(root).expect("remove test directory");
941 }
942}
943
944#[cfg(unix)]
945fn atomic_replace(path: &Path, data: &[u8]) -> Result<(), DurableCheckpointError> {
946 let parent = path.parent().unwrap_or_else(|| Path::new("."));
947 fs::create_dir_all(parent).map_err(|source| DurableCheckpointError::CreateDir {
948 path: parent.to_path_buf(),
949 source,
950 })?;
951
952 let (temp_path, mut file) = create_unique_temp_file(path, || next_temp_path(path))?;
953 let result = (|| {
954 file.write_all(data)
955 .and_then(|()| file.sync_all())
956 .map_err(|source| DurableCheckpointError::Write {
957 path: temp_path.clone(),
958 source,
959 })?;
960 fs::rename(&temp_path, path).map_err(|source| DurableCheckpointError::Rename {
961 from: temp_path.clone(),
962 to: path.to_path_buf(),
963 source,
964 })?;
965 sync_parent_directory(parent)?;
966 Ok(())
967 })();
968
969 if result.is_err() {
970 let _ = fs::remove_file(&temp_path);
971 }
972 result
973}
974
975#[cfg(not(unix))]
976fn atomic_replace(path: &Path, _data: &[u8]) -> Result<(), DurableCheckpointError> {
977 Err(DurableCheckpointError::AtomicReplaceUnsupported {
978 path: path.to_path_buf(),
979 })
980}
981
982#[cfg(unix)]
983fn create_unique_temp_file(
984 destination: &Path,
985 mut next_candidate: impl FnMut() -> PathBuf,
986) -> Result<(PathBuf, File), DurableCheckpointError> {
987 use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
988
989 for _ in 0..MAX_TEMP_CREATE_ATTEMPTS {
990 let candidate = next_candidate();
991 match OpenOptions::new()
992 .write(true)
993 .create_new(true)
994 .mode(0o600)
995 .open(&candidate)
996 {
997 Ok(file) => {
998 if let Err(source) = file.set_permissions(fs::Permissions::from_mode(0o600)) {
1002 drop(file);
1003 let _ = fs::remove_file(&candidate);
1004 return Err(DurableCheckpointError::Write {
1005 path: candidate,
1006 source,
1007 });
1008 }
1009 return Ok((candidate, file));
1010 }
1011 Err(source) if source.kind() == io::ErrorKind::AlreadyExists => continue,
1012 Err(source) => {
1013 return Err(DurableCheckpointError::Write {
1014 path: candidate,
1015 source,
1016 });
1017 }
1018 }
1019 }
1020 Err(DurableCheckpointError::TemporaryPathExhausted {
1021 path: destination.to_path_buf(),
1022 attempts: MAX_TEMP_CREATE_ATTEMPTS,
1023 })
1024}
1025
1026#[cfg(unix)]
1027fn sync_parent_directory(parent: &Path) -> Result<(), DurableCheckpointError> {
1028 File::open(parent)
1029 .and_then(|directory| directory.sync_all())
1030 .map_err(|source| DurableCheckpointError::SyncDirectory {
1031 path: parent.to_path_buf(),
1032 source,
1033 })
1034}
1035
1036#[cfg(not(unix))]
1037fn sync_parent_directory(_parent: &Path) -> Result<(), DurableCheckpointError> {
1038 Ok(())
1041}
1042
1043#[cfg(unix)]
1044fn next_temp_path(path: &Path) -> PathBuf {
1045 let id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
1046 let name = path
1047 .file_name()
1048 .and_then(|name| name.to_str())
1049 .unwrap_or("checkpoint");
1050 path.with_file_name(format!(".{name}.tmp-{}-{id}", std::process::id()))
1051}
1052
1053#[derive(Debug, thiserror::Error)]
1055#[non_exhaustive]
1056pub enum DurableCheckpointError {
1057 #[error("atomic durable checkpoint replacement for {path:?} is unsupported on this platform")]
1059 AtomicReplaceUnsupported {
1060 path: PathBuf,
1062 },
1063 #[error(transparent)]
1065 Encode(#[from] crate::errors::PersistenceError),
1066 #[error("durable checkpoint writer task failed: {0}")]
1068 TaskJoin(#[source] tokio::task::JoinError),
1069 #[error("durable checkpoint writer generation exhausted")]
1071 GenerationExhausted,
1072 #[error(
1074 "durable checkpoint write generation {generation} was superseded by generation {latest}"
1075 )]
1076 WriteSuperseded {
1077 generation: u64,
1079 latest: u64,
1081 },
1082 #[error("failed to read durable checkpoint {path:?}: {source}")]
1084 Read {
1085 path: PathBuf,
1087 #[source]
1089 source: io::Error,
1090 },
1091 #[error("durable checkpoint {path:?} has an invalid or unsupported format")]
1093 InvalidFormat {
1094 path: PathBuf,
1096 },
1097 #[error("durable checkpoint {path:?} failed its integrity checksum")]
1099 ChecksumMismatch {
1100 path: PathBuf,
1102 },
1103 #[error(
1105 "durable checkpoint {path:?} is {bytes} bytes, exceeding the configured {max_bytes}-byte limit"
1106 )]
1107 CheckpointTooLarge {
1108 path: PathBuf,
1110 bytes: u64,
1112 max_bytes: u64,
1114 },
1115 #[error("durable checkpoint {path:?} size exceeds supported accounting")]
1117 CheckpointSizeOverflow {
1118 path: PathBuf,
1120 },
1121 #[error("failed to create durable checkpoint directory {path:?}: {source}")]
1123 CreateDir {
1124 path: PathBuf,
1126 #[source]
1128 source: io::Error,
1129 },
1130 #[error("failed to write durable checkpoint {path:?}: {source}")]
1132 Write {
1133 path: PathBuf,
1135 #[source]
1137 source: io::Error,
1138 },
1139 #[error(
1141 "failed to allocate a unique temporary file for durable checkpoint {path:?} after {attempts} attempts"
1142 )]
1143 TemporaryPathExhausted {
1144 path: PathBuf,
1146 attempts: usize,
1148 },
1149 #[error("failed to replace durable checkpoint {to:?} from {from:?}: {source}")]
1151 Rename {
1152 from: PathBuf,
1154 to: PathBuf,
1156 #[source]
1158 source: io::Error,
1159 },
1160 #[error("failed to sync durable checkpoint directory {path:?}: {source}")]
1164 SyncDirectory {
1165 path: PathBuf,
1167 #[source]
1169 source: io::Error,
1170 },
1171 #[error("durable checkpoint identity mismatch: expected {expected:?}, found {actual:?}")]
1173 IdentityMismatch {
1174 expected: DurableCheckpointIdentity,
1176 actual: DurableCheckpointIdentity,
1178 },
1179 #[error(
1181 "durable checkpoint chain {checkpoint_chain_id} does not match cache chain {cache_chain_id}"
1182 )]
1183 CacheChainMismatch {
1184 cache_chain_id: u64,
1186 checkpoint_chain_id: u64,
1188 },
1189}