mirror of
https://github.com/QuilibriumNetwork/ceremonyclient.git
synced 2026-02-21 10:27:26 +08:00
* wip: conversion of hotstuff from flow into Q-oriented model * bulk of tests * remaining non-integration tests * add integration test, adjust log interface, small tweaks * further adjustments, restore full pacemaker shape * add component lifecycle management+supervisor * further refinements * resolve timeout hanging * mostly finalized state for consensus * bulk of engine swap out * lifecycle-ify most types * wiring nearly complete, missing needed hooks for proposals * plugged in, vetting message validation paths * global consensus, plugged in and verified * app shard now wired in too * do not decode empty keys.yml (#456) * remove obsolete engine.maxFrames config parameter (#454) * default to Info log level unless debug is enabled (#453) * respect config's "logging" section params, remove obsolete single-file logging (#452) * Trivial code cleanup aiming to reduce Go compiler warnings (#451) * simplify range traversal * simplify channel read for single select case * delete rand.Seed() deprecated in Go 1.20 and no-op as of Go 1.24 * simplify range traversal * simplify channel read for single select case * remove redundant type from array * simplify range traversal * simplify channel read for single select case * RC slate * finalize 2.1.0.5 * Update comments in StrictMonotonicCounter Fix comment formatting and clarify description. --------- Co-authored-by: Black Swan <3999712+blacks1ne@users.noreply.github.com>
658 lines
27 KiB
Go
658 lines
27 KiB
Go
package forks
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"source.quilibrium.com/quilibrium/monorepo/consensus"
|
|
"source.quilibrium.com/quilibrium/monorepo/consensus/forest"
|
|
"source.quilibrium.com/quilibrium/monorepo/consensus/models"
|
|
)
|
|
|
|
// Forks enforces structural validity of the consensus state and implements
|
|
// finalization rules as defined in Jolteon consensus
|
|
// https://arxiv.org/abs/2106.10362 The same approach has later been adopted by
|
|
// the Diem team resulting in DiemBFT v4:
|
|
// https://developers.diem.com/papers/diem-consensus-state-machine-replication-in-the-diem-blockchain/2021-08-17.pdf
|
|
// Forks is NOT safe for concurrent use by multiple goroutines.
|
|
type Forks[StateT models.Unique, VoteT models.Unique] struct {
|
|
finalizationCallback consensus.Finalizer
|
|
notifier consensus.FollowerConsumer[StateT, VoteT]
|
|
forest forest.LevelledForest
|
|
trustedRoot *models.CertifiedState[StateT]
|
|
|
|
// finalityProof holds the latest finalized state including the certified
|
|
// child as proof of finality. CAUTION: is nil, when Forks has not yet
|
|
// finalized any states beyond the finalized root state it was initialized
|
|
// with
|
|
finalityProof *consensus.FinalityProof[StateT]
|
|
}
|
|
|
|
var _ consensus.Forks[*nilUnique] = (*Forks[*nilUnique, *nilUnique])(nil)
|
|
|
|
func NewForks[StateT models.Unique, VoteT models.Unique](
|
|
trustedRoot *models.CertifiedState[StateT],
|
|
finalizationCallback consensus.Finalizer,
|
|
notifier consensus.FollowerConsumer[StateT, VoteT],
|
|
) (*Forks[StateT, VoteT], error) {
|
|
if trustedRoot == nil {
|
|
return nil,
|
|
models.NewConfigurationErrorf("invalid root: root is nil")
|
|
}
|
|
|
|
if (trustedRoot.State.Identifier != trustedRoot.CertifyingQuorumCertificate.Identity()) ||
|
|
(trustedRoot.State.Rank != trustedRoot.CertifyingQuorumCertificate.GetRank()) {
|
|
return nil,
|
|
models.NewConfigurationErrorf(
|
|
"invalid root: root QC is not pointing to root state",
|
|
)
|
|
}
|
|
|
|
forks := Forks[StateT, VoteT]{
|
|
finalizationCallback: finalizationCallback,
|
|
notifier: notifier,
|
|
forest: *forest.NewLevelledForest(trustedRoot.State.Rank),
|
|
trustedRoot: trustedRoot,
|
|
finalityProof: nil,
|
|
}
|
|
|
|
// verify and add root state to levelled forest
|
|
err := forks.EnsureStateIsValidExtension(trustedRoot.State)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"invalid root state %x: %w",
|
|
trustedRoot.Identifier(),
|
|
err,
|
|
)
|
|
}
|
|
forks.forest.AddVertex(ToStateContainer2[StateT](trustedRoot.State))
|
|
return &forks, nil
|
|
}
|
|
|
|
// FinalizedRank returns the largest rank number where a finalized state is
|
|
// known
|
|
func (f *Forks[StateT, VoteT]) FinalizedRank() uint64 {
|
|
if f.finalityProof == nil {
|
|
return f.trustedRoot.State.Rank
|
|
}
|
|
return f.finalityProof.State.Rank
|
|
}
|
|
|
|
// FinalizedState returns the finalized state with the largest rank number
|
|
func (f *Forks[StateT, VoteT]) FinalizedState() *models.State[StateT] {
|
|
if f.finalityProof == nil {
|
|
return f.trustedRoot.State
|
|
}
|
|
return f.finalityProof.State
|
|
}
|
|
|
|
// FinalityProof returns the latest finalized state and a certified child from
|
|
// the subsequent rank, which proves finality.
|
|
// CAUTION: method returns (nil, false), when Forks has not yet finalized any
|
|
// states beyond the finalized root state it was initialized with.
|
|
func (f *Forks[StateT, VoteT]) FinalityProof() (
|
|
*consensus.FinalityProof[StateT],
|
|
bool,
|
|
) {
|
|
return f.finalityProof, f.finalityProof != nil
|
|
}
|
|
|
|
// GetState returns (*models.State, true) if the state with the specified
|
|
// id was found and (nil, false) otherwise.
|
|
func (f *Forks[StateT, VoteT]) GetState(stateID models.Identity) (
|
|
*models.State[StateT],
|
|
bool,
|
|
) {
|
|
stateContainer, hasState := f.forest.GetVertex(stateID)
|
|
if !hasState {
|
|
return nil, false
|
|
}
|
|
return stateContainer.(*StateContainer[StateT]).GetState(), true
|
|
}
|
|
|
|
// GetStatesForRank returns all known states for the given rank
|
|
func (f *Forks[StateT, VoteT]) GetStatesForRank(
|
|
rank uint64,
|
|
) []*models.State[StateT] {
|
|
vertexIterator := f.forest.GetVerticesAtLevel(rank)
|
|
// in the vast majority of cases, there will only be one proposal for a
|
|
// particular rank
|
|
states := make([]*models.State[StateT], 0, 1)
|
|
for vertexIterator.HasNext() {
|
|
v := vertexIterator.NextVertex()
|
|
states = append(states, v.(*StateContainer[StateT]).GetState())
|
|
}
|
|
return states
|
|
}
|
|
|
|
// IsKnownState checks whether state is known.
|
|
func (f *Forks[StateT, VoteT]) IsKnownState(stateID models.Identity) bool {
|
|
_, hasState := f.forest.GetVertex(stateID)
|
|
return hasState
|
|
}
|
|
|
|
// IsProcessingNeeded determines whether the given state needs processing,
|
|
// based on the state's rank and hash.
|
|
// Returns false if any of the following conditions applies
|
|
// - state rank is _below_ the most recently finalized state
|
|
// - the state already exists in the consensus state
|
|
//
|
|
// UNVALIDATED: expects state to pass Forks.EnsureStateIsValidExtension(state)
|
|
func (f *Forks[StateT, VoteT]) IsProcessingNeeded(state *models.State[StateT]) bool {
|
|
if state.Rank < f.FinalizedRank() || f.IsKnownState(state.Identifier) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// EnsureStateIsValidExtension checks that the given state is a valid extension
|
|
// to the tree of states already stored (no state modifications). Specifically,
|
|
// the following conditions are enforced, which are critical to the correctness
|
|
// of Forks:
|
|
//
|
|
// 1. If a state with the same ID is already stored, their ranks must be
|
|
// identical.
|
|
// 2. The state's rank must be strictly larger than the rank of its parent.
|
|
// 3. The parent must already be stored (or below the pruning height).
|
|
//
|
|
// Exclusions to these rules (by design):
|
|
// Let W denote the rank of state's parent (i.e. W := state.QC.Rank) and F the
|
|
// latest finalized rank.
|
|
//
|
|
// (i) If state.Rank < F, adding the state would be a no-op. Such states are
|
|
// considered compatible (principle of vacuous truth), i.e. we skip
|
|
// checking 1, 2, 3.
|
|
// (ii) If state.Rank == F, we do not inspect the QC / parent at all (skip 2
|
|
// and 3). This exception is important for compatability with genesis or
|
|
// spork-root states, which do not contain a QC.
|
|
// (iii) If state.Rank > F, but state.QC.Rank < F the parent has already been
|
|
// pruned. In this case, we omit rule 3. (principle of vacuous truth
|
|
// applied to the parent)
|
|
//
|
|
// We assume that all states are fully verified. A valid state must satisfy all
|
|
// consistency requirements; otherwise we have a bug in the compliance layer.
|
|
//
|
|
// Error returns:
|
|
// - models.MissingStateError if the parent of the input proposal does not
|
|
// exist in the forest (but is above the pruned rank). Represents violation
|
|
// of condition 3.
|
|
// - models.InvalidStateError if the state violates condition 1. or 2.
|
|
// - generic error in case of unexpected bug or internal state corruption
|
|
func (f *Forks[StateT, VoteT]) EnsureStateIsValidExtension(
|
|
state *models.State[StateT],
|
|
) error {
|
|
if state.Rank < f.forest.LowestLevel { // exclusion (i)
|
|
return nil
|
|
}
|
|
|
|
// LevelledForest enforces conditions 1. and 2. including the respective
|
|
// exclusions (ii) and (iii).
|
|
stateContainer := ToStateContainer2[StateT](state)
|
|
err := f.forest.VerifyVertex(stateContainer)
|
|
if err != nil {
|
|
if forest.IsInvalidVertexError(err) {
|
|
return models.NewInvalidStateErrorf(
|
|
state,
|
|
"not a valid vertex for state tree: %w",
|
|
err,
|
|
)
|
|
}
|
|
return fmt.Errorf(
|
|
"state tree generated unexpected error validating vertex: %w",
|
|
err,
|
|
)
|
|
}
|
|
|
|
// Condition 3:
|
|
// LevelledForest implements a more generalized algorithm that also works for
|
|
// disjoint graphs. Therefore, LevelledForest _not_ enforce condition 3. Here,
|
|
// we additionally require that the pending states form a tree (connected
|
|
// graph), i.e. we need to enforce condition 3
|
|
if (state.Rank == f.forest.LowestLevel) ||
|
|
(state.ParentQuorumCertificate.GetRank() < f.forest.LowestLevel) { // exclusion (ii) and (iii)
|
|
return nil
|
|
}
|
|
// For a state whose parent is _not_ below the pruning height, we expect the
|
|
// parent to be known.
|
|
_, isParentKnown := f.forest.GetVertex(
|
|
state.ParentQuorumCertificate.Identity(),
|
|
)
|
|
if !isParentKnown { // missing parent
|
|
return models.MissingStateError{
|
|
Rank: state.ParentQuorumCertificate.GetRank(),
|
|
Identifier: state.ParentQuorumCertificate.Identity(),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddCertifiedState[StateT] appends the given certified state to the tree of
|
|
// pending states and updates the latest finalized state (if finalization
|
|
// progressed). Unless the parent is below the pruning threshold (latest
|
|
// finalized rank), we require that the parent is already stored in Forks.
|
|
// Calling this method with previously processed states leaves the consensus
|
|
// state invariant (though, it will potentially cause some duplicate
|
|
// processing).
|
|
//
|
|
// Possible error returns:
|
|
// - models.MissingStateError if the parent does not exist in the forest (but
|
|
// is above the pruned rank). From the perspective of Forks, this error is
|
|
// benign (no-op).
|
|
// - models.InvalidStateError if the state is invalid (see
|
|
// `Forks.EnsureStateIsValidExtension` for details). From the perspective of
|
|
// Forks, this error is benign (no-op). However, we assume all states are
|
|
// fully verified, i.e. they should satisfy all consistency requirements.
|
|
// Hence, this error is likely an indicator of a bug in the compliance
|
|
// layer.
|
|
// - models.ByzantineThresholdExceededError if conflicting QCs or conflicting
|
|
// finalized states have been detected (violating a foundational consensus
|
|
// guarantees). This indicates that there are 1/3+ Byzantine nodes (weighted
|
|
// by seniority) in the network, breaking the safety guarantees of HotStuff
|
|
// (or there is a critical bug / data corruption). Forks cannot recover from
|
|
// this exception.
|
|
// - All other errors are potential symptoms of bugs or state corruption.
|
|
func (f *Forks[StateT, VoteT]) AddCertifiedState(
|
|
certifiedState *models.CertifiedState[StateT],
|
|
) error {
|
|
if !f.IsProcessingNeeded(certifiedState.State) {
|
|
return nil
|
|
}
|
|
|
|
// Check proposal for byzantine evidence, store it and emit
|
|
// `OnStateIncorporated` notification. Note: `checkForByzantineEvidence` only
|
|
// inspects the state, but _not_ its certifying QC. Hence, we have to
|
|
// additionally check here, whether the certifying QC conflicts with any known
|
|
// QCs.
|
|
err := f.checkForByzantineEvidence(certifiedState.State)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"cannot check for Byzantine evidence in certified state %x: %w",
|
|
certifiedState.State.Identifier,
|
|
err,
|
|
)
|
|
}
|
|
err = f.checkForConflictingQCs(&certifiedState.CertifyingQuorumCertificate)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"certifying QC for state %x failed check for conflicts: %w",
|
|
certifiedState.State.Identifier,
|
|
err,
|
|
)
|
|
}
|
|
f.forest.AddVertex(ToStateContainer2[StateT](certifiedState.State))
|
|
f.notifier.OnStateIncorporated(certifiedState.State)
|
|
|
|
// Update finality status:
|
|
err = f.checkForAdvancingFinalization(certifiedState)
|
|
if err != nil {
|
|
return fmt.Errorf("updating finalization failed: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddValidatedState appends the validated state to the tree of pending
|
|
// states and updates the latest finalized state (if applicable). Unless the
|
|
// parent is below the pruning threshold (latest finalized rank), we require
|
|
// that the parent is already stored in Forks. Calling this method with
|
|
// previously processed states leaves the consensus state invariant (though, it
|
|
// will potentially cause some duplicate processing).
|
|
// Notes:
|
|
// - Method `AddCertifiedState[StateT](..)` should be used preferably, if a QC
|
|
// certifying `state` is already known. This is generally the case for the
|
|
// consensus follower. Method `AddValidatedState` is intended for active
|
|
// consensus participants, which fully validate states (incl. payload), i.e.
|
|
// QCs are processed as part of validated proposals.
|
|
//
|
|
// Possible error returns:
|
|
// - models.MissingStateError if the parent does not exist in the forest (but
|
|
// is above the pruned rank). From the perspective of Forks, this error is
|
|
// benign (no-op).
|
|
// - models.InvalidStateError if the state is invalid (see
|
|
// `Forks.EnsureStateIsValidExtension` for details). From the perspective of
|
|
// Forks, this error is benign (no-op). However, we assume all states are
|
|
// fully verified, i.e. they should satisfy all consistency requirements.
|
|
// Hence, this error is likely an indicator of a bug in the compliance
|
|
// layer.
|
|
// - models.ByzantineThresholdExceededError if conflicting QCs or conflicting
|
|
// finalized states have been detected (violating a foundational consensus
|
|
// guarantees). This indicates that there are 1/3+ Byzantine nodes (weighted
|
|
// by seniority) in the network, breaking the safety guarantees of HotStuff
|
|
// (or there is a critical bug / data corruption). Forks cannot recover from
|
|
// this exception.
|
|
// - All other errors are potential symptoms of bugs or state corruption.
|
|
func (f *Forks[StateT, VoteT]) AddValidatedState(
|
|
proposal *models.State[StateT],
|
|
) error {
|
|
if !f.IsProcessingNeeded(proposal) {
|
|
return nil
|
|
}
|
|
|
|
// Check proposal for byzantine evidence, store it and emit
|
|
// `OnStateIncorporated` notification:
|
|
err := f.checkForByzantineEvidence(proposal)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"cannot check Byzantine evidence for state %x: %w",
|
|
proposal.Identifier,
|
|
err,
|
|
)
|
|
}
|
|
f.forest.AddVertex(ToStateContainer2[StateT](proposal))
|
|
f.notifier.OnStateIncorporated(proposal)
|
|
|
|
// Update finality status: In the implementation, our notion of finality is
|
|
// based on certified states.
|
|
// The certified parent essentially combines the parent, with the QC contained
|
|
// in state, to drive finalization.
|
|
parent, found := f.GetState(proposal.ParentQuorumCertificate.Identity())
|
|
if !found {
|
|
// Not finding the parent means it is already pruned; hence this state does
|
|
// not change the finalization state.
|
|
return nil
|
|
}
|
|
certifiedParent, err := models.NewCertifiedState[StateT](
|
|
parent,
|
|
proposal.ParentQuorumCertificate,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"mismatching QC with parent (corrupted Forks state):%w",
|
|
err,
|
|
)
|
|
}
|
|
err = f.checkForAdvancingFinalization(certifiedParent)
|
|
if err != nil {
|
|
return fmt.Errorf("updating finalization failed: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkForByzantineEvidence inspects whether the given `state` together with
|
|
// the already known information yields evidence of byzantine behaviour.
|
|
// Furthermore, the method enforces that `state` is a valid extension of the
|
|
// tree of pending states. If the state is a double proposal, we emit an
|
|
// `OnStateIncorporated` notification. Though, provided the state is a valid
|
|
// extension of the state tree by itself, it passes this method without an
|
|
// error.
|
|
//
|
|
// Possible error returns:
|
|
// - models.MissingStateError if the parent does not exist in the forest (but
|
|
// is above the pruned rank). From the perspective of Forks, this error is
|
|
// benign (no-op).
|
|
// - models.InvalidStateError if the state is invalid (see
|
|
// `Forks.EnsureStateIsValidExtension` for details). From the perspective of
|
|
// Forks, this error is benign (no-op). However, we assume all states are
|
|
// fully verified, i.e. they should satisfy all consistency requirements.
|
|
// Hence, this error is likely an indicator of a bug in the compliance
|
|
// layer.
|
|
// - models.ByzantineThresholdExceededError if conflicting QCs have been
|
|
// detected. Forks cannot recover from this exception.
|
|
// - All other errors are potential symptoms of bugs or state corruption.
|
|
func (f *Forks[StateT, VoteT]) checkForByzantineEvidence(
|
|
state *models.State[StateT],
|
|
) error {
|
|
err := f.EnsureStateIsValidExtension(state)
|
|
if err != nil {
|
|
return fmt.Errorf("consistency check on state failed: %w", err)
|
|
}
|
|
err = f.checkForConflictingQCs(&state.ParentQuorumCertificate)
|
|
if err != nil {
|
|
return fmt.Errorf("checking QC for conflicts failed: %w", err)
|
|
}
|
|
f.checkForDoubleProposal(state)
|
|
return nil
|
|
}
|
|
|
|
// checkForConflictingQCs checks if QC conflicts with a stored Quorum
|
|
// Certificate. In case a conflicting QC is found, an
|
|
// ByzantineThresholdExceededError is returned. Two Quorum Certificates q1 and
|
|
// q2 are defined as conflicting iff:
|
|
//
|
|
// q1.Rank == q2.Rank AND q1.Identifier ≠ q2.Identifier
|
|
//
|
|
// This means there are two Quorums for conflicting states at the same rank.
|
|
// Per 'Observation 1' from the Jolteon paper https://arxiv.org/pdf/2106.10362v1.pdf,
|
|
// two conflicting QCs can exist if and only if the Byzantine threshold is
|
|
// exceeded.
|
|
// Error returns:
|
|
// - models.ByzantineThresholdExceededError if conflicting QCs have been
|
|
// detected. Forks cannot recover from this exception.
|
|
// - All other errors are potential symptoms of bugs or state corruption.
|
|
func (f *Forks[StateT, VoteT]) checkForConflictingQCs(
|
|
qc *models.QuorumCertificate,
|
|
) error {
|
|
it := f.forest.GetVerticesAtLevel((*qc).GetRank())
|
|
for it.HasNext() {
|
|
otherState := it.NextVertex() // by construction, must have same rank as qc.Rank
|
|
if (*qc).Identity() != otherState.VertexID() {
|
|
// * we have just found another state at the same rank number as qc.Rank
|
|
// but with different hash
|
|
// * if this state has a child c, this child will have
|
|
// c.qc.rank = parentRank
|
|
// c.qc.ID != parentIdentifier
|
|
// => conflicting qc
|
|
otherChildren := f.forest.GetChildren(otherState.VertexID())
|
|
if otherChildren.HasNext() {
|
|
otherChild := otherChildren.NextVertex().(*StateContainer[StateT]).GetState()
|
|
conflictingQC := otherChild.ParentQuorumCertificate
|
|
return models.ByzantineThresholdExceededError{Evidence: fmt.Sprintf(
|
|
"conflicting QCs at rank %d: %x and %x",
|
|
(*qc).GetRank(), (*qc).Identity(), conflictingQC.Identity(),
|
|
)}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkForDoubleProposal checks if the input proposal is a double proposal.
|
|
// A double proposal occurs when two proposals with the same rank exist in
|
|
// Forks. If there is a double proposal, notifier.OnDoubleProposeDetected is
|
|
// triggered.
|
|
func (f *Forks[StateT, VoteT]) checkForDoubleProposal(
|
|
state *models.State[StateT],
|
|
) {
|
|
it := f.forest.GetVerticesAtLevel(state.Rank)
|
|
for it.HasNext() {
|
|
otherVertex := it.NextVertex() // by construction, must have same rank as state
|
|
otherState := otherVertex.(*StateContainer[StateT]).GetState()
|
|
if state.Identifier != otherState.Identifier {
|
|
f.notifier.OnDoubleProposeDetected(state, otherState)
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkForAdvancingFinalization checks whether observing certifiedState leads
|
|
// to progress of finalization. This function should be called every time a new
|
|
// state is added to Forks. If the new state is the head of a 2-chain satisfying
|
|
// the finalization rule, we update `Forks.finalityProof` to the new latest
|
|
// finalized state. Calling this method with previously-processed states leaves
|
|
// the consensus state invariant.
|
|
// UNVALIDATED: assumes that relevant state properties are consistent with
|
|
// previous states
|
|
// Error returns:
|
|
// - models.MissingStateError if the parent does not exist in the forest (but
|
|
// is above the pruned rank). From the perspective of Forks, this error is
|
|
// benign (no-op).
|
|
// - models.ByzantineThresholdExceededError in case we detect a finalization
|
|
// fork (violating a foundational consensus guarantee). This indicates that
|
|
// there are 1/3+ Byzantine nodes (weighted by seniority) in the network,
|
|
// breaking the safety guarantees of HotStuff (or there is a critical bug /
|
|
// data corruption). Forks cannot recover from this exception.
|
|
// - generic error in case of unexpected bug or internal state corruption
|
|
func (f *Forks[StateT, VoteT]) checkForAdvancingFinalization(
|
|
certifiedState *models.CertifiedState[StateT],
|
|
) error {
|
|
// We prune all states in forest which are below the most recently finalized
|
|
// state. Hence, we have a pruned ancestry if and only if either of the
|
|
// following conditions applies:
|
|
// (a) If a state's parent rank (i.e. state.QC.Rank) is below the most
|
|
// recently finalized state.
|
|
// (b) If a state's rank is equal to the most recently finalized state.
|
|
// Caution:
|
|
// * Under normal operation, case (b) is covered by the logic for case (a)
|
|
// * However, the existence of a genesis state requires handling case (b)
|
|
// explicitly:
|
|
// The root state is specified and trusted by the node operator. If the root
|
|
// state is the genesis state, it might not contain a QC pointing to a
|
|
// parent (as there is no parent). In this case, condition (a) cannot be
|
|
// evaluated.
|
|
lastFinalizedRank := f.FinalizedRank()
|
|
if (certifiedState.Rank() <= lastFinalizedRank) ||
|
|
(certifiedState.State.ParentQuorumCertificate.GetRank() < lastFinalizedRank) {
|
|
// Repeated states are expected during normal operations. We enter this code
|
|
// state if and only if the parent's rank is _below_ the last finalized
|
|
// state. It is straight forward to show:
|
|
// Lemma: Let B be a state whose 2-chain reaches beyond the last finalized
|
|
// state => B will not update the locked or finalized state
|
|
return nil
|
|
}
|
|
|
|
// retrieve parent; always expected to succeed, because we passed the checks
|
|
// above
|
|
qcForParent := certifiedState.State.ParentQuorumCertificate
|
|
parentVertex, parentStateKnown := f.forest.GetVertex(
|
|
qcForParent.Identity(),
|
|
)
|
|
if !parentStateKnown {
|
|
return models.MissingStateError{
|
|
Rank: qcForParent.GetRank(),
|
|
Identifier: qcForParent.Identity(),
|
|
}
|
|
}
|
|
parentState := parentVertex.(*StateContainer[StateT]).GetState()
|
|
|
|
// Note: we assume that all stored states pass
|
|
// Forks.EnsureStateIsValidExtension(state); specifically, that state's
|
|
// RankNumber is strictly monotonically increasing which is enforced by
|
|
// LevelledForest.VerifyVertex(...)
|
|
// We denote:
|
|
// * a DIRECT 1-chain as '<-'
|
|
// * a general 1-chain as '<~' (direct or indirect)
|
|
// Jolteon's rule for finalizing `parentState` is
|
|
// parentState <- State <~ certifyingQC (i.e. a DIRECT 1-chain PLUS
|
|
// ╰─────────────────────╯ any 1-chain)
|
|
// certifiedState
|
|
// Hence, we can finalize `parentState` as head of a 2-chain,
|
|
// if and only if `State.Rank` is exactly 1 higher than the rank of
|
|
// `parentState`
|
|
if parentState.Rank+1 != certifiedState.Rank() {
|
|
return nil
|
|
}
|
|
|
|
// `parentState` is now finalized:
|
|
// * While Forks is single-threaded, there is still the possibility of
|
|
// reentrancy. Specifically, the consumers of our finalization events are
|
|
// served by the goroutine executing Forks. It is conceivable that a
|
|
// consumer might access Forks and query the latest finalization proof.
|
|
// This would be legal, if the component supplying the goroutine to Forks
|
|
// also consumes the notifications.
|
|
// * Therefore, for API safety, we want to first update Fork's
|
|
// `finalityProof` before we emit any notifications.
|
|
|
|
// Advancing finalization step (i): we collect all states for finalization (no
|
|
// notifications are emitted)
|
|
statesToBeFinalized, err := f.collectStatesForFinalization(&qcForParent)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"advancing finalization to state %x from rank %d failed: %w",
|
|
qcForParent.Identity(),
|
|
qcForParent.GetRank(),
|
|
err,
|
|
)
|
|
}
|
|
|
|
// Advancing finalization step (ii): update `finalityProof` and prune
|
|
// `LevelledForest`
|
|
f.finalityProof = &consensus.FinalityProof[StateT]{
|
|
State: parentState,
|
|
CertifiedChild: certifiedState,
|
|
}
|
|
err = f.forest.PruneUpToLevel(f.FinalizedRank())
|
|
if err != nil {
|
|
return fmt.Errorf("pruning levelled forest failed unexpectedly: %w", err)
|
|
}
|
|
|
|
// Advancing finalization step (iii): iterate over the states from (i) and
|
|
// emit finalization events
|
|
for _, b := range statesToBeFinalized {
|
|
// first notify other critical components about finalized state - all errors
|
|
// returned here are fatal exceptions
|
|
err = f.finalizationCallback.MakeFinal(b.Identifier)
|
|
if err != nil {
|
|
return fmt.Errorf("finalization error in other component: %w", err)
|
|
}
|
|
|
|
// notify less important components about finalized state
|
|
f.notifier.OnFinalizedState(b)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// collectStatesForFinalization collects and returns all newly finalized states
|
|
// up to (and including) the state pointed to by `qc`. The states are listed in
|
|
// order of increasing height.
|
|
// Error returns:
|
|
// - models.ByzantineThresholdExceededError in case we detect a finalization
|
|
// fork (violating a foundational consensus guarantee). This indicates that
|
|
// there are 1/3+ Byzantine nodes (weighted by seniority) in the network,
|
|
// breaking the safety guarantees of HotStuff (or there is a critical bug /
|
|
// data corruption). Forks cannot recover from this exception.
|
|
// - generic error in case of bug or internal state corruption
|
|
func (f *Forks[StateT, VoteT]) collectStatesForFinalization(
|
|
qc *models.QuorumCertificate,
|
|
) ([]*models.State[StateT], error) {
|
|
lastFinalized := f.FinalizedState()
|
|
if (*qc).GetRank() < lastFinalized.Rank {
|
|
return nil, models.ByzantineThresholdExceededError{Evidence: fmt.Sprintf(
|
|
"finalizing state with rank %d which is lower than previously finalized state at rank %d",
|
|
(*qc).GetRank(), lastFinalized.Rank,
|
|
)}
|
|
}
|
|
if (*qc).GetRank() == lastFinalized.Rank { // no new states to be finalized
|
|
return nil, nil
|
|
}
|
|
|
|
// Collect all states that are pending finalization in slice. While we crawl
|
|
// the states starting from the newest finalized state backwards (decreasing
|
|
// ranks), we would like to return them in order of _increasing_ rank.
|
|
// Therefore, we fill the slice starting with the highest index.
|
|
l := (*qc).GetRank() - lastFinalized.Rank // l is an upper limit to the number of states that can be maximally finalized
|
|
statesToBeFinalized := make([]*models.State[StateT], l)
|
|
for (*qc).GetRank() > lastFinalized.Rank {
|
|
b, ok := f.GetState((*qc).Identity())
|
|
if !ok {
|
|
return nil, fmt.Errorf(
|
|
"failed to get state (rank=%d, stateID=%x) for finalization",
|
|
(*qc).GetRank(),
|
|
(*qc).Identity(),
|
|
)
|
|
}
|
|
l--
|
|
statesToBeFinalized[l] = b
|
|
qc = &b.ParentQuorumCertificate // move to parent
|
|
}
|
|
// Now, `l` is the index where we stored the oldest state that should be
|
|
// finalized. Note that `l` might be larger than zero, if some ranks have no
|
|
// finalized states. Hence, `statesToBeFinalized` might start with nil
|
|
// entries, which we remove:
|
|
statesToBeFinalized = statesToBeFinalized[l:]
|
|
|
|
// qc should now point to the latest finalized state. Otherwise, the
|
|
// consensus committee is compromised (or we have a critical internal bug).
|
|
if (*qc).GetRank() < lastFinalized.Rank {
|
|
return nil, models.ByzantineThresholdExceededError{Evidence: fmt.Sprintf(
|
|
"finalizing state with rank %d which is lower than previously finalized state at rank %d",
|
|
(*qc).GetRank(), lastFinalized.Rank,
|
|
)}
|
|
}
|
|
if (*qc).GetRank() == lastFinalized.Rank &&
|
|
lastFinalized.Identifier != (*qc).Identity() {
|
|
return nil, models.ByzantineThresholdExceededError{Evidence: fmt.Sprintf(
|
|
"finalizing states with rank %d at conflicting forks: %x and %x",
|
|
(*qc).GetRank(), (*qc).Identity(), lastFinalized.Identifier,
|
|
)}
|
|
}
|
|
|
|
return statesToBeFinalized, nil
|
|
}
|