mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
Major changes: - Merge pkg/wire Python types into pkg/python/lux_consensus - Add MCP mesh consensus module (from hanzo-consensus) - Add types.py with Candidate, Vote, Certificate for wire protocol - Bridge functions for AI consensus -> blockchain interop - Keep pkg/wire as Go-only wire protocol types Python SDK now provides: - lux_consensus: Core chain/block/vote types - lux_consensus.types: Wire protocol (Candidate, WireVote, WireCertificate) - lux_consensus.mcp: MCP mesh for agent-to-agent consensus Also includes engine improvements: - Chain syncer with persistence - PQ crypto enhancements - External sequencer finality example - Protocol field/horizon/prism updates All tests passing.
261 lines
6.1 KiB
Go
261 lines
6.1 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package dag
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/luxfi/consensus/config"
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
// Transaction represents a DAG transaction
|
|
type Transaction interface {
|
|
ID() ids.ID
|
|
Parent() ids.ID
|
|
Height() uint64
|
|
Bytes() []byte
|
|
Verify(context.Context) error
|
|
Accept(context.Context) error
|
|
Reject(context.Context) error
|
|
}
|
|
|
|
// Engine defines the DAG consensus engine interface
|
|
type Engine interface {
|
|
// GetVtx gets a vertex by ID
|
|
GetVtx(context.Context, ids.ID) (Transaction, error)
|
|
|
|
// BuildVtx builds a new vertex
|
|
BuildVtx(context.Context) (Transaction, error)
|
|
|
|
// ParseVtx parses a vertex from bytes
|
|
ParseVtx(context.Context, []byte) (Transaction, error)
|
|
|
|
// Start starts the engine
|
|
Start(context.Context, uint32) error
|
|
|
|
// Shutdown shuts down the engine
|
|
Shutdown(context.Context) error
|
|
}
|
|
|
|
// dagEngine implements real DAG consensus using Lux protocols (Photon → Wave → Prism)
|
|
type dagEngine struct {
|
|
mu sync.RWMutex
|
|
|
|
consensus *DAGConsensus
|
|
params config.Parameters
|
|
bootstrapped bool
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
// Vertex builder
|
|
pendingData [][]byte
|
|
}
|
|
|
|
// New creates a new DAG engine with real Lux consensus
|
|
func New() Engine {
|
|
return NewWithParams(config.DefaultParams())
|
|
}
|
|
|
|
// NewWithParams creates an engine with specific parameters
|
|
func NewWithParams(params config.Parameters) Engine {
|
|
return &dagEngine{
|
|
consensus: NewDAGConsensus(params.K, params.AlphaPreference, int(params.Beta)),
|
|
params: params,
|
|
bootstrapped: false,
|
|
pendingData: make([][]byte, 0),
|
|
}
|
|
}
|
|
|
|
// GetVtx gets a vertex by ID
|
|
func (e *dagEngine) GetVtx(ctx context.Context, id ids.ID) (Transaction, error) {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
vertex, exists := e.consensus.GetVertex(id)
|
|
if !exists {
|
|
return nil, nil
|
|
}
|
|
|
|
return vertex, nil
|
|
}
|
|
|
|
// BuildVtx builds a new vertex from pending data
|
|
func (e *dagEngine) BuildVtx(ctx context.Context) (Transaction, error) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if len(e.pendingData) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Get frontier vertices as parents
|
|
frontier := e.consensus.Frontier()
|
|
if len(frontier) == 0 {
|
|
frontier = []ids.ID{ids.Empty}
|
|
}
|
|
|
|
// Create new vertex ID
|
|
vertexID := ids.GenerateTestID()
|
|
|
|
// Build vertex with first pending data
|
|
data := e.pendingData[0]
|
|
e.pendingData = e.pendingData[1:]
|
|
|
|
vertex := NewVertex(
|
|
vertexID,
|
|
frontier,
|
|
0, // Height calculation would be based on parents
|
|
0, // Timestamp
|
|
data,
|
|
)
|
|
|
|
// Add to consensus
|
|
if err := e.consensus.AddVertex(ctx, vertex); err != nil {
|
|
return nil, fmt.Errorf("failed to add vertex: %w", err)
|
|
}
|
|
|
|
return vertex, nil
|
|
}
|
|
|
|
// ParseVtx parses a vertex from bytes.
|
|
// Format: [32-byte ID][parent count][parents...][8-byte height][8-byte timestamp][data...]
|
|
// Returns nil if the data is not a valid vertex encoding.
|
|
func (e *dagEngine) ParseVtx(ctx context.Context, b []byte) (Transaction, error) {
|
|
if len(b) < 42 { // 32 + 1 + 8 + 1 minimum
|
|
return nil, nil
|
|
}
|
|
|
|
// Parse ID - must not be all zeros
|
|
var id ids.ID
|
|
copy(id[:], b[:32])
|
|
if id == ids.Empty {
|
|
return nil, nil // Invalid: zero ID indicates invalid data
|
|
}
|
|
|
|
// Parse parent count - sanity check
|
|
parentCount := int(b[32])
|
|
if parentCount > 128 { // Unreasonable number of parents
|
|
return nil, nil
|
|
}
|
|
offset := 33
|
|
|
|
// Calculate expected minimum size with parents
|
|
expectedMinSize := 33 + (parentCount * 32) + 16
|
|
if len(b) < expectedMinSize {
|
|
return nil, nil // Not enough data for declared parent count
|
|
}
|
|
|
|
// Parse parents
|
|
parents := make([]ids.ID, parentCount)
|
|
for i := 0; i < parentCount; i++ {
|
|
if offset+32 > len(b) {
|
|
return nil, nil
|
|
}
|
|
copy(parents[i][:], b[offset:offset+32])
|
|
offset += 32
|
|
}
|
|
|
|
// Parse height and timestamp
|
|
if offset+16 > len(b) {
|
|
return nil, nil
|
|
}
|
|
height := binary.BigEndian.Uint64(b[offset : offset+8])
|
|
timestamp := binary.BigEndian.Uint64(b[offset+8 : offset+16])
|
|
offset += 16
|
|
|
|
// Remaining bytes are data
|
|
var data []byte
|
|
if offset < len(b) {
|
|
data = b[offset:]
|
|
}
|
|
|
|
return NewVertex(id, parents, height, int64(timestamp), data), nil
|
|
}
|
|
|
|
// Start starts the engine
|
|
func (e *dagEngine) Start(ctx context.Context, requestID uint32) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
e.ctx, e.cancel = context.WithCancel(ctx)
|
|
e.bootstrapped = true
|
|
|
|
return nil
|
|
}
|
|
|
|
// Shutdown shuts down the engine
|
|
func (e *dagEngine) Shutdown(ctx context.Context) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if e.cancel != nil {
|
|
e.cancel()
|
|
}
|
|
e.bootstrapped = false
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the engine (alias for Shutdown for interface compatibility)
|
|
func (e *dagEngine) Stop(ctx context.Context) error {
|
|
return e.Shutdown(ctx)
|
|
}
|
|
|
|
// HealthCheck performs a health check
|
|
func (e *dagEngine) HealthCheck(ctx context.Context) (interface{}, error) {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
stats := e.consensus.Stats()
|
|
stats["bootstrapped"] = e.bootstrapped
|
|
stats["k"] = e.params.K
|
|
stats["alpha"] = e.params.AlphaPreference
|
|
stats["beta"] = e.params.Beta
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// IsBootstrapped returns whether the engine is bootstrapped
|
|
func (e *dagEngine) IsBootstrapped() bool {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
return e.bootstrapped
|
|
}
|
|
|
|
// AddVertex adds a vertex to consensus
|
|
func (e *dagEngine) AddVertex(ctx context.Context, vertex *Vertex) error {
|
|
return e.consensus.AddVertex(ctx, vertex)
|
|
}
|
|
|
|
// ProcessVote processes a vote for a vertex
|
|
func (e *dagEngine) ProcessVote(ctx context.Context, vertexID ids.ID, accept bool) error {
|
|
return e.consensus.ProcessVote(ctx, vertexID, accept)
|
|
}
|
|
|
|
// Poll conducts a consensus poll
|
|
func (e *dagEngine) Poll(ctx context.Context, responses map[ids.ID]int) error {
|
|
return e.consensus.Poll(ctx, responses)
|
|
}
|
|
|
|
// IsAccepted checks if a vertex is accepted
|
|
func (e *dagEngine) IsAccepted(vertexID ids.ID) bool {
|
|
return e.consensus.IsAccepted(vertexID)
|
|
}
|
|
|
|
// Preference returns the current preferred vertex
|
|
func (e *dagEngine) Preference() ids.ID {
|
|
return e.consensus.Preference()
|
|
}
|
|
|
|
// QueueData queues data for the next vertex
|
|
func (e *dagEngine) QueueData(data []byte) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
e.pendingData = append(e.pendingData, data)
|
|
}
|