Repository navigation
fix(consensus): verify commit vote extensions before a commit is taken - #1520
Conversation
Add intentionally failing tests for stripped and duplicated commit extensions, RAW extension context binding, a known but incorrect header proposer, and repeated proposer capture through stored block metadata. Keep production behavior unchanged pending remediation. The RAW signing test is a diagnostic probe, not a requirement to break Core-compatible signatures. Co-Authored-By: Codex <noreply@openai.com> <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Validate the complete threshold vote-extension vector through ABCI after ProcessProposal and before storing or finalizing blocks across consensus, block sync, and replay paths. Co-Authored-By: OpenAI GPT-5 <noreply@openai.com>
Keep the unrelated red proposer tests on test/proposer-header-regressions. PR #1509 now contains only the commit extension fix and its regression tests. Co-Authored-By: Codex GPT-6 <noreply@openai.com> <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Replace the pasted permissive VerifyVoteExtension stubs with a shared acceptCommitExtensions helper. Given SaveBlock expectations, it makes each wait for the commit-vector check, so moving the check after persistence fails the applier tests instead of passing silently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A commit whose extensions the application REJECTs used to strand its height: peers that sent a commit while the rejected one was parked mark this node as holding one and never resend, and forcing the step back to Precommit made every queued timeout a no-op. - TryAddCommit keeps commits that arrive while another is parked as unverified replacement candidates: one slot per peer, capped at 32, discarded on height change, retried in arrival order (so WAL replay is deterministic). - After a REJECT, ApplyCommit tries those candidates through the normal TryAddCommit verification, then the node's own +2/3 precommits for the block, and failing both schedules a precommit-wait timeout so the node moves to the next round, which makes peers gossip their commits again. Recovery is iterative; a rejected commit is never relayed. - tryFinalizeCommit returns the ApplyCommit error instead of dropping it. - The ApplyCommit step transition shared with EnterCommit becomes StateData.enterApplyCommit; a peer's rejected commit no longer rewrites the round's step. Tests pin the Marvin stall scenario, the own-quorum fallback, the real tryFinalizeCommit path, the round-change fallback, replay, no relay of rejected commits, and the previously surviving mutants M8/M9/M10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The proto (source of truth, regenerated: types.pb.go and api.md change in comments only) and the ABCI++ spec pages now describe both kinds of request: an empty validator_pro_tx_hash verifies a commit's threshold-recoverable extension vector after ProcessProposal, which the application must compare exactly. REJECT semantics are spelled out per path (consensus replacement or round change, block sync peer drop and refetch, replay start-up failure), the request table gains the missing round field and correct field numbers, and the expected-behavior call counts, grammar and trivial-app rule include the commit call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use the locked state snapshot for proposal creation and processing. Add a regression test exercising both paths during concurrent state updates. Co-Authored-By: OpenAI Codex GPT-6 <noreply@openai.com> <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Read peer capacity before locking PEX so peer updates can drain while PeerManager.Ready holds its mutex during broadcast. Reproduce the blocked update path in a concurrency regression test. Co-Authored-By: OpenAI Codex GPT-6 <noreply@openai.com> <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Pass the workflow token to buf-setup-action to avoid anonymous API rate limits preventing protobuf lint from starting. Co-Authored-By: OpenAI Codex GPT-6 <noreply@openai.com> <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Advance rounds directly after application rejection, avoid blaming the block-part peer, and expose rejection metrics across consensus and block sync. Co-Authored-By: OpenAI Codex GPT-5 <noreply@openai.com> <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Generate e2e vote extensions for the requested initial height. Retain block-sync ProcessProposal output across semantic rejection and keep the downloaded block and processed context during consensus round fallback. Sequence discard/save before recovery and clarify the upgrade and historical replay requirements. Add initial-height, retry, cache-invalidation and round-retention regressions. Co-Authored-By: Codex GPT-6 <noreply@openai.com> <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Make KeepProcessedBlock preserve both the block and its ProcessProposal result inside EnterNewRoundAction, removing caller-side restoration. Cover direct round entry and rejection recovery with automatic proposing enabled for both proposer roles. Remove ineffective fixture configuration and document why replaying unchecked historical extensions intentionally stops on rejection. Co-Authored-By: Codex GPT-6 <noreply@openai.com> <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Keep the propose timeout while suppressing proposal creation for a retained committed block. Include height, round and hash in commit extension errors on every verification path. Cover queued future-round recovery with a test. Co-Authored-By: Codex GPT-6 <noreply@openai.com> <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Co-Authored-By: Codex GPT-6 <noreply@openai.com> <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Release queued candidates after accepting a height. Clarify rollback and startup replay behavior, and remove unreachable rejection log classification. Co-Authored-By: Codex GPT-6 <noreply@openai.com> <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
A peer resending an authentic commit with stripped extensions made the node advance one round per rejection, retaining vote sets and growing timeouts without bound. Recovery no longer changes the round; instead no honest commit can be lost, so re-gossip is unnecessary: - commit candidates keep one slot per connected peer (no fixed cap); disconnected peers' slots are freed on the next add, using the peer lanes' live sessions; replayed and local entries are exempt - queued commits prepay their verification in their own scheduler turn, and recovery retries them uncharged, so recovery never spends budget out of turn and cannot starve the message the scheduler reserved for - a commit that still fails for want of budget is retained in its sender's slot for the next recovery (prepaid retries never re-queue) - remove KeepProcessedBlock / SkipProposalCreation; keep the EnterPropose skip for a held committed block, needed by the future-round commit path Docs describe the remaining stall: with no pending timeout, progress waits for a commit from a peer that has not sent one or reconnects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository: dashpay/tenderdash/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
|
✅ Final review complete — no blockers (commit 483f7e2) |
Ask the application to accept a commit's extension vector in TryAddCommit, right after the held block is processed, so a rejection is an ordinary verification error: nothing is saved, parked or recovered, the round stays, and the sender is not evicted. ApplyCommit, AddCommit, EnterCommit, EnterPropose and StateData return to their v1.8-dev shape. - A future-round commit whose block is held is checked before its round is entered, so a rejection changes no round. - Commits arriving while one is parked awaiting its block are authenticated on arrival (threshold signature, charged in-turn) and kept one slot per connected peer. When the block completes, the parked commit and then the queued ones are checked in order; the first accepted is applied. If all are rejected the block takes the ordinary path, which lets a held +2/3 precommit quorum finish the height. - The commit built from the node's own precommits is checked in tryFinalizeCommit; a rejection logs an error and persists nothing. Drops the recovery loop, Prepaid budget flag, budget-exhausted retention, ApplyCommitEvent.Rejected/FromOwnPrecommits and the proposer skip, and consolidates the recovery and queue tests into commit_extensions_test.go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Claudius here. Reviewed via sequential producers (QA first, per CI ordering); its own MEDIUM/BLOCKING self-assessment tripped the early-stop gate, so the security, project-consistency, and docs passes didn't run this time around — they'll pick it up on the next push. One MEDIUM, non-blocking finding posted inline: selectParkedCommit can drive an uncapped number of full ABCI ProcessProposal re-executions (one per distinct candidate round among connected peers) on a single block-completion event, with no budget analogous to the authentication step's verificationBudget. It's bounded by peer count and recoverable via block sync, not a safety break — but it sits in the exact commit-recovery code this PR adds, so worth a look. Full report: review-report/report.html.
📊 View full HTML review report
Commits for the same block may differ only in round, and the held block is processed once per round. Trying queued candidates in raw arrival order could switch back to a round already processed, re-running ProcessProposal for the same height and round, which apps such as kvstore reject as a duplicate call, so an honest commit could fail on a processing error. selectParkedCommit now groups candidates by round: the round already processed for the held block first, then each other round in order of first appearance, keeping arrival order within a round. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the processedBlock/processedRound cache key with the same check consensus' ensureProcess uses: a ProcessProposal-sourced CurrentRoundState that MatchesBlock(header, round). The commit is authenticated for the block before processing, so the header identifies it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Your move: thepastaclaw left review threads unresolved; resolve them. |
|
Your move: thepastaclaw left review threads unresolved; resolve them. |
The commit-checking executor now fails a commit-level VerifyVoteExtension unless the application last processed the commit's block for its round, and a FinalizeBlock whose round state or last ProcessProposal is for another round. TestForeignRoundCommitsCannotForceReprocessing gets an ample verification budget: it sends more commits than the rate-limited budget admits on a slow run, and what it tests is the expectations bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr5SvZoMmosbQdRrMX1p51
selectParkedCommit and ensureOwnRound restated processedFor's condition. Also name the quorum hash among what commits with equal keys may differ in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr5SvZoMmosbQdRrMX1p51
A commit of the round processed last still goes to the application, whatever its vector; say so where the ExtendVote expectation is documented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr5SvZoMmosbQdRrMX1p51
It runs for every peer precommit; the block is hashed only once another round was processed last. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr5SvZoMmosbQdRrMX1p51
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr5SvZoMmosbQdRrMX1p51
The gofmt run by golangci-lint in CI rejects the reindented block, so restore the v1.8-dev formatting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr5SvZoMmosbQdRrMX1p51
A failed ProcessProposal left CurrentRoundState naming the round processed before, while the application (Drive) may already have dropped that context. A later commit for that round then skipped ProcessProposal, and a rejected vector sent the node to ExtendVote on a missing context, which panics. The stale state also made ensureOwnRound retry ProcessProposal for every resent peer precommit. ensureProcess now clears CurrentRoundState on failure, and verifyCommitExtensions asks ExtendVote only while the block is known to be processed for the commit's round. The foreign-round flood test now interleaves peer precommits and pins the exact ProcessProposal sequence, 2·G, in place of a bound that could not fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr5SvZoMmosbQdRrMX1p51
ExtendVote is asked for a commit's expected vector only after a commit-level VerifyVoteExtension rejection that follows ProcessProposal of that block and round, and an invalid response is as fatal as an ABCI error. Say so in the proto, api.md, the methods spec and UPGRADING, including the "When does Tenderdash call it?" section, and stop calling the extension uninterpreted. Restore the non-determinism allowance for extensions that are not threshold-recoverable, consistently with the application requirements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr5SvZoMmosbQdRrMX1p51
Every reader of CurrentRoundState's fields runs right after a successful ensureProcess. After a failure, ensureOwnRound lets a peer precommit reach VerifyVoteExtension without a known context. That is not fatal, and Drive checks only the height and rejects without an execution context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr5SvZoMmosbQdRrMX1p51
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
At the exact reviewed head, all six prior findings are fixed, and commit-extension verification precedes persistence across consensus, block sync, and handshake catch-up. One non-blocking replay scalability suggestion remains; the claimed injection of unsigned DEFAULT extensions is prevented by existing signature-count validation. Uncached Makefile-driven tests passed for the seven affected core packages, and focused regression and concurrency tests passed with the race detector and deadlock tag.
🟡 1 suggestion(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: tenderdash-consensus-security); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: tenderdash-consensus-security); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: tenderdash-consensus-security); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The large, intricate diff changes consensus commit acceptance in internal/consensus/state_try_add_commit.go and prepareCommitForApply in internal/consensus/block_executor.go, including application verification of vote extensions, authenticated peer commit queuing, and round-transition behavior before persistence. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— tenderdash-consensus-security (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 13% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— tenderdash-consensus-security (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— tenderdash-consensus-security (completed, effort xhigh); agentphase2-reviewer
🤖 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 `internal/consensus/commit_candidates.go`:
- [SUGGESTION] internal/consensus/commit_candidates.go:79-84: Avoid quadratic candidate insertion during WAL replay
During WAL replay, retainable preserves every fromReplay entry regardless of whether its sender remains connected. Distinct historical peer IDs therefore accumulate at one unfinished height, and each insertion scans the accumulated slice in holds, DeleteFunc, and the replacement lookup, making reconstruction O(N²). Identical copies of an authentic replacement still receive separate sender-owned slots, so connection churn can trigger this without producing distinct commit vectors. The live connection limit does not bound replay's historical sender count, and the default WAL limit is 1 GB. A candidate-only replay benchmark measured approximately 19 ms, 62 ms, and 263 ms for 2,000, 4,000, and 8,000 senders, respectively. Preserve arrival order and per-peer ownership while indexing slots by peer ID, and avoid repeated disconnected-peer pruning scans during replay. Add a replay/churn benchmark to protect that behavior.
|
Bots are done — your move: post |
|
Ready for review — needs ktechmidas or shumkov. |
|
Ready for review — needs ktechmidas or shumkov. |
Issue being fixed or feature implemented
Alternative to #1509. It contains all of #1509 plus commits that replace how the node handles a commit whose vote extensions the application rejects.
A peer can send an authentic commit whose threshold block signature is valid but whose extension vector is stripped, duplicated, replayed from another height, or empty. The block signature does not cover the vector. If a node persisted such a commit, Drive would reject it in
FinalizeBlockand the node would crash-loop.#1509 checked the extensions in
ApplyCommit, after the commit was already taken. That required recovery machinery. In one version, a rejected commit made the node move to the next round right away. A peer resending one stripped commit then added a round per rejection, and every round kept vote sets and lengthened timeouts with no limit (review thread).This PR checks the extensions before a commit is taken. A rejection is then an ordinary verification failure: the commit is not taken, and there is nothing to recover.
What was done?
TryAddCommitalready runsProcessProposal(ensureProcess) before it takes a commit. It now callsVerifyCommitExtensionsright after, at the end ofprepareCommitForApply. When the application rejects the vector:extensions_rejectedmetric is incremented;v1.8-dev, and the check runs when the block arrives. When the commit is accepted and the node is that round's proposer, it does not propose: a proposal would replace the checkedProcessProposalresult and force a second one for the same round.ValidateBasicand a BLS check, charged to the sender's budget in its turn), and a forged one gets its sender evicted;BlockIDand extension vector), is dropped before its BLS check and takes no slot. A copy of another peer's queued commit is verified (charged to its sender) and takes the sender's own slot, so the first sender cannot make the authentic commit disappear by replacing its slot or disconnecting; a commit that would need its block processed again is refused by the expectation (see below);ProcessProposal) once per round, then the application checks each commit's vector. The first one accepted is applied;ApplyCommit. If the application rejects it, the node logs an error and persists nothing. It waits for a peer's commit instead of panicking, because a panic would loop: WAL replay rebuilds the same commit.THRESHOLD_RECOVERentry's sign request ID, and app-supplied (RAW) sign request IDs bound to no height or round all vary under one genuine block signature, so no cache of verdicts on vectors can bound the work. After the application rejects a commit at a (block, round), Tenderdash asks itsExtendVotefor the vector expected there and keeps it for the height. A later commit that would need that block processed again for its round is refused beforeProcessProposaland without any ABCI call when its vector differs from the expected one, or equals it and was already rejected. Commits of the round processed last still go toVerifyVoteExtension. ForcedProcessProposalcalls per height are ≤ 2·G plus the finalizing one, where G is the number of foreign rounds with a signature-valid commit for the held block, whatever the number of peers or vectors.ExtendVoteon a round mismatch). Rejections of peer commits log at debug (peer-floodable); rejection of the node's own commit stays at Error.ensureProcess, in the spec and inUPGRADING.md:CurrentRoundStatemust match the application's latestProcessProposal; a result from another round is never reused without a newProcessProposal. A failedProcessProposalclearsCurrentRoundState, since the application may have dropped the context it held (Drive does), so a later commit of the round processed before is not judged, andExtendVoteis not asked, on a context the application no longer holds; the failed own-round restore is not retried per resent precommit. Drive keeps one execution context per height and finalizes only the round processed last, so consensus logic keeps no per-round result map and restores nothing.ProcessProposal(proto comment onRequestProcessProposal,api.md, ABCI++ methods spec,UPGRADING.md): Tenderdash may callProcessProposalmore than once for the same height and round with the same block (e.g. after a commit-extension rejection, or when block sync hands over to consensus, which starts without block sync's result), and again for a round after processing another round. The application must treat it as re-execution replacing its execution context;FinalizeBlockrefers to the round processed last. This matches Drive v4.1.1 (process_proposal.rs: same round + same hash, non-proposer → drop context and re-execute; only same round + different hash errors).ProcessProposalresult by header and round (CurrentRoundStateSource+MatchesBlock, the same rule as consensus'ensureProcess). A commit rejected at the round processed last is retried without processing the block again; a commit of another round processes it again.ProcessProposalerrors only when called again for the round processed last with a different (or empty) block hash. A repeat of that round with the same block hash re-executes it (e.g. block sync processes the block, the commit's extensions are rejected, and consensus processes the same block and round again after the handover). Re-processing a round after another one (e.g. commits rejected at r0 then r1, then the node's own r0 quorum; or block sync r1 → r2 → r1) is allowed too. In both casesFinalizeBlock's round-keyed state lookup still finds the state, and the replaced round state is closed. Before this, those sequences made the authentic commit fail (consensus) orblockApplier.Apply(block sync) panic under kvstore/e2e networks.state_apply_commit.go,state_add_commit.go,state_data.go,state_enter_commit.goandstate_enter_propose.goare back to theirv1.8-devversions. The following are gone:KeepProcessedBlockandSkipProposalCreation;Prepaidand keeping commits for which the budget ran out.UPGRADING.md(including "before you upgrade" ordering, theconsensus_commit_verify_failures{reason="extensions_rejected"}metric and the own-commit log line), the ABCI++ spec, the proto comment (withtypes.pb.goandapi.mdregenerated), anddocs/nodes/metrics.md.UNKNOWNis documented asREJECT; handshake catch-up is named apart from WAL replay. The metric help text covers own and block-sync commits, block sync labels it throughconsensus.CommitVerifyFailureReason, and both handshake catch-up paths give the same rollback hint.calculateNextRequestTimeno longer holdsr.mtxwhile callingpeerManager.PeerRatio()(lock-order deadlock), withreactor_deadlock_test.go;proto-lint.ymlpassesgithub_tokentobuf-setup-action;blockExecutor:create/ensureProcessread the committed state through theRWMutexaccessorgetCommittedState();VerifyVoteExtensionenforces the expected commit vector (emptyvalidator_pro_tx_hash).How Has This Been Tested?
commit_extensions_test.go.TestCommitExtensionsRejectedBeforeSavecovers 5 paths (held, parked, future, future-held, replay) × 4 vectors (stripped, duplicated, cross-height replay, empty). Each case asserts that the rejection happens beforeSaveBlock, nothing is persisted or relayed, the metric is incremented, nobody is evicted, and an honest commit then finishes the height without processing the block again.TestParkedCommitsAreTriedRoundByRound);TestOwnQuorumAfterCommitsRejectedInTwoRounds: waiting r0 commit rejected, queued r1 commit rejected, then the own r0 quorum finalizes with a kvstore-like executor (ProcessProposalrounds[0 1 0], no panic);TestQueuedCommitCopies: an attacker parks a stripped commit and queues the authentic one; an honest peer's copy of the authentic commit is kept, so after the attacker replaces its slot with another bad vector, or disconnects, the authentic commit is still applied; the same sender's repeat and a copy of the parked commit cost no BLS check and no slot;TestSameRoundReprocessedAfterHandover: the block processed for round 0 as block sync would (extensions rejected), then consensus, without that result, processes the same block and round again for the authentic commit, which is applied with a kvstore-like executor. A full block sync → consensus reactor handover test was judged disproportionate: the blocksync package imports consensus and the consensus test fixture is internal;TestForeignRoundCommitsCannotForceReprocessing: 116 signature-valid vectors (subsets, permutation, duplicates, junkTHRESHOLD_RECOVERsign request IDs, foreign-height RAW) alternated over two rounds, with a resent peer precommit after each commit making the node return to its own round:ProcessProposalrounds exactly[1 0 2 0](2·G),ExtendVote×2, no application call for refused commits; the genuine commit still finalizes;TestFailedProcessProposalForgetsProcessedRound(a failedProcessProposalfor a foreign commit or for the own-round restore, then a rejected commit for the round processed before: the block is processed again or the commit is refused by the expectation, andExtendVoteis asked only in that context),TestFailedOwnRoundRestoreIsNotRetried(11 resends of a precommit after a failed restore: oneProcessProposal),TestExpectationLearnedOnlyForProcessedRound;TestRejectedExpectedVectorIsRemembered(an app rejecting its own expected vector is not asked again);TestOwnRoundRestoredAfterForeignCommitRejected(peer precommit and relock, with a Drive-like round-checking executor);TestCommitExtensionRejectionLogsAtDebug,TestSameVoteExtensionShape,TestCommitExpectations,TestCommitKey;TestBlockApplierRejectsCommitExtensionsAndRetries,TestBlockApplierInvalidatesRejectedProposal(block/round/state change forces a newProcessProposal), andTestBlockApplierReprocessesRoundAfterAnother(r1, r1, r2 rejected, then r1 accepted: rounds[1 2 1], no panic).TestProcessProposalRoundReprocessing(r0, r0 same hash → ok, r0 other hash → error, r0 no hash → error, r1, r0 → ok, r0 same hash → ok, thenFinalizeBlockat r0 succeeds).ensureProcessremoved, floodable classification removed, replay hint removed, staleCurrentRoundStatekept after a failedProcessProposal,ExtendVoteasked outside the processed context,ensureOwnRounddisabled or re-processing on every precommit).TestParkedCommitChecksStateIDBeforeSavingBlockis adjusted. A commit with a mismatched state ID is now dropped rather than returned as an error, and the test still asserts that nothing is persisted.go test -tags=deadlock -count=1 ./internal/consensus ./internal/state ./internal/blocksync ./types ./abci/... ./test/e2e/apppasses (packages run one after another).-raceon the new and changed tests passes.make lint LINT_BASE=origin/v1.8-devreports 0 issues.Breaking Changes
Same as #1509, which changes the ABCI
VerifyVoteExtensioncontract for applications. The spec now also states that Tenderdash may callProcessProposalmore than once for the same height and round with the same block (re-execution that replaces the execution context), and again for a round after processing another round at the same height, and thatFinalizeBlockrefers to the round processed last. Drive v4.1.1 already behaves this way; the reference kvstore/e2e app is updated to match. An application that errors on a repeated same-blockProcessProposalfor a round must be changed to re-execute instead.ExtendVotecontract: Tenderdash may callExtendVoteon any node, non-validators included, after a commit-levelVerifyVoteExtensionrejection that followsProcessProposalof that block and round, to learn the vector a commit of that block and round must carry.ExtendVotemust be free of side effects, and its threshold-recoverable subset must equal exactly what the commit-levelVerifyVoteExtensionaccepts; an ABCI error or an invalid response is fatal. Extensions that are not threshold-recoverable may still be non-deterministic. Drive v4.1.1 and the e2e app comply. No other breaking change.Checklist:
For repository code-owners and collaborators only
🤖 Generated with Claude Code
PR Hygiene ·
483f7e2github(.github/workflows/proto-lint.yml) — ktechmidas or shumkovWhen every box is checked the
PR Hygienecheck passes and this can merge.