Fix stale entry being published to ledger history after rollback - #8223
Draft
Amaury Chamayou (achamayou) wants to merge 1 commit into
Draft
Fix stale entry being published to ledger history after rollback#8223Amaury Chamayou (achamayou) wants to merge 1 commit into
Amaury Chamayou (achamayou) wants to merge 1 commit into
Conversation
Store::commit() checks a transaction's view under version_lock, then releases it before serialising and appending the entry to the ledger history. A rollback (for example from become_leader() during an election) can land in that window: it retracts the history and moves the Store to a new view, and the in-flight transaction then appends its now-stale digest to the retracted history. Consensus subsequently rejects the entry and rolls back again, but in the interval the history publishes a root which is in neither the KV nor the ledger. That root is read without any Store lock, notably by set_root_on_proposals(), which binds a proposal id to it. Re-check the transaction's view atomically with the history append, under version_lock, which rollback also takes. Adds a unit test reproducing the interleaving deterministically. Without the Store::commit() change it fails, observing the history at seqno 2 while the store and ledger are both at seqno 1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #8209 - this PR targets the
life_raftbranch, so the diff shows only the two files changed here.The problem
While reviewing #8209 I found a window in which a rolled-back transaction can still publish its digest to the ledger history.
Store::commit()checks the transaction's view underversion_lock, then releases the lock before serialising the entry and appending it to the history:CCF/src/kv/store.h
Lines 978 to 1079 in ec26daf
A rollback can land in that window.
Store::rollback()takesmaps_lockandversion_lock, but notcommit_lock, so it is free to run while a commit is in flight.TxID 2.2Store::commit(), takescommit_lock2 == term_of_next_version, releasesversion_lockbecome_leader()rolls back to committed index 1Store::rollback()retracts the history to seqno 12.2digest to the retracted historyRaft::replicate(), blocks onstate->lockThe history's own
state_lockdoes not help: it only serialisesrollback()againstappend_entry(), so "retract to 1, then append leaf 2" is an accepted sequence.The stale root has a real consumer.
set_root_on_proposals()reads it without holding any Store lock, and the proposal id is a digest of that root:CCF/src/node/rpc/frontend.h
Lines 1119 to 1132 in ec26daf
Before #8209 this was prevented:
Store::commit()capturedreplication_viewunderversion_lockand passed it toTxHistory::append_entry(), which compared it againstterm_of_next_versionwhile holding the history lock and dropped the append if it no longer matched. #8209 removes both the argument and the check.The test
src/consensus/aft/test/view_straddling_transactions.cppgains a test that drives the interleaving deterministically:PausingMovePendingTxbehaves exactly likeccf::kv::MovePendingTx(thePendingTxof every normal transaction - it hands over an already-serialised entry and cannot fail), but pauses first. That models the committing thread being descheduled after the view check and before the history append. The signature path is not affected, becausecommit_reserved()already re-checks the rollback count.PausableHistorysignals and pauses on the next entry appended, after the base class has released its lock, so the resulting tree is observable by other threads.The core assertion is that the rolled-back entry is never appended at all. Reverting the
store.hhunk reproduces the failure:The history is at seqno 2 while
store->current_txid(),raft->get_last_idx()and the ledger are all at seqno 1.The fix
The
store.hchange is deliberately minimal, and is a suggestion rather than the only option: re-check the entry's view atomically with the append, underversion_lock, which rollback also takes. Lock ordering is unchanged (version_lockthen historystate_lock, as inStore::rollback()).An alternative that fits #8209's direction more closely would be to restore a view/generation guard inside
TxHistory::append_entry()itself, keyed on each entry'sTxID. Happy to switch if you prefer that.Testing
raft_test,kv_test,history_testpass.map_test,state_machine_test,kv_test,ledger_test,raft_test,raft_enclave_test,history_test,encryptor_test,historical_queries_test,indexing_test,snapshot_test,snapshotter_test,frontend_test,endpoint_registry_test,tx_status_test,node_frontend_test,internal_tables_access_test,merkle_test.cpp-format-checks.sh,copyright-checks.sh,ascii-checks.shclean.No
CHANGELOG.mdentry: this fixes a regression that is not in any release, and there is no user-visible API change.