Skip to content

bft: restore the start view from the trailing proposal record - #750

Merged
HagarMeir merged 1 commit into
hyperledger:mainfrom
pfi79:load-new-view-across-records
Nov 4, 2026
Merged

HagarMeir merged 1 commit into
hyperledger:mainfrom
pfi79:load-new-view-across-records

Conversation

@pfi79

@pfi79 pfi79 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

A NewView record can be followed by a proposal or a commit when the node
restarts before the proposal is decided. The built-in WAL saves the
pre-prepare with truncateTo=true, which drops the NewView record. After a
restart only the proposal pair remains.

LoadNewViewIfApplicable returned nil, the controller started from the older
application metadata, and Restore set View.Number to the proposal's view.
The controller and the view disagreed about the current view and leader.

Walk the WAL backwards to the latest view transition. Return the last
NewView, and stop at a later ViewChange so it keeps precedence. When no
NewView survives, take the view from the trailing ProposedRecord or Commit
and start the controller there.

Add unit tests for the fallback and an integration test that restarts a node
after a NewView and a proposal, then checks that the controller starts in
the restored view.

Fixes #751

@pfi79
pfi79 marked this pull request as draft November 4, 2026 08:10
@pfi79
pfi79 force-pushed the load-new-view-across-records branch 2 times, most recently from 994969d to c29067c Compare November 4, 2026 08:25
@pfi79 pfi79 changed the title bft: restore the start view from the last new view record bft: restore the start view from the trailing proposal record Nov 4, 2026
@pfi79
pfi79 marked this pull request as ready for review November 4, 2026 08:27

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

Written by Claude (AI) at @HagarMeir's request.

Two points on LoadLastProposalView and setViewAndSeq:

  1. The bool isn't needed. The only caller checks found && lastView > newView. When nothing is found the view is 0, and 0 > newView is always false, so found doesn't change behaviour.

  2. newDec = 0 is hard-coded, but Restore reads DecisionsInView from the proposal metadata. It's 0 in practice, because the first proposal after a view change has 0 decisions. But nothing reconciles the two values (NewProposer doesn't, since viewNum == view.Number after Restore). If that ever breaks, the controller and the view disagree on the decision count, and with LeaderRotation on they disagree on the leader too. The tests wouldn't catch it: TestSetViewAndSeqUsesTheTrailingProposalView uses seq 0, which already forces dec to 0 before this branch, and the integration test runs without leader rotation.

Suggestion: return the trailing proposal's *protos.ViewMetadata (nil if there isn't one) and take both values from it:

if md != nil && md.GetViewId() > newView {
	newView = md.GetViewId()
	newDec = md.GetDecisionsInView()
}

A commit always follows its ProposedRecord, so the walk can skip commits and read the proposal before it. It would also be good to give the unit test a non-zero seq so it actually checks dec.

@pfi79
pfi79 force-pushed the load-new-view-across-records branch from c29067c to 0a76ff0 Compare November 4, 2026 11:19
@pfi79

pfi79 commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Written by Claude (AI) at @HagarMeir's request.

Two points on LoadLastProposalView and setViewAndSeq:

  1. The bool isn't needed. The only caller checks found && lastView > newView. When nothing is found the view is 0, and 0 > newView is always false, so found doesn't change behaviour.
  2. newDec = 0 is hard-coded, but Restore reads DecisionsInView from the proposal metadata. It's 0 in practice, because the first proposal after a view change has 0 decisions. But nothing reconciles the two values (NewProposer doesn't, since viewNum == view.Number after Restore). If that ever breaks, the controller and the view disagree on the decision count, and with LeaderRotation on they disagree on the leader too. The tests wouldn't catch it: TestSetViewAndSeqUsesTheTrailingProposalView uses seq 0, which already forces dec to 0 before this branch, and the integration test runs without leader rotation.

Suggestion: return the trailing proposal's *protos.ViewMetadata (nil if there isn't one) and take both values from it:

if md != nil && md.GetViewId() > newView {
	newView = md.GetViewId()
	newDec = md.GetDecisionsInView()
}

A commit always follows its ProposedRecord, so the walk can skip commits and read the proposal before it. It would also be good to give the unit test a non-zero seq so it actually checks dec.

fixed

A NewView record can be followed by a proposal or a commit when the node
restarts before the proposal is decided. The built-in WAL saves the
pre-prepare with truncateTo=true, which drops the NewView record. After a
restart only the proposal pair remains.

LoadNewViewIfApplicable returned nil, the controller started from the older
application metadata, and Restore set View.Number to the proposal's view.
The controller and the view disagreed about the current view and leader.

Walk the WAL backwards to the latest view transition. Return the last
NewView, and stop at a later ViewChange so it keeps precedence. When no
NewView survives, take the view from the trailing ProposedRecord or Commit
and start the controller there.

Add unit tests for the fallback and an integration test that restarts a node
after a NewView and a proposal, then checks that the controller starts in
the restored view.

Signed-off-by: Fedor Partanskiy <fredprtnsk@gmail.com>
@pfi79
pfi79 force-pushed the load-new-view-across-records branch from 0a76ff0 to a57afdd Compare November 4, 2026 11:24
@HagarMeir
HagarMeir merged commit 41bec1b into hyperledger:main Nov 4, 2026
6 checks passed
@pfi79
pfi79 deleted the load-new-view-across-records branch November 4, 2026 15:04
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.

A restarted node starts the controller in an older view when a proposal follows a NewView record

2 participants