Files
consensus/engine/engine.go
T
zeekayandHanzo Dev 80569ef0e8 alpha means alpha distinct validators, on every road that reaches it
Four ways the tally could be reached by something that is not a quorum.

A peer that is not a validator could vote. The signed path never had to ask:
VerifyVote resolves the voter's key out of the set at the block's epoch, so an
id that is not in the set has no key and its vote dies there. The
transport-authenticated path has no signature to resolve and the tallies are
keyed by NodeID, so it counted distinct PEERS. A peer need not be a validator to
connect, and its NodeID is a hash of a certificate it makes for itself, so
distinct peers are free to mint. Worst on the reject road, which has no cert in
front of it and commits inline.

One node could vote repeatedly. The engine behind consensus.NewChain appended
votes to a slice and compared its length to alpha, so a single node reached
quorum alone. Votes are now keyed by voter: a second vote from a node replaces
its first. The Config comment called alpha "the number of votes", which is the
same mistake written as prose.

A cert for one block could finalize another. The outer envelope ids are the one
part of a position no signature covers — the signed identity is the inner
canonical — so the id we looked the block up by proves nothing. Re-point it at a
sibling at the same height and a genuine cert asks us to accept a block it does
not name. The arm that finds a wrapper now checks the canonical, which the arm
that finds none already did.

And a node could go silent on a block it still holds. Abandonment stops
re-soliciting peers who cannot vote; the slot sweep read it as "do not vote
either", so the settle pass never offered the slot and the height was never
signed while the block sat pending and undecided. It is also a per-node clock
decision, so it dropped nodes out of the tally at different times. One meaning
now, and no flag to select the other.

Also: a seed below the node's own applied head is refused rather than planting
the certified frontier under it, and the phantom-floor reconcile is deleted —
a repair for a bug already fixed, called by nothing, that asked an operator to
verify a fleet-wide height by hand before it would prune the equivocation
bindings.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-15 23:30:25 -07:00

169 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2019-2025, Lux Industries Inc All rights reserved.
// See the file LICENSE for licensing terms.
package engine
import (
"context"
"sync"
"time"
"github.com/luxfi/consensus/types"
"github.com/luxfi/ids"
)
// Engine is the main consensus engine interface
type Engine interface {
// Add a new block to the consensus
Add(ctx context.Context, block *types.Block) error
// RecordVote records a vote for a block
RecordVote(ctx context.Context, vote *types.Vote) error
// IsAccepted returns whether a block has been accepted
IsAccepted(id types.ID) bool
// GetStatus returns the status of a block
GetStatus(id types.ID) types.Status
// Start the consensus engine
Start(ctx context.Context) error
// Stop the consensus engine
Stop() error
}
// Chain represents a linear blockchain consensus engine
type Chain struct {
mu sync.RWMutex
// Configuration
config types.Config
// Block storage
blocks map[types.ID]*types.Block
// One vote per voter per block. α means α DISTINCT validators agreed, so the
// voter is the key: a second vote from the same node replaces its first
// rather than counting twice. A slice here let one node reach quorum alone.
votes map[types.ID]map[types.NodeID]types.Vote
status map[types.ID]types.Status
// Consensus state
lastAccepted types.ID
height uint64
// Network
validators []types.NodeID
}
// NewChain creates a new chain consensus engine
func NewChain(config types.Config) *Chain {
return &Chain{
config: config,
blocks: make(map[types.ID]*types.Block),
votes: make(map[types.ID]map[types.NodeID]types.Vote),
status: make(map[types.ID]types.Status),
lastAccepted: types.GenesisID,
}
}
// Add adds a new block to the chain
func (c *Chain) Add(ctx context.Context, block *types.Block) error {
c.mu.Lock()
defer c.mu.Unlock()
// Store the block
c.blocks[block.ID] = block
c.status[block.ID] = types.StatusProcessing
// Initialize vote tracking
if c.votes[block.ID] == nil {
c.votes[block.ID] = map[types.NodeID]types.Vote{}
}
return nil
}
// RecordVote records a vote for a block
func (c *Chain) RecordVote(ctx context.Context, vote *types.Vote) error {
c.mu.Lock()
defer c.mu.Unlock()
// Check if block exists
if _, exists := c.blocks[vote.BlockID]; !exists {
return types.ErrBlockNotFound
}
// Add vote
c.votes[vote.BlockID][vote.Voter] = *vote
// Check if we have quorum
if len(c.votes[vote.BlockID]) >= c.config.Alpha {
c.acceptBlock(vote.BlockID)
}
return nil
}
// IsAccepted returns whether a block has been accepted
func (c *Chain) IsAccepted(id types.ID) bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.status[id] == types.StatusAccepted
}
// GetStatus returns the status of a block
func (c *Chain) GetStatus(id types.ID) types.Status {
c.mu.RLock()
defer c.mu.RUnlock()
status, exists := c.status[id]
if !exists {
return types.StatusUnknown
}
return status
}
// Start starts the consensus engine
func (c *Chain) Start(ctx context.Context) error {
// Initialize genesis block
genesis := &types.Block{
ID: types.GenesisID,
ParentID: ids.Empty,
Height: 0,
Time: time.Now(),
}
c.mu.Lock()
c.blocks[genesis.ID] = genesis
c.status[genesis.ID] = types.StatusAccepted
c.lastAccepted = genesis.ID
c.mu.Unlock()
return nil
}
// Stop stops the consensus engine
func (c *Chain) Stop() error {
// Cleanup resources if needed
return nil
}
// acceptBlock marks a block as accepted
func (c *Chain) acceptBlock(id types.ID) {
c.status[id] = types.StatusAccepted
if block, exists := c.blocks[id]; exists {
if block.Height > c.height {
c.height = block.Height
c.lastAccepted = id
}
}
}
// DefaultConfig returns the default chain configuration
func DefaultConfig() types.Config {
return types.DefaultConfig()
}