mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
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]>
169 lines
3.7 KiB
Go
169 lines
3.7 KiB
Go
// 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()
|
||
}
|