Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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");
Expand All @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions packages/rs-drive-abci/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
55 changes: 55 additions & 0 deletions packages/rs-drive-abci/src/utils/replay.rs
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading