// 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) }