diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/v0/mod.rs index f16198bc266..79d2034f99c 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/v0/mod.rs @@ -53,6 +53,16 @@ where let block_time = block_info.block_time_ms(); let block_height = block_info.height(); + // Checkpoints are restore points for a running node. Replaying history, + // ten minutes of chain time is a handful of blocks, so this fires dozens + // of times a second and all but the last `keep_n` are deleted again + // immediately — each one a RocksDB checkpoint over the whole database + // plus a copy of the platform state. The node writes its first real + // checkpoint once it reaches the tip. + if crate::utils::is_historical_block(block_time) { + return Ok(None); + } + let most_recent_checkpoint_interval_time = block_time - block_time % checkpoint_interval_milliseconds; @@ -95,6 +105,13 @@ mod tests { use dpp::version::PlatformVersion; use std::collections::BTreeMap; + fn now_ms() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock is before the unix epoch") + .as_millis() as u64 + } + fn make_block_execution_context(height: u64, block_time_ms: u64) -> BlockExecutionContext { let platform_version = PlatformVersion::latest(); let platform_state = @@ -158,7 +175,10 @@ mod tests { return; } - let block_execution_context = make_block_execution_context(1, 1_000_000); + // A block the network has just produced: checkpoints are restore points + // for a running node, so the age of the block decides whether one is worth + // taking, and a fixed fixture timestamp would read as ancient history. + let block_execution_context = make_block_execution_context(1, now_ms()); let result = platform .should_checkpoint_v0(&block_execution_context, platform_version) .expect("expected Ok"); @@ -167,6 +187,38 @@ mod tests { assert!(result.is_some(), "first block should trigger checkpoint"); } + /// Replaying history, ten minutes of chain time is a handful of blocks, so a + /// checkpoint would be taken dozens of times a second and all but the last + /// few deleted again immediately. A node catching up takes none. + #[test] + fn test_historical_block_does_not_checkpoint() { + let platform_version = PlatformVersion::latest(); + if platform_version + .drive_abci + .methods + .block_end + .should_checkpoint + .is_none() + { + return; + } + + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let block_execution_context = + make_block_execution_context(1, now_ms() - 24 * 60 * 60 * 1000); + let result = platform + .should_checkpoint_v0(&block_execution_context, platform_version) + .expect("expected Ok"); + + assert!( + result.is_none(), + "a day-old block is being replayed, not followed" + ); + } + #[test] fn test_checkpoint_interval_zero_returns_none() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive-abci/src/utils/mod.rs b/packages/rs-drive-abci/src/utils/mod.rs index b7292f50cff..3ca96761bdb 100644 --- a/packages/rs-drive-abci/src/utils/mod.rs +++ b/packages/rs-drive-abci/src/utils/mod.rs @@ -1,6 +1,8 @@ +mod replay; mod serialization; mod spawn; +pub use replay::is_historical_block; pub use serialization::from_opt_str_or_number; pub use serialization::from_str_or_number; pub use spawn::spawn_blocking_task_with_name_if_supported; diff --git a/packages/rs-drive-abci/src/utils/replay.rs b/packages/rs-drive-abci/src/utils/replay.rs new file mode 100644 index 00000000000..47b89a3d96d --- /dev/null +++ b/packages/rs-drive-abci/src/utils/replay.rs @@ -0,0 +1,55 @@ +//! Telling a node that is replaying history from one that is following the tip. +//! +//! Some per-block work only earns its cost at the tip. Creating a GroveDB +//! checkpoint every ten minutes of chain time is useful on a running node and +//! pure waste while catching up, where ten minutes of chain time is a handful of +//! blocks and every checkpoint but the last few is deleted within the second. + +/// A block older than this is not one the network just produced. Mainnet aims at +/// about 2.5 minutes a block, so this leaves several blocks of slack for a node +/// that is merely a little behind. +const HISTORICAL_BLOCK_AGE_MS: u64 = 10 * 60 * 1000; + +/// True when a block with this timestamp is old enough that the node producing +/// it is clearly replaying history rather than following the tip. +pub fn is_historical_block(block_time_ms: u64) -> bool { + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|since_epoch| since_epoch.as_millis() as u64) + .unwrap_or(0); + now_ms.saturating_sub(block_time_ms) > HISTORICAL_BLOCK_AGE_MS +} + +#[cfg(test)] +mod tests { + use super::*; + + fn now_ms() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock is before the unix epoch") + .as_millis() as u64 + } + + #[test] + fn a_block_from_a_year_ago_is_historical() { + assert!(is_historical_block( + now_ms() - 365 * 24 * 60 * 60 * 1000 + )); + } + + #[test] + fn a_block_from_a_minute_ago_is_not_historical() { + assert!(!is_historical_block(now_ms() - 60 * 1000)); + } + + #[test] + fn a_block_at_the_threshold_is_not_yet_historical() { + assert!(!is_historical_block(now_ms() - HISTORICAL_BLOCK_AGE_MS)); + } + + #[test] + fn a_block_timestamped_in_the_future_is_not_historical() { + assert!(!is_historical_block(now_ms() + 60 * 1000)); + } +}