ceremonyclient/consensus/eventhandler/event_handler.go
Cassandra Heart bc10bd497c
v2.1.0.7
2025-11-13 23:37:09 -06:00

826 lines
28 KiB
Go

package eventhandler
import (
"context"
"errors"
"fmt"
"time"
"source.quilibrium.com/quilibrium/monorepo/consensus"
"source.quilibrium.com/quilibrium/monorepo/consensus/models"
)
// EventHandler is the main handler for individual events that trigger state
// transition. It exposes API to handle one event at a time synchronously.
// EventHandler is *not concurrency safe*. Please use the EventLoop to ensure
// that only a single go-routine executes the EventHandler's algorithms.
// EventHandler is implemented in event-driven way, it reacts to incoming events
// and performs certain actions. It doesn't perform any actions on its own.
// There are 3 main responsibilities of EventHandler, vote, propose, timeout.
// There are specific scenarios that lead to each of those actions.
// - create vote: voting logic is triggered by OnReceiveProposal, after
// receiving proposal we have all required information to create a valid
// vote. Compliance engine makes sure that we receive proposals, whose
// parents are known. Creating a vote can be triggered ONLY by receiving
// proposal.
// - create timeout: creating models.TimeoutState is triggered by
// OnLocalTimeout, after reaching deadline for current round. EventHandler
// gets notified about it and has to create a models.TimeoutState and
// broadcast it to other replicas. Creating a TO can be triggered by
// reaching round deadline or triggered as part of Bracha broadcast when
// superminority of replicas have contributed to TC creation and created a
// partial TC.
// - create a proposal: proposing logic is more complicated. Creating a
// proposal is triggered by the EventHandler receiving a QC or TC that
// induces a rank change to a rank where the replica is primary. As an edge
// case, the EventHandler can receive a QC or TC that triggers the rank
// change, but we can't create a proposal in case we are missing parent
// state the newest QC refers to. In case we already have the QC, but are
// still missing the respective parent, OnReceiveProposal can trigger the
// proposing logic as well, but only when receiving proposal for rank lower
// than active rank. To summarize, to make a valid proposal for rank N we
// need to have a QC or TC for N-1 and know the proposal with stateID
// NewestQC.Identifier.
//
// Not concurrency safe.
type EventHandler[
StateT models.Unique,
VoteT models.Unique,
PeerIDT models.Unique,
CollectedT models.Unique,
] struct {
tracer consensus.TraceLogger
paceMaker consensus.Pacemaker
stateProducer consensus.StateProducer[StateT, VoteT]
forks consensus.Forks[StateT]
store consensus.ConsensusStore[VoteT]
committee consensus.Replicas
safetyRules consensus.SafetyRules[StateT, VoteT]
notifier consensus.Consumer[StateT, VoteT]
}
var _ consensus.EventHandler[*nilUnique, *nilUnique] = (*EventHandler[
*nilUnique, *nilUnique, *nilUnique, *nilUnique,
])(nil)
// NewEventHandler creates an EventHandler instance with initial components.
func NewEventHandler[
StateT models.Unique,
VoteT models.Unique,
PeerIDT models.Unique,
CollectedT models.Unique,
](
paceMaker consensus.Pacemaker,
stateProducer consensus.StateProducer[StateT, VoteT],
forks consensus.Forks[StateT],
store consensus.ConsensusStore[VoteT],
committee consensus.Replicas,
safetyRules consensus.SafetyRules[StateT, VoteT],
notifier consensus.Consumer[StateT, VoteT],
tracer consensus.TraceLogger,
) (*EventHandler[StateT, VoteT, PeerIDT, CollectedT], error) {
e := &EventHandler[StateT, VoteT, PeerIDT, CollectedT]{
paceMaker: paceMaker,
stateProducer: stateProducer,
forks: forks,
store: store,
safetyRules: safetyRules,
committee: committee,
notifier: notifier,
tracer: tracer,
}
return e, nil
}
// OnReceiveQuorumCertificate processes a valid qc constructed by internal vote
// aggregator or discovered in TimeoutState. All inputs should be validated
// before feeding into this function. Assuming trusted data. No errors are
// expected during normal operation.
func (e *EventHandler[
StateT,
VoteT,
PeerIDT,
CollectedT,
]) OnReceiveQuorumCertificate(qc models.QuorumCertificate) error {
curRank := e.paceMaker.CurrentRank()
e.tracer.Trace(
"received QC",
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param("qc_rank", qc.GetRank()),
consensus.IdentityParam("state_id", qc.Identity()),
)
e.notifier.OnReceiveQuorumCertificate(curRank, qc)
defer e.notifier.OnEventProcessed()
newRankEvent, err := e.paceMaker.ReceiveQuorumCertificate(qc)
if err != nil {
return fmt.Errorf("could not process QC: %w", err)
}
if newRankEvent == nil {
e.tracer.Trace("QC didn't trigger rank change, nothing to do")
return nil
}
// current rank has changed, go to new rank
e.tracer.Trace("QC triggered rank change, starting new rank now")
return e.proposeForNewRankIfPrimary()
}
// OnReceiveTimeoutCertificate processes a valid tc constructed by internal
// timeout aggregator, discovered in TimeoutState or broadcast over the network.
// All inputs should be validated before feeding into this function. Assuming
// trusted data. No errors are expected during normal operation.
func (e *EventHandler[
StateT,
VoteT,
PeerIDT,
CollectedT,
]) OnReceiveTimeoutCertificate(tc models.TimeoutCertificate) error {
curRank := e.paceMaker.CurrentRank()
e.tracer.Trace(
"received TC",
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param("tc_rank", tc.GetRank()),
consensus.Uint64Param(
"tc_newest_qc_rank",
tc.GetLatestQuorumCert().GetRank(),
),
consensus.IdentityParam(
"tc_newest_qc_state_id",
tc.GetLatestQuorumCert().Identity(),
),
)
e.notifier.OnReceiveTimeoutCertificate(curRank, tc)
defer e.notifier.OnEventProcessed()
newRankEvent, err := e.paceMaker.ReceiveTimeoutCertificate(tc)
if err != nil {
return fmt.Errorf("could not process TC for rank %d: %w", tc.GetRank(), err)
}
if newRankEvent == nil {
e.tracer.Trace("TC didn't trigger rank change, nothing to do",
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param("tc_rank", tc.GetRank()),
consensus.Uint64Param(
"tc_newest_qc_rank",
tc.GetLatestQuorumCert().GetRank(),
),
consensus.IdentityParam(
"tc_newest_qc_state_id",
tc.GetLatestQuorumCert().Identity(),
))
return nil
}
// current rank has changed, go to new rank
e.tracer.Trace("TC triggered rank change, starting new rank now",
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param("tc_rank", tc.GetRank()),
consensus.Uint64Param(
"tc_newest_qc_rank",
tc.GetLatestQuorumCert().GetRank(),
),
consensus.IdentityParam(
"tc_newest_qc_state_id",
tc.GetLatestQuorumCert().Identity(),
))
return e.proposeForNewRankIfPrimary()
}
// OnReceiveProposal processes a state proposal received from another HotStuff
// consensus participant.
// All inputs should be validated before feeding into this function. Assuming
// trusted data. No errors are expected during normal operation.
func (e *EventHandler[
StateT,
VoteT,
PeerIDT,
CollectedT,
]) OnReceiveProposal(proposal *models.SignedProposal[StateT, VoteT]) error {
state := proposal.State
curRank := e.paceMaker.CurrentRank()
e.tracer.Trace(
"proposal received from compliance engine",
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param("state_rank", state.Rank),
consensus.IdentityParam("state_id", state.Identifier),
consensus.Uint64Param("qc_rank", state.ParentQuorumCertificate.GetRank()),
consensus.IdentityParam("proposer_id", state.ProposerID),
)
e.notifier.OnReceiveProposal(curRank, proposal)
defer e.notifier.OnEventProcessed()
// ignore stale proposals
if (*state).Rank < e.forks.FinalizedRank() {
e.tracer.Trace(
"stale proposal",
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param("state_rank", state.Rank),
consensus.IdentityParam("state_id", state.Identifier),
consensus.Uint64Param("qc_rank", state.ParentQuorumCertificate.GetRank()),
consensus.IdentityParam("proposer_id", state.ProposerID),
)
return nil
}
// store the state.
err := e.forks.AddValidatedState(proposal.State)
if err != nil {
return fmt.Errorf(
"cannot add proposal to forks (%x): %w",
state.Identifier,
err,
)
}
_, err = e.paceMaker.ReceiveQuorumCertificate(
proposal.State.ParentQuorumCertificate,
)
if err != nil {
return fmt.Errorf(
"could not process QC for state %x: %w",
state.Identifier,
err,
)
}
_, err = e.paceMaker.ReceiveTimeoutCertificate(
proposal.PreviousRankTimeoutCertificate,
)
if err != nil {
return fmt.Errorf(
"could not process TC for state %x: %w",
state.Identifier,
err,
)
}
// if the state is for the current rank, then try voting for this state
err = e.processStateForCurrentRank(proposal)
if err != nil {
return fmt.Errorf("failed processing current state: %w", err)
}
e.tracer.Trace(
"proposal processed from compliance engine",
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param("state_rank", state.Rank),
consensus.IdentityParam("state_id", state.Identifier),
consensus.Uint64Param("qc_rank", state.ParentQuorumCertificate.GetRank()),
consensus.IdentityParam("proposer_id", state.ProposerID),
)
// nothing to do if this proposal is for current rank
if proposal.State.Rank == e.paceMaker.CurrentRank() {
return nil
}
return e.proposeForNewRankIfPrimary()
}
// TimeoutChannel returns the channel for subscribing the waiting timeout on
// receiving state or votes for the current rank.
func (e *EventHandler[
StateT,
VoteT,
PeerIDT,
CollectedT,
]) TimeoutChannel() <-chan time.Time {
return e.paceMaker.TimeoutCh()
}
// OnLocalTimeout handles a local timeout event by creating a
// models.TimeoutState and broadcasting it. No errors are expected during normal
// operation.
func (e *EventHandler[
StateT,
VoteT,
PeerIDT,
CollectedT,
]) OnLocalTimeout() error {
curRank := e.paceMaker.CurrentRank()
e.tracer.Trace(
"timeout received from event loop",
consensus.Uint64Param("current_rank", curRank),
)
e.notifier.OnLocalTimeout(curRank)
defer e.notifier.OnEventProcessed()
err := e.broadcastTimeoutStateIfAuthorized()
if err != nil {
return fmt.Errorf(
"unexpected exception while processing timeout in rank %d: %w",
curRank,
err,
)
}
return nil
}
// OnPartialTimeoutCertificateCreated handles notification produces by the
// internal timeout aggregator. If the notification is for the current rank, a
// corresponding models.TimeoutState is broadcast to the consensus committee. No
// errors are expected during normal operation.
func (e *EventHandler[
StateT,
VoteT,
PeerIDT,
CollectedT,
]) OnPartialTimeoutCertificateCreated(
partialTC *consensus.PartialTimeoutCertificateCreated,
) error {
curRank := e.paceMaker.CurrentRank()
previousRankTimeoutCert := partialTC.PriorRankTimeoutCertificate
e.tracer.Trace(
"constructed partial TC",
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param(
"qc_rank",
partialTC.NewestQuorumCertificate.GetRank(),
),
)
e.notifier.OnPartialTimeoutCertificate(curRank, partialTC)
defer e.notifier.OnEventProcessed()
// process QC, this might trigger rank change
_, err := e.paceMaker.ReceiveQuorumCertificate(
partialTC.NewestQuorumCertificate,
)
if err != nil {
return fmt.Errorf("could not process newest QC: %w", err)
}
// process TC, this might trigger rank change
_, err = e.paceMaker.ReceiveTimeoutCertificate(previousRankTimeoutCert)
if err != nil {
return fmt.Errorf(
"could not process TC for rank %d: %w",
previousRankTimeoutCert.GetRank(),
err,
)
}
// NOTE: in other cases when we have observed a rank change we will trigger
// proposing logic, this is desired logic for handling proposal, QC and TC.
// However, observing a partial TC means that superminority have timed out and
// there was at least one honest replica in that set. Honest replicas will
// never vote after timing out for current rank meaning we won't be able to
// collect supermajority of votes for a proposal made after observing partial
// TC.
// by definition, we are allowed to produce timeout state if we have received
// partial TC for current rank
if e.paceMaker.CurrentRank() != partialTC.Rank {
return nil
}
e.tracer.Trace(
"partial TC generated for current rank, broadcasting timeout",
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param(
"qc_rank",
partialTC.NewestQuorumCertificate.GetRank(),
),
)
err = e.broadcastTimeoutStateIfAuthorized()
if err != nil {
return fmt.Errorf(
"unexpected exception while processing partial TC in rank %d: %w",
partialTC.Rank,
err,
)
}
return nil
}
// Start starts the event handler. No errors are expected during normal
// operation. CAUTION: EventHandler is not concurrency safe. The Start method
// must be executed by the same goroutine that also calls the other business
// logic methods, or concurrency safety has to be implemented externally.
func (e *EventHandler[
StateT,
VoteT,
PeerIDT,
CollectedT,
]) Start(ctx context.Context) error {
e.notifier.OnStart(e.paceMaker.CurrentRank())
defer e.notifier.OnEventProcessed()
e.paceMaker.Start(ctx)
err := e.proposeForNewRankIfPrimary()
if err != nil {
return fmt.Errorf("could not start new rank: %w", err)
}
return nil
}
// broadcastTimeoutStateIfAuthorized attempts to generate a
// models.TimeoutState, adds it to `timeoutAggregator` and broadcasts it to the
// consensus commettee. We check, whether this node, at the current rank, is
// part of the consensus committee. Otherwise, this method is functionally a
// no-op. For example, right after an rank switchover a consensus node might
// still be online but not part of the _active_ consensus committee anymore.
// Consequently, it should not broadcast timeouts anymore. No errors are
// expected during normal operation.
func (e *EventHandler[
StateT,
VoteT,
PeerIDT,
CollectedT,
]) broadcastTimeoutStateIfAuthorized() error {
curRank := e.paceMaker.CurrentRank()
newestQC := e.paceMaker.LatestQuorumCertificate()
previousRankTimeoutCert := e.paceMaker.PriorRankTimeoutCertificate()
if newestQC.GetRank()+1 == curRank {
// in case last rank has ended with QC and TC, make sure that only QC is
// included otherwise such timeout is invalid. This case is possible if TC
// has included QC with the same rank as the TC itself, meaning that
// newestQC.Rank == previousRankTimeoutCert.Rank
previousRankTimeoutCert = nil
}
timeout, err := e.safetyRules.ProduceTimeout(
curRank,
newestQC,
previousRankTimeoutCert,
)
if err != nil {
if models.IsNoTimeoutError(err) {
e.tracer.Error(
"not generating timeout as this node is not part of the active committee",
err,
consensus.Uint64Param("current_rank", curRank),
)
return nil
}
return fmt.Errorf("could not produce timeout: %w", err)
}
// raise a notification to broadcast timeout
e.notifier.OnOwnTimeout(timeout)
e.tracer.Trace(
"broadcast TimeoutState done",
consensus.Uint64Param("current_rank", curRank),
)
return nil
}
// proposeForNewRankIfPrimary will only be called when we may be able to propose
// a state, after processing a new event.
// - after entering a new rank as a result of processing a QC or TC, then we
// may propose for the newly entered rank
// - after receiving a proposal (but not changing rank), if that proposal is
// referenced by our highest known QC, and the proposal was previously
// unknown, then we can propose a state in the current rank
//
// Enforced INVARIANTS:
// - There will at most be `OnOwnProposal` notification emitted for ranks
// where this node is the leader, and none if another node is the leader.
// This holds irrespective of restarts. Formally, this prevents proposal
// equivocation.
//
// It reads the current rank, and generates a proposal if we are the leader.
// No errors are expected during normal operation.
func (e *EventHandler[
StateT,
VoteT,
PeerIDT,
CollectedT,
]) proposeForNewRankIfPrimary() error {
start := time.Now() // track the start time
curRank := e.paceMaker.CurrentRank()
e.tracer.Trace(
"deciding to propose",
consensus.Uint64Param("current_rank", curRank),
consensus.IdentityParam("self", e.committee.Self()),
)
currentLeader, err := e.committee.LeaderForRank(curRank)
if err != nil {
return fmt.Errorf(
"failed to determine primary for new rank %d: %w",
curRank,
err,
)
}
finalizedRank := e.forks.FinalizedRank()
e.notifier.OnCurrentRankDetails(curRank, finalizedRank, currentLeader)
// check that I am the primary for this rank
if e.committee.Self() != currentLeader {
e.tracer.Trace(
"not current leader, waiting",
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param("finalized_rank", finalizedRank),
consensus.IdentityParam("leader_id", currentLeader),
)
return nil
}
// attempt to generate proposal:
newestQC := e.paceMaker.LatestQuorumCertificate()
previousRankTimeoutCert := e.paceMaker.PriorRankTimeoutCertificate()
_, found := e.forks.GetState(newestQC.Identity())
if !found {
// we don't know anything about state referenced by our newest QC, in this
// case we can't create a valid proposal since we can't guarantee validity
// of state payload.
e.tracer.Trace(
"haven't synced the latest state yet; can't propose",
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param("finalized_rank", finalizedRank),
consensus.IdentityParam("leader_id", currentLeader),
)
return nil
}
e.tracer.Trace(
"generating proposal as leader",
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param("finalized_rank", finalizedRank),
consensus.IdentityParam("leader_id", currentLeader),
)
// Sanity checks to make sure that resulting proposal is valid:
// In its proposal, the leader for rank N needs to present evidence that it
// has legitimately entered rank N. As evidence, we include a QC or TC for
// rank N-1, which should always be available as the PaceMaker advances to
// rank N only after observing a QC or TC from rank N-1. Moreover, QC and TC
// are always processed together. As EventHandler is strictly single-threaded
// without reentrancy, we must have a QC or TC for the prior rank (curRank-1).
// Failing one of these sanity checks is a symptom of state corruption or a
// severe implementation bug.
if newestQC.GetRank()+1 != curRank {
if previousRankTimeoutCert == nil {
return fmt.Errorf("possible state corruption, expected previousRankTimeoutCert to be not nil")
}
if previousRankTimeoutCert.GetRank()+1 != curRank {
return fmt.Errorf(
"possible state corruption, don't have QC(rank=%d) and TC(rank=%d) for previous rank(currentRank=%d)",
newestQC.GetRank(),
previousRankTimeoutCert.GetRank(),
curRank,
)
}
} else {
// In case last rank has ended with QC and TC, make sure that only QC is
// included, otherwise such proposal is invalid. This case is possible if TC
// has included QC with the same rank as the TC itself, meaning that
// newestQC.Rank == previousRankTimeoutCert.Rank
previousRankTimeoutCert = nil
}
// Construct Own SignedProposal
// CAUTION, design constraints:
// (i) We cannot process our own proposal within the `EventHandler` right
// away.
// (ii) We cannot add our own proposal to Forks here right away.
// (iii) Metrics for the PaceMaker/CruiseControl assume that the EventHandler
// is the only caller of `TargetPublicationTime`. Technically,
// `TargetPublicationTime` records the publication delay relative to
// its _latest_ call.
//
// To satisfy all constraints, we construct the proposal here and query
// (once!) its `TargetPublicationTime`. Though, we do _not_ process our own
// states right away and instead ingest them into the EventHandler the same
// way as proposals from other consensus participants. Specifically, on the
// path through the HotStuff state machine leading to state construction, the
// node's own proposal is largely ephemeral. The proposal is handed to the
// `MessageHub` (via the `OnOwnProposal` notification including the
// `TargetPublicationTime`). The `MessageHub` waits until
// `TargetPublicationTime` and only then broadcast the proposal and puts it
// into the EventLoop's queue for inbound states. This is exactly the same way
// as proposals from other nodes are ingested by the `EventHandler`, except
// that we are skipping the ComplianceEngine (assuming that our own proposals
// are protocol-compliant).
//
// Context:
// • On constraint (i): We want to support consensus committees only
// consisting of a *single* node. If the EventHandler internally processed
// the state right away via a direct message call, the call-stack would be
// ever-growing and the node would crash eventually (we experienced this
// with a very early HotStuff implementation). Specifically, if we wanted
// to process the state directly without taking a detour through the
// EventLoop's inbound queue, we would call `OnReceiveProposal` here. The
// function `OnReceiveProposal` would then end up calling
// `proposeForNewRankIfPrimary` (this function) to generate the next
// proposal, which again would result in calling `OnReceiveProposal` and so
// on so forth until the call stack or memory limit is reached and the node
// crashes. This is only a problem for consensus committees of size 1.
// • On constraint (ii): When adding a proposal to Forks, Forks emits a
// `StateIncorporatedEvent` notification, which is observed by Cruise
// Control and would change its state. However, note that Cruise Control
// is trying to estimate the point in time when _other_ nodes are observing
// the proposal. The time when we broadcast the proposal (i.e.
// `TargetPublicationTime`) is a reasonably good estimator, but *not* the
// time the proposer constructed the state (because there is potentially
// still a significant wait until `TargetPublicationTime`).
//
// The current approach is for a node to process its own proposals at the same
// time and through the same code path as proposals from other nodes. This
// satisfies constraints (i) and (ii) and generates very strong consistency,
// from a software design perspective.
// Just hypothetically, if we changed Cruise Control to be notified about
// own state proposals _only_ when they are broadcast (satisfying constraint
// (ii) without relying on the EventHandler), then we could add a proposal to
// Forks here right away. Nevertheless, the restriction remains that we cannot
// process that proposal right away within the EventHandler and instead need
// to put it into the EventLoop's inbound queue to support consensus
// committees of size 1.
stateProposal, err := e.stateProducer.MakeStateProposal(
curRank,
newestQC,
previousRankTimeoutCert,
)
if err != nil {
if models.IsNoVoteError(err) {
e.tracer.Error(
"aborting state proposal to prevent equivocation (likely re-entered proposal logic due to crash)",
err,
consensus.Uint64Param("current_rank", curRank),
consensus.Uint64Param("finalized_rank", finalizedRank),
consensus.IdentityParam("leader_id", currentLeader),
)
return nil
}
return fmt.Errorf(
"can not make state proposal for curRank %d: %w",
curRank,
err,
)
}
targetPublicationTime := e.paceMaker.TargetPublicationTime(
stateProposal.State.Rank,
start,
stateProposal.State.ParentQuorumCertificate.Identity(),
) // determine target publication time
e.tracer.Trace(
"forwarding proposal to communicator for broadcasting",
consensus.Uint64Param("state_rank", stateProposal.State.Rank),
consensus.TimeParam("target_publication", targetPublicationTime),
consensus.IdentityParam("state_id", stateProposal.State.Identifier),
consensus.Uint64Param("parent_rank", newestQC.GetRank()),
consensus.IdentityParam("parent_id", newestQC.Identity()),
consensus.IdentityParam("signer", stateProposal.State.ProposerID),
)
// emit notification with own proposal (also triggers broadcast)
e.notifier.OnOwnProposal(stateProposal, targetPublicationTime)
return nil
}
// processStateForCurrentRank processes the state for the current rank.
// It is called AFTER the state has been stored or found in Forks
// It checks whether to vote for this state.
// No errors are expected during normal operation.
func (e *EventHandler[
StateT,
VoteT,
PeerIDT,
CollectedT,
]) processStateForCurrentRank(
proposal *models.SignedProposal[StateT, VoteT],
) error {
// sanity check that state is really for the current rank:
curRank := e.paceMaker.CurrentRank()
state := proposal.State
if state.Rank != curRank {
// ignore outdated proposals in case we have moved forward
return nil
}
// leader (node ID) for next rank
nextLeader, err := e.committee.LeaderForRank(curRank + 1)
if errors.Is(err, models.ErrRankUnknown) {
// We are attempting process a state in an unknown rank
// This should never happen, because:
// * the compliance layer ensures proposals are passed to the event loop
// strictly after their parent
// * the protocol state ensures that, before incorporating the first state
// of an rank R, either R is known or we have triggered fallback mode - in
// either case the current rank is known
return fmt.Errorf("attempting to process a state for unknown rank")
}
if err != nil {
return fmt.Errorf(
"failed to determine primary for next rank %d: %w",
curRank+1,
err,
)
}
// safetyRules performs all the checks to decide whether to vote for this
// state or not.
err = e.ownVote(proposal, curRank, nextLeader)
if err != nil {
return fmt.Errorf("unexpected error in voting logic: %w", err)
}
return nil
}
// ownVote generates and forwards the own vote, if we decide to vote.
// Any errors are potential symptoms of uncovered edge cases or corrupted
// internal state (fatal). No errors are expected during normal operation.
func (e *EventHandler[
StateT,
VoteT,
PeerIDT,
CollectedT,
]) ownVote(
proposal *models.SignedProposal[StateT, VoteT],
curRank uint64,
nextLeader models.Identity,
) error {
_, found := e.forks.GetState(
proposal.State.ParentQuorumCertificate.Identity(),
)
if !found {
// we don't have parent for this proposal, we can't vote since we can't
// guarantee validity of proposals payload. Strictly speaking this shouldn't
// ever happen because compliance engine makes sure that we receive
// proposals with valid parents.
return fmt.Errorf(
"won't vote for proposal, no parent state for this proposal",
)
}
// safetyRules performs all the checks to decide whether to vote for this
// state or not.
ownVote, err := e.safetyRules.ProduceVote(proposal, curRank)
if err != nil {
if !models.IsNoVoteError(err) {
// unknown error, exit the event loop
return fmt.Errorf("could not produce vote: %w", err)
}
e.tracer.Trace(
"should not vote for this state",
consensus.Uint64Param("state_rank", proposal.State.Rank),
consensus.IdentityParam("state_id", proposal.State.Identifier),
consensus.Uint64Param(
"parent_rank",
proposal.State.ParentQuorumCertificate.GetRank(),
),
consensus.IdentityParam(
"parent_id",
proposal.State.ParentQuorumCertificate.Identity(),
),
consensus.IdentityParam("signer", proposal.State.ProposerID[:]),
)
return nil
}
e.tracer.Trace(
"forwarding vote to compliance engine",
consensus.Uint64Param("state_rank", proposal.State.Rank),
consensus.IdentityParam("state_id", proposal.State.Identifier),
consensus.Uint64Param(
"parent_rank",
proposal.State.ParentQuorumCertificate.GetRank(),
),
consensus.IdentityParam(
"parent_id",
proposal.State.ParentQuorumCertificate.Identity(),
),
consensus.IdentityParam("signer", proposal.State.ProposerID[:]),
)
e.notifier.OnOwnVote(ownVote, nextLeader)
return nil
}
// Type used to satisfy generic arguments in compiler time type assertion check
type nilUnique struct{}
// GetSignature implements models.Unique.
func (n *nilUnique) GetSignature() []byte {
panic("unimplemented")
}
// GetTimestamp implements models.Unique.
func (n *nilUnique) GetTimestamp() uint64 {
panic("unimplemented")
}
// Source implements models.Unique.
func (n *nilUnique) Source() models.Identity {
panic("unimplemented")
}
// Clone implements models.Unique.
func (n *nilUnique) Clone() models.Unique {
panic("unimplemented")
}
// GetRank implements models.Unique.
func (n *nilUnique) GetRank() uint64 {
panic("unimplemented")
}
// Identity implements models.Unique.
func (n *nilUnique) Identity() models.Identity {
panic("unimplemented")
}
var _ models.Unique = (*nilUnique)(nil)