Skip to content

perf(drive-abci): don't create GroveDB checkpoints while replaying history - #4570

Open
PastaPastaPasta wants to merge 2 commits into
v4.2-devfrom
perf/checkpoint-skip-during-replay
Open

perf(drive-abci): don't create GroveDB checkpoints while replaying history#4570
PastaPastaPasta wants to merge 2 commits into
v4.2-devfrom
perf/checkpoint-skip-during-replay

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Sep 1, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

A node replaying mainnet history creates and destroys tens of thousands of GroveDB checkpoints.

Protocol version 11 (mainnet height 318,704) turns checkpoints on:

should_checkpoint: Some(0),
update_checkpoints: Some(0),

with frequency_seconds: 600, num_checkpoints: 3. The interval is in chain time, and mainnet blocks are ~2.5 minutes apart, so that is a checkpoint every four blocks. At replay speed that is roughly 37 a second, each one a RocksDB checkpoint over the whole database — hard-linking every SST, flushing and copying the WAL — plus a copy of the platform state, and all but the last three deleted again immediately.

Measured with per-block phase timing, replaying mainnet:

fb_checkpoint = 15,122 µs/block

against roughly 7,000 µs for everything else in a block put together. A finished mainnet sync left four checkpoint directories on disk out of some 26,000 created:

drive-db/checkpoints:  369259  424972  424976  424979   (100M)

The last three are four blocks apart, which is the cadence.

It shows up as an I/O stall rather than CPU: during the affected range drive-abci sat at 0.38 of one core with the system 47–73% idle and the disk at 2,000–4,000 tps.

What was done?

Skip checkpoint creation for blocks more than ten minutes old. Checkpoints are restore points for a running node; a node catching up has no use for restore points into blocks it is about to replace, and it writes its first real checkpoint on reaching the tip.

The age test is a new utils::is_historical_block, with unit tests — the predicate is the testable part, since a historical replay makes every block historical by construction.

Checkpoints live outside the tree, so no app hash changes.

How Has This Been Tested?

Same 3,000-block window from mainnet height 331,648, two local peers, back to back:

blocks/s ms/block fb_checkpoint
before 47.6 21.01 12,136 µs
after 121.6 8.23 0

−60.8% per block. The 47.6 blocks/s reproduces what a full sync does over that range.

Full mainnet replay, genesis to 424,981, with this change on top of an otherwise identical build: 4,684.9 s → 3,032.9 s, 35% of the whole sync. Every committed app hash matched a reference sync across all 424,971 heights.

cargo test -p drive-abci --lib should_checkpoint utils::replay — 11 passed.

One existing test needed updating: test_first_block_should_always_checkpoint built its context with block_time_ms = 1_000_000, which is 1970 and now reads as history. It uses a current timestamp, and a companion test covers the new behaviour for an old block.

Breaking Changes

None for a node that is running. Once it reaches the tip it checkpoints exactly as it does today.

A node still catching up keeps no checkpoints, where before it kept three covering the last thirty minutes of chain time. Nothing during sync depends on them: a node that is interrupted picks up where it left off and carries on by itself, with no operator action, the same as it does today.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

…story

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.
@thepastaclaw

thepastaclaw commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

🕓 Queued for automated review — 33rd in line, estimated start in ~55 h (commit 9dfffa6)
Estimated review time once started: ~3 h (two-phase automated review; median of recent runs).

  • Request priority review — tick this box and the review moves to the front of the queue.

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.16667% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.59%. Comparing base (7c77247) to head (9dfffa6).
⚠️ Report is 67 commits behind head on v4.2-dev.

Files with missing lines Patch % Lines
...tform_events/block_end/should_checkpoint/v0/mod.rs 64.28% 15 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           v4.2-dev    #4570      +/-   ##
============================================
- Coverage     87.68%   85.59%   -2.09%     
============================================
  Files          2778     2792      +14     
  Lines        359190   369992   +10802     
============================================
+ Hits         314960   316705    +1745     
- Misses        44230    53287    +9057     
Components Coverage Δ
dpp 86.92% <ø> (-2.13%) ⬇️
drive 84.15% <ø> (-2.48%) ⬇️
drive-abci 88.71% <79.16%> (-1.17%) ⬇️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.92% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 42.02% <ø> (-6.63%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 22 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: b974c0f3-de40-44a5-95dd-7c6e26f6eb68

📥 Commits

Reviewing files that changed from the base of the PR and between c0e9a86 and 9dfffa6.

📒 Files selected for processing (6)
  • packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/v0/mod.rs
  • packages/rs-drive-abci/src/utils/mod.rs
  • packages/rs-drive-abci/src/utils/replay.rs
  • packages/rs-drive-abci/tests/strategy_tests/execution.rs
  • packages/rs-drive-abci/tests/strategy_tests/strategy.rs
  • packages/rs-drive-abci/tests/strategy_tests/test_cases/address_tests.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final validation — GLM Flash + Sol

The checkpoint gate avoids replay-time I/O without changing authenticated state, but it confuses an old block timestamp with replay status. A fresh node that reaches a tip older than ten minutes can finish synchronization with no checkpoint, disabling checkpoint-backed address synchronization until another block arrives, so changes are required. The boundary test is also reproducibly flaky, and two smaller API and naming issues remain.

Source: reviewer 1: glm-5.3-flash (agent: phase1-reviewer, role: general); reviewer 2: glm-5.3-flash (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-5.6-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-5.6-sol (agent: phase2-reviewer, role: rust-quality); final verifier: gpt-5.6-sol (agent: sol-verifier, role: final-verifier)

Review provenance

  • Phase 1 reviewers (GLM Flash): glm-5.3-flash — general (completed); agent phase1-reviewer, glm-5.3-flash — rust-quality (completed); agent phase1-reviewer
  • Fresh verifier (Sol): gpt-5.6-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers (Sol): gpt-5.6-sol — general (completed); agent phase2-reviewer, gpt-5.6-sol — rust-quality (completed); agent phase2-reviewer

🔴 1 blocking | 🟡 2 suggestion(s) | 💬 1 nitpick(s)

1 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/v0/mod.rs`:
- [BLOCKING] packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/v0/mod.rs:62-63: A stale chain tip can leave a synchronized node with no checkpoint
  Block age is not equivalent to replay status. If a fresh node catches up while the network tip is more than ten minutes old, such as during a consensus halt, this branch skips every checkpoint including the actual tip. No additional block-finalization callback runs when catch-up completes, so the promised first real checkpoint is never created until the network produces another block. This leaves a fully synchronized node unable to serve address full-tree synchronization: `prove_address_funds_trunk_query_v0` explicitly selects `GroveDBToUse::LatestCheckpoint`, whose GroveDB query returns `NoCheckpointsAvailable` when the registry is empty. Use an actual catch-up/tip signal or otherwise ensure completion creates a checkpoint instead of inferring synchronization state solely from the block timestamp.
- [NITPICK] packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/v0/mod.rs:151: Test name `test_first_block_should_always_checkpoint` no longer describes what it tests
  The new age gate means a height-one historical block does not always checkpoint, as the companion test demonstrates. This test now verifies that a recent block with an empty checkpoint registry triggers a checkpoint, so its name should state those conditions rather than claim unconditional behavior.

In `packages/rs-drive-abci/src/utils/replay.rs`:
- [SUGGESTION] packages/rs-drive-abci/src/utils/replay.rs:13-55: Exact-threshold test races against the system clock
  The test computes its block timestamp from one `SystemTime::now()` call, while `is_historical_block` immediately samples the clock again. Crossing a millisecond boundary makes the measured age exceed `HISTORICAL_BLOCK_AGE_MS`, reversing the assertion. This was reproduced on the exact head, where the targeted test failed immediately. Extract the comparison into a helper with an injected current time so the boundary semantics are deterministic.

In `packages/rs-drive-abci/src/utils/mod.rs`:
- [SUGGESTION] packages/rs-drive-abci/src/utils/mod.rs:5: Keep the replay heuristic out of the crate's public API
  `is_historical_block` has only one caller, the internal checkpoint implementation, but this public re-export exposes the ten-minute operational policy as `drive_abci::utils::is_historical_block` to downstream crates. That creates an unnecessary compatibility surface around an implementation detail. A crate-private re-export retains the required internal access.

Comment on lines +62 to +63
if crate::utils::is_historical_block(block_time) {
return Ok(None);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: A stale chain tip can leave a synchronized node with no checkpoint

Block age is not equivalent to replay status. If a fresh node catches up while the network tip is more than ten minutes old, such as during a consensus halt, this branch skips every checkpoint including the actual tip. No additional block-finalization callback runs when catch-up completes, so the promised first real checkpoint is never created until the network produces another block. This leaves a fully synchronized node unable to serve address full-tree synchronization: prove_address_funds_trunk_query_v0 explicitly selects GroveDBToUse::LatestCheckpoint, whose GroveDB query returns NoCheckpointsAvailable when the registry is empty. Use an actual catch-up/tip signal or otherwise ensure completion creates a checkpoint instead of inferring synchronization state solely from the block timestamp.

source: ['claude']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed in code. The limitation is documented in the replay.rs module doc on head 9dfffa61 (lines 8-11): block age is a proxy for catching up, and a node that finishes syncing during a network halt will treat the tip as historical and skip the checkpoint until the next block arrives. Whether that trade-off is acceptable needs maintainer acceptance, so this thread is left open.


🤖 Posted autonomously by Claude on behalf of pasta.

Comment thread packages/rs-drive-abci/src/utils/replay.rs
Comment thread packages/rs-drive-abci/src/utils/mod.rs Outdated

@PastaPastaPasta PastaPastaPasta left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Verdict: merge after changes. The idea is right and the measured win is large, but the branch as pushed fails cargo fmt --check, has a flaky unit test, and breaks an existing chain-simulation test that CI only runs after merge.

1. Correctness

Skipping checkpoints for blocks more than ten minutes old is safe for consensus: checkpoints live outside the tree and the app hash is unchanged, which the full replay confirms. A node interrupted mid-sync resumes without a checkpoint today as well.

Three problems I reproduced locally on 302f7eff:

  1. cargo fmt --check fails on utils/replay.rs. This is why "Rust workspace tests / Tests" is red: the job stops at the fmt step, so no Rust test has run in CI for this PR. (#4571 quietly carries the fmt fix, which is a sign the two branches got out of order.)
  2. a_block_at_the_threshold_is_not_yet_historical is flaky. The test reads the clock, then is_historical_block reads it again; a millisecond tick in between flips the result. It failed on my first local run. Split the comparison into a pure function that takes now_ms and test that.
  3. run_chain_address_transitions_with_checkpoints_stop_and_restart now fails (address_tests.rs:1007, "expected at least one checkpoint to be created"). The strategy harness starts every chain at GENESIS_TIME_MS (April 2023), so with this change every simulated block is historical and no checkpoint is ever taken. The PR run does not catch it because CI runs strategy_tests in full only on push and nightly. Merging as is would turn v4.2-dev red. The test needs a chain that starts near the wall clock.

One design limitation worth stating in the code and the PR body: block age is a proxy for "catching up". If the network tip itself is more than ten minutes old (a chain halt), a node that finishes syncing during the halt takes no checkpoint until the next block arrives, so address trunk queries return NoCheckpointsAvailable on that node until then. I think that is acceptable, since the whole network is degraded at that point, but it should be written down rather than discovered.

2. Clarity

The description is excellent: it names the activation height, the cadence, the I/O symptom, and the before/after numbers. The comments in should_checkpoint and replay.rs say why, not what.

3. Codebase standards

is_historical_block is re-exported pub from a pub mod utils, so a ten-minute internal policy becomes crate API. pub(crate) is enough (#4571 is the only other user and is in-crate). Otherwise idiomatic.

4. Importance and alternatives

35% of a full mainnet sync and a 60% per-block win in the affected range. The alternative is a real "am I caught up" signal from Tenderdash, which ABCI does not carry, so a timestamp heuristic is what is available. Keeping the predicate in one small pure function is the right shape.

5. Existing bot findings

  • thepastaclaw "stale chain tip leaves no checkpoint": valid as a limitation, not a blocker. See above.
  • thepastaclaw "threshold test races the clock": confirmed; will fix as suggested.
  • thepastaclaw pub(crate): agree.

I will push: fmt, the pure is_historical_block_at split with deterministic tests, pub(crate), the strategy test fix, and the limitation note. #4571 will need a rebase afterwards.


🤖 Posted autonomously by Claude on behalf of pasta.

…eplay skip

The chain simulations start at a fixed 2023 timestamp, so with checkpoints skipped for historical blocks none of the seven checkpoint-enabled tests ever took one, and CI only runs them after merge. Give NetworkStrategy a start_time_ms and start those chains near the wall clock, at the same phase within the ten-minute interval so the asserted checkpoint heights are unchanged.

Also: format replay.rs, test the age threshold against an injected clock instead of racing SystemTime, make is_historical_block crate-private, and write down the chain-halt limitation of the age heuristic.
@PastaPastaPasta

Copy link
Copy Markdown
Member Author

Pushed 9dfffa6 on top of the original commit:

  • cargo fmt on replay.rs, which is what was failing "Rust workspace tests / Tests" before any test ran. The Rust tests are green now.
  • is_historical_block split into a clock-reading wrapper and a pure is_historical_block_at(block_time_ms, now_ms); the unit tests use a fixed now, so the threshold test cannot race the clock any more. Added the one-past-threshold case.
  • is_historical_block is pub(crate).
  • The seven chain simulations that enable checkpoints (all in address_tests.rs) now start their chain near the wall clock. NetworkStrategy gained start_time_ms (default unchanged, GENESIS_TIME_MS), and those tests set it to the most recent instant with the same phase inside the ten-minute checkpoint interval as the fixed genesis time, so the same heights checkpoint as before and every existing height assertion holds. All seven pass locally; before this they all failed with "expected at least one checkpoint to be created", and CI only runs them after merge.
  • The chain-halt limitation of the age heuristic is written down in the replay.rs module doc.

cargo test -p drive-abci --lib -- should_checkpoint utils::replay update_checkpoints: 14 passed. Checkpoint-enabled strategy tests: 7 passed. fmt and clippy clean.

codecov/project is red with a 2.1 point drop that this diff cannot have caused (carryforward gap on the push-only flags); not a required check.

#4571 has been rebased onto this branch's new head.

Ready for human review.


🤖 Posted autonomously by Claude on behalf of pasta.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants