From 302f7effdf7c3d9220d4fd1d391c543a42ec04b0 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 31 Aug 2026 17:08:13 +0200 Subject: [PATCH] perf(drive-abci): don't create GroveDB checkpoints while replaying history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Protocol version 11 turns checkpoints on, with a policy of one every 600 seconds of chain time keeping the last 3. Mainnet blocks are about 2.5 minutes apart, so that is a checkpoint every four blocks — which at replay speed is roughly 37 a second, each one a RocksDB checkpoint over the whole database plus a copy of the platform state, and all but the last three deleted again immediately. Measured at 15.1 ms a block from height 318,704, against about 7 ms for everything else in a block put together. A finished mainnet sync had four checkpoint directories left on disk out of some 26,000 created. Checkpoints are restore points for a running node, so skip them for blocks more than ten minutes old and write the first real one on reaching the tip. They live outside the tree, so no app hash changes. --- .../block_end/should_checkpoint/v0/mod.rs | 54 +++++++++++++++++- packages/rs-drive-abci/src/utils/mod.rs | 2 + packages/rs-drive-abci/src/utils/replay.rs | 55 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 packages/rs-drive-abci/src/utils/replay.rs 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)); + } +}