Skip to content

fix(replay): implement strict final state root verification#3916

Merged
jolestar merged 1 commit into
mainfrom
fix_3902
Jan 8, 2026
Merged

fix(replay): implement strict final state root verification#3916
jolestar merged 1 commit into
mainfrom
fix_3902

Conversation

@jolestar

@jolestar jolestar commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

Overview

This PR fixes issue #3902 by implementing strict verification of the final state root during replay operations.

Problem

Previously, the verify_final_state_root method would always pass if startup info existed, without actually comparing the expected state root (from the last changeset applied) against the actual state root (from the snapshot store's startup info). This meant that replay operations could report success even when the final state didn't match expectations.

Solution

Changes Made

  1. Modified replay_changesets_batched to track and return expected state root

    • Now returns the state root from the last changeset applied
    • This represents the expected final state after replay
  2. Updated verify_final_state_root to accept and compare expected state root

    • Takes an additional parameter: expected_state_root: H256
    • Compares expected vs actual state root from startup info
    • Returns Err with detailed error message on mismatch
    • Sets report.verification_passed based on actual comparison
    • Stores the actual state root in report.final_state_root
  3. Updated main replay_changesets method

    • Captures the expected state root from replay_changesets_batched
    • Passes it to verify_final_state_root
    • Marks metadata as failed when verification fails
  4. Added comprehensive tests

    • test_verify_final_state_root_success: Tests matching state roots pass
    • test_verify_final_state_root_failure: Tests mismatching state roots fail

Acceptance Criteria Met

✅ Mismatch causes ReplayReport failure + metadata status Failed

  • On mismatch, error is added to report
  • report.verification_passed is set to false
  • Metadata is marked as failed with detailed error message
  • report.is_success() returns false

✅ Matching root passes

  • When state roots match, verification succeeds
  • report.verification_passed is set to true
  • No errors are added to the report

✅ Tests cover both match and mismatch cases

  • Two new tests verify both success and failure scenarios

Testing

  • All 106 tests in rooch-pruner pass
  • make lint passes (cargo fmt, clippy)
  • Binary compiles successfully

Files Changed

  • crates/rooch-pruner/src/state_prune/incremental_replayer.rs
    • Added expected state root tracking and return
    • Implemented strict verification logic
    • Added comprehensive tests

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings January 7, 2026 10:38
@jolestar jolestar requested a review from baichuan3 as a code owner January 7, 2026 10:38
@vercel

vercel Bot commented Jan 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
rooch-portal-v2.1 Ready Ready Preview, Comment Jan 7, 2026 5:39pm
test-portal Ready Ready Preview, Comment Jan 7, 2026 5:39pm
1 Skipped Deployment
Project Deployment Review Updated (UTC)
rooch Ignored Ignored Preview Jan 7, 2026 5:39pm

@github-actions

github-actions Bot commented Jan 7, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR implements strict verification of the final state root during replay operations to address issue #3902. Previously, the verification would always pass if startup info existed, without actually comparing expected vs actual state roots.

Key Changes:

  • Modified replay_changesets_batched to track and return the expected state root from the last changeset applied
  • Updated verify_final_state_root to accept the expected state root and perform strict comparison against the actual state root from startup info
  • Enhanced error handling to mark metadata as failed when verification fails
Comments suppressed due to low confidence (1)

crates/rooch-pruner/src/state_prune/incremental_replayer.rs:376

  • The replay process writes new SMT nodes but never updates the startup info with the new state root. After applying all changesets, the startup info should be updated to reflect the final state root. Without this, the verification will always compare against the stale initial state root instead of the actual final state. Consider updating the startup info after successfully applying all changesets, similar to what's done in import_state.rs (lines 165-174).
    /// Apply a batch of changesets
    fn apply_changeset_batch(
        &self,
        batch: &[(u64, StateChangeSetExt)],
        snapshot_store: &MoveOSStore,
        report: &mut ReplayReport,
    ) -> Result<()> {
        let mut all_nodes = BTreeMap::new();
        let mut total_objects_created = 0u64;
        let mut total_objects_updated = 0u64;

        // Process each changeset in the batch
        for (tx_order, changeset_ext) in batch {
            let mut changeset = changeset_ext.state_change_set.clone();

            // Convert changeset to SMT nodes using existing API
            let (nodes, _stale_indices) = snapshot_store
                .get_state_store()
                .change_set_to_nodes(&mut changeset)
                .map_err(|e| {
                    anyhow::anyhow!("Failed to convert changeset {} to nodes: {}", tx_order, e)
                })?;

            // Accumulate nodes for batch write
            all_nodes.extend(nodes);

            // Count object changes
            total_objects_created += self.count_objects_created(changeset_ext);
            total_objects_updated += self.count_objects_updated(changeset_ext);
        }

        // Batch write all nodes atomically
        let nodes_count = all_nodes.len();
        if !all_nodes.is_empty() {
            snapshot_store
                .get_state_node_store()
                .write_nodes(all_nodes)
                .map_err(|e| anyhow::anyhow!("Failed to write {} nodes: {}", nodes_count, e))?;
        }

        // Update report statistics
        report.nodes_updated += nodes_count as u64;

        info!(
            "Applied batch: {} changesets, {} nodes, {} objects created, {} objects updated",
            batch.len(),
            nodes_count,
            total_objects_created,
            total_objects_updated
        );

        Ok(())
    }

Comment on lines +311 to +314
let expected_state_root = changesets
.last()
.map(|(_, changeset)| changeset.state_change_set.state_root)
.unwrap_or_else(H256::zero);

Copilot AI Jan 7, 2026

Copy link

Choose a reason for hiding this comment

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

When the changesets vector is empty, this code returns H256::zero() as the expected state root. However, this may not be the correct behavior. If no changesets were applied, the expected state root should be the initial state root from the snapshot, not zero. Consider handling the empty changeset case explicitly by either returning an error or using the initial state root from the snapshot.

Copilot uses AI. Check for mistakes.
This commit fixes issue #3902 by implementing strict verification of the
final state root during replay operations.

Changes:
- Modified `replay_changesets_batched` to return the expected final state
  root from the last changeset applied
- Updated `verify_final_state_root` to accept and compare the expected
  state root against the actual state root from startup info
- Verification now fails with detailed error message when state roots don't match
- The replay report's `verification_passed` flag is set based on actual comparison
- Metadata is marked as failed when verification fails in the main replay method
- Added comprehensive tests for both matching and mismatching state root cases

Acceptance criteria met:
- Mismatch causes ReplayReport failure + metadata status Failed
- Matching root passes verification
- Tests cover both match and mismatch cases

All 106 tests pass, linting passes, binary builds successfully.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@jolestar jolestar merged commit c51152b into main Jan 8, 2026
28 checks passed
@jolestar jolestar deleted the fix_3902 branch January 8, 2026 00:16
@jolestar jolestar restored the fix_3902 branch January 19, 2026 10:28
@jolestar jolestar deleted the fix_3902 branch January 19, 2026 10:29
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