Skip to content

fix(drive-abci): execute a different block at the same height/round instead of serving the stale context - #4462

Open
QuantumExplorer wants to merge 4 commits into
v4.2-devfrom
fix/drive-abci-process-proposal-different-block
Open

fix(drive-abci): execute a different block at the same height/round instead of serving the stale context#4462
QuantumExplorer wants to merge 4 commits into
v4.2-devfrom
fix/drive-abci-process-proposal-different-block

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 23, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

App-side half of dashpay/tenderdash#1413.

process_proposal keeps its block execution context keyed by height and round only. When a second, different block arrives for the same height and round, it does one of two wrong things:

  • proposer path (block_hash set, proposer_results cached): it returns the cached result for any request hash — i.e. the app hash of our own proposal is served for the network's block;
  • otherwise: it returns Err(BadRequest("received a process proposal request twice with different hash")), which reaches Tenderdash as an ABCI exception.

Tenderdash turns the first into a mustValidate panic (wrong Block.Header.AppHash …) and the second into a mustEnsureProcess panic, and its WAL replay re-creates the same situation on every restart, so the node never comes back without discarding the Tenderdash data.

This is exactly what happens to a validator that hands over from block sync to consensus while still behind: it proposes at a historical height (it is in that era's quorum), and the block the network really committed at that height and round then reaches it through consensus catch-up — same height, same round, different block. Observed on a mainnet evonode re-syncing from scratch (stall at 24174, own proposal at h=24175 r=1, the collision at 24176 where it was the genuine round-0 proposer).

What was done?

  • process_proposal: key the cache by block hash. Same hash → unchanged (proposer cache served; non-proposer re-run with the existing warning). Different hash at the same height/round → log a warning, drop the stale context, and execute the block we were asked about. Never serve the other block's result, never error.
  • New strategy tests test_cases/process_proposal_collision_tests.rs:
    • process_proposal_for_a_different_block_must_not_be_served_from_the_proposer_cache — PrepareProposal + ProcessProposal of our own block at (H, 0), then ProcessProposal of a different block at (H, 0): the answer must be that block's app hash (checked against a clean execution of the same request). Before the fix: Accept with our own block's app hash.
    • process_proposal_for_a_second_different_block_in_the_same_round_must_execute — two different blocks at (H, 0), neither ours: the second must execute. Before the fix: the ABCI BadRequest above.

With this change the collision is harmless: the genuine block executes and commits and the node keeps catching up. The Tenderdash side (not proposing while behind, not evicting the catch-up peer) is tracked in dashpay/tenderdash#1413 and dashpay/tenderdash#1414.

How Has This Been Tested?

  • cargo test -p drive-abci --test strategy_tests process_proposal_collision — both tests red on the base, green with the fix.
  • cargo test -p drive-abci --test strategy_tests — 95 passed, 0 failed, 4 ignored (pre-existing).
  • cargo test -p drive-abci --lib abci::handler — 52 passed.
  • cargo clippy -p drive-abci --tests --features mocks clean; cargo fmt --all -- --check clean.

Breaking Changes

None. Valid flows (same block re-sent, next round, next height) are unchanged; only the "different block at the same height/round" case changes, from a wrong answer / error to execution.

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 added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved proposal processing when different blocks arrive at the same height and round.
    • Prevented stale cached results from blocking execution of a new proposal.
    • Continued returning cached results safely when the same block is submitted again.
  • Tests

    • Added regression coverage confirming competing proposals are executed independently, accepted, and assigned distinct application hashes.

…nstead of serving the stale context

process_proposal keeps its block execution context keyed by height and
round only. When a second, different block arrived for the same height
and round it either answered with the cached result of the block it held
(proposer path: the app hash of our own proposal was returned for the
network's block) or refused with an ABCI error. Tenderdash turns the
first into a mustValidate panic and the second into a mustEnsureProcess
panic, and its WAL replay recreates the same situation on every restart.

This is exactly what happens to a validator that hands over from block
sync to consensus while still behind: it proposes at a historical height,
and the block the network really committed at that height and round then
arrives through consensus catch-up (dashpay/tenderdash#1413).

Key the cache by block hash: the same block is still served from the
proposer cache / re-run as before; a different block at the same height
and round drops the stale context and is executed. Two strategy tests pin
both behaviours.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thepastaclaw

thepastaclaw commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit a5cab13)

@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

Next included review available in 54 minutes.

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: Pro Plus

Run ID: e1663c1a-df40-49bd-8456-879f80859d2f

📥 Commits

Reviewing files that changed from the base of the PR and between 67c5395 and a5cab13.

📒 Files selected for processing (6)
  • .github/workflows/tests-rs-workspace.yml
  • packages/rs-drive-abci/src/abci/handler/prepare_proposal.rs
  • packages/rs-drive-abci/src/abci/handler/process_proposal.rs
  • packages/rs-drive-abci/src/execution/types/block_execution_context/mod.rs
  • packages/rs-drive-abci/src/execution/types/block_execution_context/v0/mod.rs
  • packages/rs-drive-abci/tests/strategy_tests/test_cases/process_proposal_collision_tests.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ed13aba-3128-485f-93a9-3d872f872727

📥 Commits

Reviewing files that changed from the base of the PR and between fd8d8d1 and 67c5395.

📒 Files selected for processing (3)
  • packages/rs-drive-abci/src/abci/handler/process_proposal.rs
  • packages/rs-drive-abci/tests/strategy_tests/test_cases/mod.rs
  • packages/rs-drive-abci/tests/strategy_tests/test_cases/process_proposal_collision_tests.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The proposal handler now compares block hashes before using cached proposer results. Matching hashes reuse cached results. Different hashes discard the existing execution context and execute the requested block. New strategy tests cover both collision cases.

Changes

Proposal collision handling

Layer / File(s) Summary
Hash-aware proposal execution
packages/rs-drive-abci/src/abci/handler/process_proposal.rs
The handler returns cached proposer results for matching hashes. For different hashes, it logs a warning, drops the existing block execution context, and executes the requested block.
Collision regression coverage
packages/rs-drive-abci/tests/strategy_tests/test_cases/process_proposal_collision_tests.rs, packages/rs-drive-abci/tests/strategy_tests/test_cases/mod.rs
The strategy suite builds competing proposals and verifies that each proposal is accepted and receives an independently computed app hash.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to 67c53

The change makes process-proposal handling execute a different block received at the same height and round instead of reusing stale results or returning an exception; targeted tests and broader checks are reported passing, so no actionable merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant Tenderdash
  participant process_proposal
  participant BlockExecutionContext
  Tenderdash->>process_proposal: Submit proposal at height and round
  process_proposal->>BlockExecutionContext: Compare incoming hash with cached hash
  alt Hash matches
    BlockExecutionContext-->>process_proposal: Return cached proposer results
    process_proposal-->>Tenderdash: Accept with cached result
  else Hash differs
    process_proposal->>BlockExecutionContext: Drop stale execution context
    process_proposal->>BlockExecutionContext: Execute requested proposal
    BlockExecutionContext-->>process_proposal: Return independent app hash
    process_proposal-->>Tenderdash: Accept with new result
  end
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: executing a different block at the same height and round instead of using stale context.
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/drive-abci-process-proposal-different-block

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.

@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.82474% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.01%. Comparing base (fd8d8d1) to head (a5cab13).
⚠️ Report is 30 commits behind head on v4.2-dev.

Files with missing lines Patch % Lines
...rs-drive-abci/src/abci/handler/process_proposal.rs 60.97% 32 Missing ⚠️
.../execution/types/block_execution_context/v0/mod.rs 0.00% 4 Missing ⚠️
...src/execution/types/block_execution_context/mod.rs 50.00% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           v4.2-dev    #4462      +/-   ##
============================================
- Coverage     87.35%   87.01%   -0.34%     
============================================
  Files          2698     2735      +37     
  Lines        344653   349535    +4882     
============================================
+ Hits         301074   304163    +3089     
- Misses        43579    45372    +1793     
Components Coverage Δ
dpp 88.85% <ø> (-0.05%) ⬇️
drive 85.88% <ø> (-0.41%) ⬇️
drive-abci 89.25% <60.82%> (+0.17%) ⬆️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.92% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 47.14% <ø> (-0.27%) ⬇️
🚀 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.

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

Preliminary review — Codex only

The new hash comparison correctly handles collisions after a block hash has been recorded, but the immediately post-PrepareProposal cache remains unbound and can still return a stale app hash for a different block at the same height and round. The regression test establishes the local block hash before submitting the competing block, so it does not cover this remaining path.
Source: reviewer evidence from codex-general, codex-security-auditor, and codex-rust-quality (exact backend model IDs were not included in the supplied projection); final verifier is Anthropic Claude Agent SDK (exact model ID not exposed); orchestration-only and not reviewer evidence: openclaw-agent/cliproxy/gpt-5.6-sol.

Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — rust-quality (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking | 🟡 1 suggestion(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/abci/handler/process_proposal.rs`:
- [BLOCKING] packages/rs-drive-abci/src/abci/handler/process_proposal.rs:109-149: Hashless proposer cache can still answer a different block
  `PrepareProposal` stores `proposer_results` while leaving `block_hash` as `None`. The first `ProcessProposal` request at that round is therefore accepted from this cache whenever only the retained transaction count and Core chain-lock update match. The code does not compare the transaction bytes or other execution inputs such as block time, proposer, Core height, quorum, and app version. A competing empty block can consequently receive the prepared block's app hash, after which its own hash is attached to the stale context. If the two blocks execute differently, Tenderdash receives an app hash that does not belong to the requested block and can enter the same persistent failure this PR is intended to prevent. The new test first processes the local proposal through `mimic_execute_block`, which populates `block_hash`, so it bypasses this branch. Before reusing a hashless proposer result, compare the complete execution-relevant proposal identity, including exact transaction bytes; otherwise discard the context and execute the incoming request.

In `packages/rs-drive-abci/tests/strategy_tests/test_cases/process_proposal_collision_tests.rs`:
- [SUGGESTION] packages/rs-drive-abci/tests/strategy_tests/test_cases/process_proposal_collision_tests.rs:96-98: Convert milliseconds to nanoseconds with the correct scale
  `Timestamp.nanos` is expressed in nanoseconds, but the millisecond remainder is multiplied by 1,000 instead of 1,000,000. The current test timestamps have no fractional-second remainder, so the existing assertions are unaffected, but a future fractional offset would silently lose most or all of that offset and could weaken timing-sensitive proposal tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

Preliminary review — Codex only

The hash-aware branch now correctly discards a stale execution context and executes a different block at the same height and round. However, the hashless context left immediately after PrepareProposal is still matched using only transaction count and the Core chain-lock update, so the first competing ProcessProposal can receive the prepared block's stale app hash; this leaves the PR's core failure mode unresolved.
Source: Codex general, security-auditor, and rust-quality reviewer backend: gpt-5.6-sol; final verifier backend: Anthropic Claude Agent SDK (exact model ID not exposed). Orchestration-only and not reviewer evidence: openclaw-agent/cliproxy/gpt-5.6-sol.

Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — rust-quality (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking

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/abci/handler/process_proposal.rs`:
- [BLOCKING] packages/rs-drive-abci/src/abci/handler/process_proposal.rs:109-149: Hashless proposer cache can still answer a different block
  `prepare_proposal` executes the local candidate with `block_hash: None` and stores its response in `proposer_results`. On the first `ProcessProposal`, this branch reuses that response when only the retained transaction count and `core_chain_lock_update` match; it does not compare the accepted transaction bytes, block time, proposer, Core height, proposed app version, validator quorum, or validate the incoming consensus app version. These fields are consumed by `BlockProposal` and `run_block_proposal_v0` and can change execution or make the proposal invalid. A competing request with the same transaction count and chain-lock update can therefore receive the prepared candidate's app hash, after which lines 134-141 attach the competing request's hash to that stale context. This directly preserves the wrong-AppHash failure the PR is intended to eliminate. The regression test calls `mimic_execute_block` for the local proposal first, and that helper invokes `ProcessProposal` and records its hash before the competing request, so the test exercises only the corrected hash-present branch. Retain and compare a normalized identity covering every execution-relevant PrepareProposal/ProcessProposal input, including byte-for-byte accepted transactions, before serving this cache; otherwise discard the context and execute the incoming request.

QuantumExplorer and others added 2 commits August 25, 2026 11:35
…ntent

The hash-aware branch discards a stale execution context and executes a
different block at the same height and round, but the context left behind
immediately after PrepareProposal has no block hash to compare against —
Tenderdash computes one only once the response is in. That context was
matched on transaction count and the core chain lock update alone, so the
first competing ProcessProposal at the same height and round could still be
served the prepared block's app hash. Height was never compared at all.

Decide identity on the execution inputs instead: the ordered transaction
bytes, block time, proposer, core chain locked height, core chain lock
update, proposed app version, validator set quorum hash, consensus app
version and height. Anything BlockProposal discards on conversion —
proposed_last_commit, misbehavior, next_validators_hash and the consensus
block version — cannot reach execution and is left out. A mismatch on any
of them takes the same way out the hash-mismatch case already took:
discard the context and execute the block we were asked about.

The proposed app version and the quorum hash were not retained anywhere, so
proposer_results now carries them alongside the response. It is written only
by prepare_proposal and read only by process_proposal, so every other
construction site keeps its `proposer_results: None` unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
They pin a consensus-halt regression, run in under a second combined, and
their binary is already built by the PR phase. Excluded from the push-only
chain-simulation phase to keep the codecov flags disjoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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 — Codex/Sol only (Phase 2 disabled)

At exact head a5cab13, the hashless proposer-cache path now compares every input retained and consumed by BlockProposal execution, including exact accepted transaction bytes, before reusing the prepared result; mismatches discard the stale context and execute the incoming proposal. The prior blocking finding is fixed, and validation passed with all 5 targeted collision tests, cargo fmt, and drive-abci clippy; no in-scope issues remain.
Source: Codex reviewers: gpt-5.6-sol; final verifier: Anthropic Claude Agent SDK (exact model ID not exposed). Orchestration-only and not reviewer evidence: openclaw-agent/cliproxy/gpt-5.6-sol.

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — rust-quality (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

@QuantumExplorer QuantumExplorer added the bug Something isn't working label Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants