Add multi-consensus orchestration to netrunner

- Implement engine abstraction for managing multiple consensus implementations
- Add LuxEngine and AvalancheEngine implementations
- Create Host orchestrator for multi-engine management
- Add stack manifest system for defining multi-network configurations
- Implement port manager for dynamic allocation
- Add metrics collector for engine monitoring
- Create CLI commands for engine management (start, stop, list)
- Add predefined stacks for common network setups (L1+L2, bridge configs)
- Temporarily disable EVM client imports due to dependency conflicts
- All orchestrator components build successfully
This commit is contained in:
Zach Kelling
2025-08-08 18:02:36 +00:00
parent 5068bb5728
commit 466f40ec2b
17 changed files with 1950 additions and 320 deletions
+6 -6
View File
@@ -9,7 +9,7 @@ import (
"github.com/luxfi/node/indexer"
"github.com/luxfi/node/vms/xvm"
"github.com/luxfi/node/vms/platformvm"
evmclient "github.com/luxfi/evm/plugin/evm/client"
// evmclient "github.com/luxfi/evm/plugin/evm/client"
)
// interface compliance
@@ -23,7 +23,7 @@ type APIClient struct {
platform *platformvm.Client
xChain *xvm.Client
xChainWallet *xvm.WalletClient
cChain evmclient.Client
cChain interface{} // evmclient.Client
cChainEth EthClient
info *info.Client
health *health.Client
@@ -38,7 +38,7 @@ type NewAPIClientF func(ipAddr string, port uint16) Client
// NewAPIClient initialize most of node apis
func NewAPIClient(ipAddr string, port uint16) Client {
uri := fmt.Sprintf("http://%s:%d", ipAddr, port)
cChainClient := evmclient.NewClient(uri, "C")
// cChainClient := evmclient.NewClient(uri, "C")
// Create client instances
platformClient := platformvm.NewClient(uri)
@@ -54,8 +54,8 @@ func NewAPIClient(ipAddr string, port uint16) Client {
platform: &platformClient,
xChain: &xChainClient,
xChainWallet: &xChainWalletClient,
cChain: cChainClient,
cChainEth: NewEthClient(ipAddr, uint(port)), // wrapper over ethclient.Client
cChain: nil, // cChainClient,
cChainEth: nil, // NewEthClient(ipAddr, uint(port)), // wrapper over ethclient.Client
info: &infoClient,
health: &healthClient,
admin: &adminClient,
@@ -76,7 +76,7 @@ func (c APIClient) XChainWalletAPI() *xvm.WalletClient {
return c.xChainWallet
}
func (c APIClient) CChainAPI() evmclient.Client {
func (c APIClient) CChainAPI() interface{} { // evmclient.Client {
return c.cChain
}
+5 -2
View File
@@ -7,16 +7,19 @@ import (
"github.com/luxfi/node/indexer"
"github.com/luxfi/node/vms/xvm"
"github.com/luxfi/node/vms/platformvm"
evmclient "github.com/luxfi/evm/plugin/evm/client"
// evmclient "github.com/luxfi/evm/plugin/evm/client"
)
// EthClient is a placeholder interface for ethereum client
type EthClient interface{}
// Issues API calls to a node
// TODO: byzantine api. check if appropriate. improve implementation.
type Client interface {
PChainAPI() *platformvm.Client
XChainAPI() *xvm.Client
XChainWalletAPI() *xvm.WalletClient
CChainAPI() evmclient.Client
CChainAPI() interface{} // evmclient.Client
CChainEthAPI() EthClient // ethclient websocket wrapper that adds mutexed calls, and lazy conn init (on first call)
InfoAPI() *info.Client
HealthAPI() *health.Client
-262
View File
@@ -1,262 +0,0 @@
package api
import (
"context"
"fmt"
"math/big"
"sync"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/evm/ethclient"
ethereum "github.com/luxfi/geth"
"github.com/luxfi/geth/common"
)
// Interface compliance
var _ EthClient = &ethClient{}
type EthClient interface {
Close()
SendTransaction(context.Context, *types.Transaction) error
TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error)
BalanceAt(context.Context, common.Address, *big.Int) (*big.Int, error)
BlockByNumber(context.Context, *big.Int) (*types.Block, error)
BlockByHash(context.Context, common.Hash) (*types.Block, error)
BlockNumber(context.Context) (uint64, error)
CallContract(context.Context, ethereum.CallMsg, *big.Int) ([]byte, error)
NonceAt(context.Context, common.Address, *big.Int) (uint64, error)
SuggestGasPrice(context.Context) (*big.Int, error)
AcceptedCodeAt(context.Context, common.Address) ([]byte, error)
AcceptedNonceAt(context.Context, common.Address) (uint64, error)
CodeAt(context.Context, common.Address, *big.Int) ([]byte, error)
EstimateGas(context.Context, ethereum.CallMsg) (uint64, error)
AcceptedCallContract(context.Context, ethereum.CallMsg) ([]byte, error)
HeaderByNumber(context.Context, *big.Int) (*types.Header, error)
SuggestGasTipCap(context.Context) (*big.Int, error)
FilterLogs(context.Context, ethereum.FilterQuery) ([]types.Log, error)
SubscribeFilterLogs(context.Context, ethereum.FilterQuery, chan<- types.Log) (ethereum.Subscription, error)
}
// ethClient websocket ethclient.Client with mutexed api calls and lazy conn (on first call)
// All calls are wrapped in a mutex, and try to create a connection if it doesn't exist yet
type ethClient struct {
ipAddr string
chainID string
port uint
client ethclient.Client
lock sync.Mutex
}
// NewEthClient mainly takes ip/port info for usage in future calls
// Connection can't be initialized in constructor because node is not ready when the constructor is called
// It follows convention of most node api constructors that can be called without having a ready node
func NewEthClient(ipAddr string, port uint) EthClient {
// default to using the C chain
return NewEthClientWithChainID(ipAddr, port, "C")
}
// NewEthClientWithChainID creates an EthClient initialized to connect to
// ipAddr/port and communicate with the given chainID.
func NewEthClientWithChainID(ipAddr string, port uint, chainID string) EthClient {
return &ethClient{
ipAddr: ipAddr,
port: port,
chainID: chainID,
}
}
// connect attempts to connect with websocket ethclient API
func (c *ethClient) connect() error {
if c.client == nil {
client, err := ethclient.Dial(fmt.Sprintf("ws://%s:%d/ext/bc/%s/ws", c.ipAddr, c.port, c.chainID))
if err != nil {
return err
}
c.client = client
}
return nil
}
// Close closes opened connection (if any)
func (c *ethClient) Close() {
if c.client == nil {
return
}
c.lock.Lock()
defer c.lock.Unlock()
c.client.Close()
}
func (c *ethClient) SendTransaction(ctx context.Context, tx *types.Transaction) error {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return err
}
return c.client.SendTransaction(ctx, tx)
}
func (c *ethClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.TransactionReceipt(ctx, txHash)
}
func (c *ethClient) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.BalanceAt(ctx, account, blockNumber)
}
func (c *ethClient) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.BlockByNumber(ctx, number)
}
func (c *ethClient) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.BlockByHash(ctx, hash)
}
func (c *ethClient) BlockNumber(ctx context.Context) (uint64, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return 0, err
}
return c.client.BlockNumber(ctx)
}
func (c *ethClient) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
// Use ethereum.CallMsg directly
return c.client.CallContract(ctx, msg, blockNumber)
}
func (c *ethClient) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return 0, err
}
return c.client.NonceAt(ctx, account, blockNumber)
}
func (c *ethClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.SuggestGasPrice(ctx)
}
func (c *ethClient) AcceptedCodeAt(ctx context.Context, account common.Address) ([]byte, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
// TODO: AcceptedCodeAt is not in standard ethclient
// For now, use CodeAt with latest block
return c.client.CodeAt(ctx, account, nil)
}
func (c *ethClient) AcceptedNonceAt(ctx context.Context, account common.Address) (uint64, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return 0, err
}
// TODO: AcceptedNonceAt is not in standard ethclient
// For now, use NonceAt with latest block
return c.client.NonceAt(ctx, account, nil)
}
func (c *ethClient) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.CodeAt(ctx, account, blockNumber)
}
func (c *ethClient) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return 0, err
}
// Use ethereum.CallMsg directly
return c.client.EstimateGas(ctx, msg)
}
func (c *ethClient) AcceptedCallContract(ctx context.Context, call ethereum.CallMsg) ([]byte, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
// TODO: AcceptedCallContract is not in standard ethclient
// For now, use CallContract with latest block
// Use ethereum.CallMsg directly
return c.client.CallContract(ctx, call, nil)
}
func (c *ethClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.HeaderByNumber(ctx, number)
}
func (c *ethClient) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.SuggestGasTipCap(ctx)
}
func (c *ethClient) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
// Use ethereum.FilterQuery directly
return c.client.FilterLogs(ctx, query)
}
func (c *ethClient) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
// Use ethereum.FilterQuery directly
return c.client.SubscribeFilterLogs(ctx, query, ch)
}
+203
View File
@@ -0,0 +1,203 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package cmd
import (
"context"
"fmt"
"os"
"text/tabwriter"
"time"
"github.com/luxfi/netrunner/engines"
"github.com/luxfi/netrunner/orchestrator"
"github.com/spf13/cobra"
)
var (
engineType string
networkID uint32
httpPort uint16
stakingPort uint16
dataDir string
logLevel string
stackFile string
)
// engineCmd represents the engine command group
var engineCmd = &cobra.Command{
Use: "engine",
Short: "Manage consensus engines",
Long: `Start, stop, and manage multiple consensus engines (luxd, avalanchego, etc.)`,
}
// startCmd starts an engine
var startEngineCmd = &cobra.Command{
Use: "start [name]",
Short: "Start a consensus engine",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
host := orchestrator.NewHost()
config := &engines.NodeConfig{
NetworkID: networkID,
HTTPPort: httpPort,
StakingPort: stakingPort,
DataDir: dataDir,
LogLevel: logLevel,
}
if err := host.StartEngine(context.Background(), name, engines.EngineType(engineType), config); err != nil {
return fmt.Errorf("failed to start engine: %w", err)
}
fmt.Printf("Started %s engine '%s'\n", engineType, name)
fmt.Printf("RPC: http://localhost:%d\n", httpPort)
return nil
},
}
// stopCmd stops an engine
var stopEngineCmd = &cobra.Command{
Use: "stop [name]",
Short: "Stop a consensus engine",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
host := orchestrator.NewHost()
if err := host.StopEngine(context.Background(), name); err != nil {
return fmt.Errorf("failed to stop engine: %w", err)
}
fmt.Printf("Stopped engine '%s'\n", name)
return nil
},
}
// listCmd lists running engines
var listEnginesCmd = &cobra.Command{
Use: "list",
Short: "List running engines",
RunE: func(cmd *cobra.Command, args []string) error {
host := orchestrator.NewHost()
engines := host.ListEngines()
if len(engines) == 0 {
fmt.Println("No engines running")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "NAME\tTYPE\tNETWORK\tSTATUS\tRPC\tUPTIME")
for _, e := range engines {
status := "unhealthy"
if e.Healthy {
status = "healthy"
}
fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\t%s\n",
e.Name,
e.Type,
e.NetworkID,
status,
e.RPC,
formatDuration(e.Uptime),
)
}
return w.Flush()
},
}
// stackCmd manages stacks
var stackCmd = &cobra.Command{
Use: "stack",
Short: "Manage engine stacks",
}
// startStackCmd starts a stack
var startStackCmd = &cobra.Command{
Use: "start",
Short: "Start an engine stack from manifest",
RunE: func(cmd *cobra.Command, args []string) error {
if stackFile == "" {
return fmt.Errorf("--file is required")
}
manifest, err := orchestrator.LoadManifest(stackFile)
if err != nil {
return fmt.Errorf("failed to load manifest: %w", err)
}
host := orchestrator.NewHost()
if err := host.StartStack(context.Background(), manifest); err != nil {
return fmt.Errorf("failed to start stack: %w", err)
}
fmt.Printf("Started stack '%s'\n", manifest.Name)
fmt.Println("\nRunning engines:")
for _, e := range host.ListEngines() {
fmt.Printf(" %s (%s): %s\n", e.Name, e.Type, e.RPC)
}
return nil
},
}
// stopStackCmd stops a stack
var stopStackCmd = &cobra.Command{
Use: "stop [name]",
Short: "Stop an engine stack",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
host := orchestrator.NewHost()
if err := host.StopStack(context.Background(), name); err != nil {
return fmt.Errorf("failed to stop stack: %w", err)
}
fmt.Printf("Stopped stack '%s'\n", name)
return nil
},
}
func init() {
rootCmd.AddCommand(engineCmd)
// Engine commands
engineCmd.AddCommand(startEngineCmd)
engineCmd.AddCommand(stopEngineCmd)
engineCmd.AddCommand(listEnginesCmd)
// Engine start flags
startEngineCmd.Flags().StringVar(&engineType, "type", "lux", "Engine type (lux, avalanche, geth, op)")
startEngineCmd.Flags().Uint32Var(&networkID, "network-id", 96369, "Network ID")
startEngineCmd.Flags().Uint16Var(&httpPort, "http-port", 9630, "HTTP RPC port")
startEngineCmd.Flags().Uint16Var(&stakingPort, "staking-port", 9631, "P2P staking port")
startEngineCmd.Flags().StringVar(&dataDir, "data-dir", "", "Data directory")
startEngineCmd.Flags().StringVar(&logLevel, "log-level", "info", "Log level")
// Stack commands
engineCmd.AddCommand(stackCmd)
stackCmd.AddCommand(startStackCmd)
stackCmd.AddCommand(stopStackCmd)
// Stack flags
startStackCmd.Flags().StringVar(&stackFile, "file", "", "Stack manifest file")
startStackCmd.MarkFlagRequired("file")
}
func formatDuration(d time.Duration) string {
if d < time.Minute {
return fmt.Sprintf("%ds", int(d.Seconds()))
}
if d < time.Hour {
return fmt.Sprintf("%dm", int(d.Minutes()))
}
return fmt.Sprintf("%dh%dm", int(d.Hours()), int(d.Minutes())%60)
}
+261
View File
@@ -0,0 +1,261 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package avalanche
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/ava-labs/avalanchego/api/health"
"github.com/ava-labs/avalanchego/api/info"
"github.com/luxfi/ids"
"github.com/luxfi/netrunner/engines"
)
func init() {
engines.Register(engines.EngineAvalanche, NewAvalancheEngine)
}
// AvalancheEngine wraps avalanchego node management
type AvalancheEngine struct {
name string
binary string
dataDir string
config *engines.NodeConfig
process *exec.Cmd
infoClient *info.Client
healthClient *health.Client
startTime time.Time
// Cached info
networkID uint32
chainID ids.ID
}
// NewAvalancheEngine creates a new Avalanche engine
func NewAvalancheEngine(name string, binary string) (engines.Engine, error) {
if binary == "" {
binary = "avalanchego"
}
return &AvalancheEngine{
name: name,
binary: binary,
}, nil
}
func (e *AvalancheEngine) Name() string { return e.name }
func (e *AvalancheEngine) Type() engines.EngineType { return engines.EngineAvalanche }
func (e *AvalancheEngine) NetworkID() uint32 { return e.networkID }
func (e *AvalancheEngine) ChainID() ids.ID { return e.chainID }
func (e *AvalancheEngine) ParentChain() *engines.ChainInfo { return nil } // L1
func (e *AvalancheEngine) Start(ctx context.Context, config *engines.NodeConfig) error {
e.config = config
e.networkID = config.NetworkID
// Setup data directory
if config.DataDir == "" {
config.DataDir = filepath.Join(os.TempDir(), "avalanche", e.name)
}
e.dataDir = config.DataDir
if err := os.MkdirAll(e.dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data dir: %w", err)
}
// Build command arguments
args := []string{
fmt.Sprintf("--network-id=%s", getAvalancheNetwork(config.NetworkID)),
fmt.Sprintf("--http-port=%d", config.HTTPPort),
fmt.Sprintf("--staking-port=%d", config.StakingPort),
fmt.Sprintf("--data-dir=%s", e.dataDir),
fmt.Sprintf("--log-level=%s", config.LogLevel),
"--http-host=0.0.0.0",
"--http-allowed-hosts=*",
"--http-allowed-origins=*",
}
// Add bootstrap nodes if custom network
if len(config.BootstrapIPs) > 0 {
for _, ip := range config.BootstrapIPs {
args = append(args, fmt.Sprintf("--bootstrap-ips=%s", ip))
}
}
// Add extra configs
for k, v := range config.Extra {
args = append(args, fmt.Sprintf("--%s=%v", k, v))
}
// Start the process
e.process = exec.CommandContext(ctx, e.binary, args...)
e.process.Dir = e.dataDir
// Setup logs
logFile, err := os.Create(filepath.Join(e.dataDir, "node.log"))
if err != nil {
return fmt.Errorf("failed to create log file: %w", err)
}
e.process.Stdout = logFile
e.process.Stderr = logFile
if err := e.process.Start(); err != nil {
return fmt.Errorf("failed to start avalanchego: %w", err)
}
e.startTime = time.Now()
// Setup RPC clients
e.infoClient = info.NewClient(e.RPCEndpoint())
e.healthClient = health.NewClient(e.RPCEndpoint())
// Wait for node to be responsive
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
timeout := time.After(60 * time.Second) // Avalanche can take longer
for {
select {
case <-ticker.C:
if _, _, err := e.infoClient.GetNodeID(ctx); err == nil {
// Cache C-Chain ID
if cid, err := e.infoClient.GetBlockchainID(ctx, "C"); err == nil {
// Convert avalanche ID to lux ID
e.chainID, _ = ids.FromString(cid.String())
}
return nil
}
case <-timeout:
return fmt.Errorf("avalanchego failed to start within 60 seconds")
case <-ctx.Done():
return ctx.Err()
}
}
}
func (e *AvalancheEngine) Stop(ctx context.Context) error {
if e.process == nil || e.process.Process == nil {
return nil
}
// Try graceful shutdown first
if err := e.process.Process.Signal(os.Interrupt); err != nil {
return e.process.Process.Kill()
}
done := make(chan error, 1)
go func() {
done <- e.process.Wait()
}()
select {
case <-done:
return nil
case <-time.After(10 * time.Second):
return e.process.Process.Kill()
case <-ctx.Done():
return e.process.Process.Kill()
}
}
func (e *AvalancheEngine) Restart(ctx context.Context) error {
if err := e.Stop(ctx); err != nil {
return err
}
time.Sleep(2 * time.Second) // Give ports time to release
return e.Start(ctx, e.config)
}
func (e *AvalancheEngine) Health(ctx context.Context) (*engines.HealthStatus, error) {
if e.healthClient == nil || e.infoClient == nil {
return &engines.HealthStatus{Healthy: false}, nil
}
// Get node health
healthResp, err := e.healthClient.Health(ctx, nil)
if err != nil {
return &engines.HealthStatus{Healthy: false}, nil
}
// Get peers
peers, err := e.infoClient.Peers(ctx, nil)
if err != nil {
peers = []info.Peer{}
}
// Get version
versions, err := e.infoClient.GetNodeVersion(ctx)
if err != nil {
versions = &info.GetNodeVersionReply{}
}
return &engines.HealthStatus{
Healthy: healthResp.Healthy,
PeerCount: len(peers),
Version: versions.Version,
// BlockHeight from C-Chain would require eth client
}, nil
}
func (e *AvalancheEngine) IsRunning() bool {
return e.process != nil && e.process.Process != nil
}
func (e *AvalancheEngine) Uptime() time.Duration {
if !e.IsRunning() {
return 0
}
return time.Since(e.startTime)
}
func (e *AvalancheEngine) RPCEndpoint() string {
if e.config == nil {
return ""
}
return fmt.Sprintf("http://localhost:%d", e.config.HTTPPort)
}
func (e *AvalancheEngine) WSEndpoint() string {
if e.config == nil {
return ""
}
// Avalanche uses same port for HTTP and WS
return fmt.Sprintf("ws://localhost:%d/ext/bc/C/ws", e.config.HTTPPort)
}
func (e *AvalancheEngine) P2PEndpoint() string {
if e.config == nil {
return ""
}
return fmt.Sprintf("localhost:%d", e.config.StakingPort)
}
func (e *AvalancheEngine) Metrics() map[string]interface{} {
return map[string]interface{}{
"uptime_seconds": e.Uptime().Seconds(),
"running": e.IsRunning(),
"network_id": e.networkID,
"chain_id": e.chainID.String(),
}
}
// getAvalancheNetwork converts network ID to avalanche network name
func getAvalancheNetwork(networkID uint32) string {
switch networkID {
case 1:
return "mainnet"
case 5:
return "fuji"
case 43114:
return "mainnet" // C-Chain ID
case 43113:
return "fuji" // Fuji C-Chain
default:
return fmt.Sprintf("%d", networkID)
}
}
+246
View File
@@ -0,0 +1,246 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package lux
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/netrunner/engines"
"github.com/luxfi/node/api/health"
"github.com/luxfi/node/api/info"
)
func init() {
engines.Register(engines.EngineLux, NewLuxEngine)
}
// LuxEngine wraps luxd node management
type LuxEngine struct {
name string
binary string
dataDir string
config *engines.NodeConfig
process *exec.Cmd
infoClient info.Client
healthClient health.Client
startTime time.Time
// Cached info
networkID uint32
chainID ids.ID
}
// NewLuxEngine creates a new Lux engine
func NewLuxEngine(name string, binary string) (engines.Engine, error) {
if binary == "" {
binary = "luxd"
}
return &LuxEngine{
name: name,
binary: binary,
}, nil
}
func (e *LuxEngine) Name() string { return e.name }
func (e *LuxEngine) Type() engines.EngineType { return engines.EngineLux }
func (e *LuxEngine) NetworkID() uint32 { return e.networkID }
func (e *LuxEngine) ChainID() ids.ID { return e.chainID }
func (e *LuxEngine) ParentChain() *engines.ChainInfo { return nil } // L1
func (e *LuxEngine) Start(ctx context.Context, config *engines.NodeConfig) error {
e.config = config
e.networkID = config.NetworkID
// Setup data directory
if config.DataDir == "" {
config.DataDir = filepath.Join(os.TempDir(), "lux", e.name)
}
e.dataDir = config.DataDir
if err := os.MkdirAll(e.dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data dir: %w", err)
}
// Build command arguments
args := []string{
fmt.Sprintf("--network-id=%d", config.NetworkID),
fmt.Sprintf("--http-port=%d", config.HTTPPort),
fmt.Sprintf("--staking-port=%d", config.StakingPort),
fmt.Sprintf("--data-dir=%s", e.dataDir),
fmt.Sprintf("--log-level=%s", config.LogLevel),
"--http-host=0.0.0.0",
"--http-allowed-hosts=*",
}
// Add bootstrap nodes
if len(config.BootstrapIPs) > 0 {
for _, ip := range config.BootstrapIPs {
args = append(args, fmt.Sprintf("--bootstrap-ips=%s", ip))
}
}
// Add extra configs
for k, v := range config.Extra {
args = append(args, fmt.Sprintf("--%s=%v", k, v))
}
// Start the process
e.process = exec.CommandContext(ctx, e.binary, args...)
e.process.Dir = e.dataDir
// Setup logs
logFile, err := os.Create(filepath.Join(e.dataDir, "node.log"))
if err != nil {
return fmt.Errorf("failed to create log file: %w", err)
}
e.process.Stdout = logFile
e.process.Stderr = logFile
if err := e.process.Start(); err != nil {
return fmt.Errorf("failed to start luxd: %w", err)
}
e.startTime = time.Now()
// Setup RPC clients
e.infoClient = info.NewClient(e.RPCEndpoint())
e.healthClient = health.NewClient(e.RPCEndpoint())
// Wait for node to be responsive
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
timeout := time.After(30 * time.Second)
for {
select {
case <-ticker.C:
if _, _, err := e.infoClient.GetNodeID(ctx); err == nil {
// Cache chain ID
if cid, err := e.infoClient.GetBlockchainID(ctx, "C"); err == nil {
e.chainID = cid
}
return nil
}
case <-timeout:
return fmt.Errorf("luxd failed to start within 30 seconds")
case <-ctx.Done():
return ctx.Err()
}
}
}
func (e *LuxEngine) Stop(ctx context.Context) error {
if e.process == nil || e.process.Process == nil {
return nil
}
// Try graceful shutdown first
if err := e.process.Process.Signal(os.Interrupt); err != nil {
return e.process.Process.Kill()
}
done := make(chan error, 1)
go func() {
done <- e.process.Wait()
}()
select {
case <-done:
return nil
case <-time.After(10 * time.Second):
return e.process.Process.Kill()
case <-ctx.Done():
return e.process.Process.Kill()
}
}
func (e *LuxEngine) Restart(ctx context.Context) error {
if err := e.Stop(ctx); err != nil {
return err
}
time.Sleep(2 * time.Second) // Give ports time to release
return e.Start(ctx, e.config)
}
func (e *LuxEngine) Health(ctx context.Context) (*engines.HealthStatus, error) {
if e.healthClient == nil || e.infoClient == nil {
return &engines.HealthStatus{Healthy: false}, nil
}
// Get node health
healthResp, err := e.healthClient.Health(ctx, nil)
if err != nil {
return &engines.HealthStatus{Healthy: false}, nil
}
// Get peers
peers, err := e.infoClient.Peers(ctx, nil)
if err != nil {
peers = []info.Peer{}
}
// Get version
versions, err := e.infoClient.GetNodeVersion(ctx)
if err != nil {
versions = &info.GetNodeVersionReply{}
}
return &engines.HealthStatus{
Healthy: healthResp.Healthy,
PeerCount: len(peers),
Version: versions.Version,
// BlockHeight from C-Chain would require eth client
}, nil
}
func (e *LuxEngine) IsRunning() bool {
return e.process != nil && e.process.Process != nil
}
func (e *LuxEngine) Uptime() time.Duration {
if !e.IsRunning() {
return 0
}
return time.Since(e.startTime)
}
func (e *LuxEngine) RPCEndpoint() string {
if e.config == nil {
return ""
}
return fmt.Sprintf("http://localhost:%d", e.config.HTTPPort)
}
func (e *LuxEngine) WSEndpoint() string {
if e.config == nil {
return ""
}
if e.config.WSPort == 0 {
// Lux uses same port for HTTP and WS
return fmt.Sprintf("ws://localhost:%d/ext/bc/C/ws", e.config.HTTPPort)
}
return fmt.Sprintf("ws://localhost:%d", e.config.WSPort)
}
func (e *LuxEngine) P2PEndpoint() string {
if e.config == nil {
return ""
}
return fmt.Sprintf("localhost:%d", e.config.StakingPort)
}
func (e *LuxEngine) Metrics() map[string]interface{} {
return map[string]interface{}{
"uptime_seconds": e.Uptime().Seconds(),
"running": e.IsRunning(),
"network_id": e.networkID,
"chain_id": e.chainID.String(),
}
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package engines
import (
"context"
"fmt"
"time"
"github.com/luxfi/ids"
)
// EngineType identifies the consensus engine implementation
type EngineType string
const (
EngineLux EngineType = "lux"
EngineAvalanche EngineType = "avalanche"
EngineGeth EngineType = "geth"
EngineOP EngineType = "op"
)
// ChainInfo describes a blockchain's network properties
type ChainInfo struct {
ChainID ids.ID
NetworkID uint32
ParentID *ids.ID // nil for L1, set for L2/L3
}
// HealthStatus represents engine health
type HealthStatus struct {
Healthy bool
BlockHeight uint64
PeerCount int
Syncing bool
Version string
}
// NodeConfig contains engine startup configuration
type NodeConfig struct {
NetworkID uint32
HTTPPort uint16
WSPort uint16
StakingPort uint16
DataDir string
LogLevel string
BootstrapIPs []string
// Chain-specific configs
Extra map[string]interface{}
}
// Engine is the interface for all consensus implementations
type Engine interface {
// Lifecycle
Name() string
Type() EngineType
Start(ctx context.Context, config *NodeConfig) error
Stop(ctx context.Context) error
Restart(ctx context.Context) error
// Status
Health(ctx context.Context) (*HealthStatus, error)
IsRunning() bool
Uptime() time.Duration
// Network info
NetworkID() uint32
ChainID() ids.ID
RPCEndpoint() string
WSEndpoint() string
P2PEndpoint() string
// Chain relationships
ParentChain() *ChainInfo // nil for L1s
// Metrics
Metrics() map[string]interface{}
}
// EngineFactory creates engines from configs
type EngineFactory func(name string, binary string) (Engine, error)
var factories = map[EngineType]EngineFactory{}
// Register registers an engine factory
func Register(typ EngineType, factory EngineFactory) {
factories[typ] = factory
}
// New creates an engine of the given type
func New(typ EngineType, name string, binary string) (Engine, error) {
factory, ok := factories[typ]
if !ok {
return nil, fmt.Errorf("unknown engine type: %s", typ)
}
return factory(name, binary)
}
+18 -15
View File
@@ -5,12 +5,15 @@ go 1.24.5
exclude google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1
require (
github.com/ava-labs/avalanchego v1.13.4
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
github.com/luxfi/crypto v1.2.2
github.com/luxfi/evm v0.7.8-lux
github.com/luxfi/geth v1.16.1-lux
github.com/luxfi/evm v0.8.2
github.com/luxfi/geth v1.16.26
github.com/luxfi/ids v1.0.2
github.com/luxfi/node v1.13.4-lux
github.com/luxfi/log v1.0.2
github.com/luxfi/metrics v1.1.1
github.com/luxfi/node v1.16.15
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
github.com/otiai10/copy v1.14.1
@@ -20,12 +23,13 @@ require (
github.com/stretchr/testify v1.10.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792
golang.org/x/mod v0.26.0
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a
golang.org/x/mod v0.27.0
golang.org/x/sync v0.16.0
google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074
google.golang.org/grpc v1.74.2
google.golang.org/protobuf v1.36.6
gopkg.in/yaml.v3 v3.0.1
)
require (
@@ -34,8 +38,10 @@ require (
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/StephenButtolph/canoto v0.17.2 // indirect
github.com/VictoriaMetrics/fastcache v1.12.5 // indirect
github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -91,8 +97,6 @@ require (
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/luxfi/database v1.1.9 // indirect
github.com/luxfi/log v0.1.1 // indirect
github.com/luxfi/metrics v1.1.1 // indirect
github.com/luxfi/trace v0.1.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
@@ -116,7 +120,7 @@ require (
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
@@ -136,16 +140,15 @@ require (
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.2 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.35.0 // indirect
golang.org/x/tools v0.36.0 // indirect
gonum.org/v1/gonum v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+42 -35
View File
@@ -11,20 +11,28 @@ github.com/VictoriaMetrics/fastcache v1.12.5/go.mod h1:K+JGPBn0sueFlLjZ8rcVM0cKk
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/ava-labs/avalanchego v1.13.4 h1:H7bI1qx9qaOddJ3E2J9KkLvTe7d613K821eST6VGXEU=
github.com/ava-labs/avalanchego v1.13.4/go.mod h1:pMPIH9KeyXFsdxuF6sy06sztq3p2rI4XeePXvGeg9Ew=
github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6 h1:tyM659nDOknwTeU4A0fUVsGNIU7k0v738wYN92nqs/Y=
github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6/go.mod h1:zP/DOcABRWargBmUWv1jXplyWNcfmBy9cxr0lw3LW3g=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.0 h1:V2/ZgjfDFIygAX3ZapeigkVBoVUtOJKSwrhZdlpSvaA=
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
@@ -37,8 +45,6 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -69,6 +75,8 @@ github.com/crate-crypto/go-eth-kzg v1.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg
github.com/crate-crypto/go-eth-kzg v1.3.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI=
github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -94,10 +102,10 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
github.com/ethereum/c-kzg-4844/v2 v2.1.1 h1:KhzBVjmURsfr1+S3k/VE35T02+AW2qU9t9gr4R6YpSo=
github.com/ethereum/c-kzg-4844/v2 v2.1.1/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E=
github.com/ethereum/go-ethereum v1.16.1 h1:7684NfKCb1+IChudzdKyZJ12l1Tq4ybPZOITiCDXqCk=
github.com/ethereum/go-ethereum v1.16.1/go.mod h1:ngYIvmMAYdo4sGW9cGzLvSsPGhDOOzL0jK5S5iXpj0g=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
@@ -109,8 +117,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays=
github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE=
github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46/go.mod h1:QNpY22eby74jVhqH4WhDLDwxc/vqsern6pW+u2kbkpc=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
@@ -132,8 +140,6 @@ github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gojuukaze/go-bip39 v1.1.0 h1:nZoA8BOLhiOhTdH3iMYnWKgfD5FXTKZHZZS9QubH0VE=
github.com/gojuukaze/go-bip39 v1.1.0/go.mod h1:tHp4ihCWJ8KpPTymBOFVAMwfzxO4B7+OhNtVW3u1nY8=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -179,8 +185,6 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+u
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hashicorp/go-bexpr v0.1.14 h1:uKDeyuOhWhT1r5CiMTjdVY4Aoxdxs6EtwgTGnlosyp4=
github.com/hashicorp/go-bexpr v0.1.14/go.mod h1:gN7hRKB3s7yT+YvTdnhZVLTENejvhlkZ8UE4YVBS+Q8=
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs=
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330=
github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
@@ -218,16 +222,20 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/luxfi/crypto v1.2.2 h1:jkaLIUNML8f2I/SNEbZooJRRW43bOnU6E8yEG9x9D3w=
github.com/luxfi/crypto v1.2.2/go.mod h1:qIfHGq5kzTUqzyTqj6II5/6aEp0OWntfeOdhYy/d7aQ=
github.com/luxfi/database v1.1.7 h1:sskVC0lL5HMELmklQGHnGBlYYD6n+kWGls4XDxye/tg=
github.com/luxfi/database v1.1.7/go.mod h1:0uBGc+U7415yAKBjImaz+OkGzeFaqxH3pcgB3pmofjM=
github.com/luxfi/go-bip39 v1.1.0 h1:5ZNX8E5+8tOYRy+G2Yp2msiRwHWEM6hzq9HpM8ZgsIg=
github.com/luxfi/go-bip39 v1.1.0/go.mod h1:OxPL7QXUfnqqk7nOTPWgNfShxqg69OYJ4+ATKCmZWP8=
github.com/luxfi/database v1.1.9 h1:bvC62AVS9z/6klWoqSevni+AUQsMA7afLuTth/I7S/g=
github.com/luxfi/database v1.1.9/go.mod h1:lDOxPN07eYUv2WeXVN2A4bTIe6Kq/ay4NaEPkOgPoXI=
github.com/luxfi/evm v0.8.2 h1:F9BZ9EDZlB282fjdHrvP7HtzdEmOzCzRciZRYLPB2vk=
github.com/luxfi/evm v0.8.2/go.mod h1:pWNPADWd0x/ELqdjJ7M/SNMWkSP/Jr6u8B997tcd8hk=
github.com/luxfi/geth v1.16.26 h1:nYkKZAQx7JwGHaOV/6AXR3YYKRplhnWAQPsA+7cJ9RI=
github.com/luxfi/geth v1.16.26/go.mod h1:oW6L657BBT0okG+yQaBzuzg5XZ38RLdKupHyKf17LAc=
github.com/luxfi/ids v1.0.2 h1:OnbCWBgdmuBPZat1r1vRikk1FFrdHMfSknpfKmX0KBg=
github.com/luxfi/ids v1.0.2/go.mod h1:cMbto3Q8N3IaBxdz4Lzaq9wYl33coGXUdlr2+YwXeUw=
github.com/luxfi/log v0.1.1 h1:KRTOIGqyPTrN3nWcBMVI50B6EXTayYex2+HyGjJ06Ew=
github.com/luxfi/log v0.1.1/go.mod h1:trb99HbI+YW6nu+So4jWKey20oHeRhlee70xbuR6Gak=
github.com/luxfi/log v1.0.2 h1:yXuRmF+uJ34OmujnIJ5/QOqIqZSsOvGBWphwS08bAVk=
github.com/luxfi/log v1.0.2/go.mod h1:Q2eeOT4alCF+/B6+k/eWtVZQ2HncDlpd9vUvkTF0Suc=
github.com/luxfi/metrics v1.1.1 h1:aKVEtytAl3TqSvInRpDc6ZdSJsCbTmCCl0UAl2YNXWU=
github.com/luxfi/metrics v1.1.1/go.mod h1:ynSRcRjG+t1snUvFUbQFEu0UGibyw8gsitltQ9H1YDA=
github.com/luxfi/node v1.16.15 h1:ZRsteUu6ObB7Or3E6ZliNY2w/cL4tByGaSm7J37sVU8=
github.com/luxfi/node v1.16.15/go.mod h1:05FhncqqiDSp5XfSPjoPvTBBUVokCdKpHL72aRCxs4Y=
github.com/luxfi/trace v0.1.1 h1:T/ReB1MvwAJpgQaPWGsBLUx96rr5OMlJocWRo5JSvkU=
github.com/luxfi/trace v0.1.1/go.mod h1:6jhCW0hPCFv7qzFsL0w/amHRv7PfNIyW77N4WDS3U5Q=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
@@ -334,12 +342,11 @@ github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA=
github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
@@ -405,14 +412,14 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90=
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -424,8 +431,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -454,19 +461,19 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -474,8 +481,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+282
View File
@@ -0,0 +1,282 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package orchestrator
import (
"context"
"fmt"
"sync"
"time"
"github.com/luxfi/netrunner/engines"
"github.com/luxfi/netrunner/engines/lux"
"github.com/luxfi/netrunner/engines/avalanche"
)
// Host manages multiple consensus engines
type Host struct {
mu sync.RWMutex
engines map[string]engines.Engine
ports *PortManager
indexer *Indexer
metrics *MetricsCollector
networks map[string]*NetworkInfo
}
// NetworkInfo describes a network's properties
type NetworkInfo struct {
Name string
Engine string
NetworkID uint32
ChainID string
RPC string
WS string
Parent string // parent network name for L2/L3
}
// NewHost creates a new multi-engine host
func NewHost() *Host {
return &Host{
engines: make(map[string]engines.Engine),
ports: NewPortManager(),
networks: make(map[string]*NetworkInfo),
metrics: NewMetricsCollector(),
}
}
// StartEngine starts a single engine
func (h *Host) StartEngine(ctx context.Context, name string, typ engines.EngineType, config *engines.NodeConfig) error {
h.mu.Lock()
defer h.mu.Unlock()
// Check if already running
if _, exists := h.engines[name]; exists {
return fmt.Errorf("engine %s already running", name)
}
// Allocate ports if not specified
if config.HTTPPort == 0 {
config.HTTPPort = h.ports.AllocateHTTP()
}
if config.StakingPort == 0 {
config.StakingPort = h.ports.AllocateP2P()
}
// Create engine
var engine engines.Engine
var err error
switch typ {
case engines.EngineLux:
engine, err = lux.NewLuxEngine(name, "")
case engines.EngineAvalanche:
engine, err = avalanche.NewAvalancheEngine(name, "")
default:
return fmt.Errorf("unsupported engine type: %s", typ)
}
if err != nil {
return fmt.Errorf("failed to create engine: %w", err)
}
// Start engine
if err := engine.Start(ctx, config); err != nil {
h.ports.Release(config.HTTPPort)
h.ports.Release(config.StakingPort)
return fmt.Errorf("failed to start engine: %w", err)
}
h.engines[name] = engine
// Register network info
h.networks[name] = &NetworkInfo{
Name: name,
Engine: string(typ),
NetworkID: config.NetworkID,
ChainID: engine.ChainID().String(),
RPC: engine.RPCEndpoint(),
WS: engine.WSEndpoint(),
}
// Start metrics collection
go h.collectMetrics(name, engine)
return nil
}
// StopEngine stops a single engine
func (h *Host) StopEngine(ctx context.Context, name string) error {
h.mu.Lock()
defer h.mu.Unlock()
engine, exists := h.engines[name]
if !exists {
return fmt.Errorf("engine %s not found", name)
}
if err := engine.Stop(ctx); err != nil {
return fmt.Errorf("failed to stop engine: %w", err)
}
delete(h.engines, name)
delete(h.networks, name)
return nil
}
// ListEngines returns all running engines
func (h *Host) ListEngines() []EngineInfo {
h.mu.RLock()
defer h.mu.RUnlock()
var infos []EngineInfo
for name, engine := range h.engines {
health, _ := engine.Health(context.Background())
infos = append(infos, EngineInfo{
Name: name,
Type: string(engine.Type()),
NetworkID: engine.NetworkID(),
ChainID: engine.ChainID().String(),
RPC: engine.RPCEndpoint(),
WS: engine.WSEndpoint(),
Healthy: health != nil && health.Healthy,
Uptime: engine.Uptime(),
})
}
return infos
}
// EngineInfo contains engine information
type EngineInfo struct {
Name string
Type string
NetworkID uint32
ChainID string
RPC string
WS string
Healthy bool
Uptime time.Duration
}
// StartStack starts a predefined stack of engines
func (h *Host) StartStack(ctx context.Context, manifest *StackManifest) error {
// Validate manifest
if err := manifest.Validate(); err != nil {
return fmt.Errorf("invalid manifest: %w", err)
}
// Start engines in order (L1 first, then L2, then L3)
for _, engine := range manifest.Engines {
config := &engines.NodeConfig{
NetworkID: engine.NetworkID,
HTTPPort: engine.HTTPPort,
StakingPort: engine.StakingPort,
DataDir: engine.DataDir,
LogLevel: engine.LogLevel,
BootstrapIPs: engine.BootstrapIPs,
Extra: engine.Extra,
}
if err := h.StartEngine(ctx, engine.Name, engines.EngineType(engine.Type), config); err != nil {
// Rollback on failure
h.StopStack(ctx, manifest.Name)
return fmt.Errorf("failed to start %s: %w", engine.Name, err)
}
// Wait for health before starting dependent engines
if engine.WaitHealthy {
if err := h.waitHealthy(ctx, engine.Name, 30*time.Second); err != nil {
h.StopStack(ctx, manifest.Name)
return fmt.Errorf("engine %s failed health check: %w", engine.Name, err)
}
}
}
// Configure bridges if specified
if manifest.Bridge != nil {
if err := h.configureBridge(ctx, manifest.Bridge); err != nil {
return fmt.Errorf("failed to configure bridge: %w", err)
}
}
return nil
}
// StopStack stops all engines in a stack
func (h *Host) StopStack(ctx context.Context, stackName string) error {
// For now, stop all engines
// TODO: track which engines belong to which stack
h.mu.Lock()
engines := make([]string, 0, len(h.engines))
for name := range h.engines {
engines = append(engines, name)
}
h.mu.Unlock()
var lastErr error
for _, name := range engines {
if err := h.StopEngine(ctx, name); err != nil {
lastErr = err
}
}
return lastErr
}
// waitHealthy waits for an engine to become healthy
func (h *Host) waitHealthy(ctx context.Context, name string, timeout time.Duration) error {
h.mu.RLock()
engine, exists := h.engines[name]
h.mu.RUnlock()
if !exists {
return fmt.Errorf("engine %s not found", name)
}
deadline := time.Now().Add(timeout)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
health, err := engine.Health(ctx)
if err == nil && health.Healthy {
return nil
}
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for engine to become healthy")
}
case <-ctx.Done():
return ctx.Err()
}
}
}
// configureBridge sets up bridge between engines
func (h *Host) configureBridge(ctx context.Context, bridge *BridgeConfig) error {
// TODO: Implement bridge configuration
// This would:
// 1. Deploy bridge contracts
// 2. Start relayer
// 3. Configure message passing
return nil
}
// collectMetrics collects metrics from an engine
func (h *Host) collectMetrics(name string, engine engines.Engine) {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
h.mu.RLock()
if _, exists := h.engines[name]; !exists {
h.mu.RUnlock()
return // Engine stopped
}
h.mu.RUnlock()
metrics := engine.Metrics()
h.metrics.Record(name, metrics)
}
}
+225
View File
@@ -0,0 +1,225 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package orchestrator_test
import (
"context"
"testing"
"time"
"github.com/luxfi/netrunner/engines"
"github.com/luxfi/netrunner/orchestrator"
"github.com/stretchr/testify/require"
)
func TestMultiEngineOrchestration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
host := orchestrator.NewHost()
// Test starting Lux engine
t.Run("StartLuxEngine", func(t *testing.T) {
config := &engines.NodeConfig{
NetworkID: 96369,
HTTPPort: 19630,
StakingPort: 19631,
LogLevel: "info",
Extra: map[string]interface{}{
"staking-enabled": false,
"sybil-protection-enabled": false,
"health-check-frequency": "2s",
},
}
err := host.StartEngine(ctx, "test-lux", engines.EngineLux, config)
require.NoError(t, err)
// Wait for health
time.Sleep(5 * time.Second)
// Check engine is listed
engines := host.ListEngines()
require.Len(t, engines, 1)
require.Equal(t, "test-lux", engines[0].Name)
require.Equal(t, "lux", engines[0].Type)
// Stop engine
err = host.StopEngine(ctx, "test-lux")
require.NoError(t, err)
})
// Test starting multiple engines
t.Run("MultipleEngines", func(t *testing.T) {
// Start Lux
luxConfig := &engines.NodeConfig{
NetworkID: 96369,
HTTPPort: 19640,
StakingPort: 19641,
LogLevel: "info",
}
err := host.StartEngine(ctx, "lux-1", engines.EngineLux, luxConfig)
require.NoError(t, err)
// Start Avalanche
avaConfig := &engines.NodeConfig{
NetworkID: 43114,
HTTPPort: 19650,
StakingPort: 19651,
LogLevel: "info",
}
err = host.StartEngine(ctx, "ava-1", engines.EngineAvalanche, avaConfig)
require.NoError(t, err)
// Check both are running
engines := host.ListEngines()
require.Len(t, engines, 2)
// Clean up
host.StopEngine(ctx, "lux-1")
host.StopEngine(ctx, "ava-1")
})
}
func TestStackManifest(t *testing.T) {
manifest := &orchestrator.StackManifest{
Name: "test-stack",
Version: "1.0.0",
Description: "Test stack",
Engines: []orchestrator.EngineConfig{
{
Name: "lux-l1",
Type: "lux",
NetworkID: 96369,
HTTPPort: 9630,
StakingPort: 9631,
WaitHealthy: true,
},
{
Name: "lux-l2",
Type: "op",
NetworkID: 200200,
HTTPPort: 8545,
DependsOn: []string{"lux-l1"},
},
},
}
t.Run("Validate", func(t *testing.T) {
err := manifest.Validate()
require.NoError(t, err)
})
t.Run("SaveAndLoad", func(t *testing.T) {
// Save as YAML
yamlPath := "/tmp/test-stack.yaml"
err := manifest.Save(yamlPath)
require.NoError(t, err)
// Load back
loaded, err := orchestrator.LoadManifest(yamlPath)
require.NoError(t, err)
require.Equal(t, manifest.Name, loaded.Name)
require.Len(t, loaded.Engines, 2)
// Save as JSON
jsonPath := "/tmp/test-stack.json"
err = manifest.Save(jsonPath)
require.NoError(t, err)
// Load JSON
loaded, err = orchestrator.LoadManifest(jsonPath)
require.NoError(t, err)
require.Equal(t, manifest.Name, loaded.Name)
})
}
func TestPortManager(t *testing.T) {
pm := orchestrator.NewPortManager()
t.Run("AllocatePorts", func(t *testing.T) {
// Allocate HTTP ports
port1 := pm.AllocateHTTP()
port2 := pm.AllocateHTTP()
require.NotEqual(t, port1, port2)
// Allocate P2P ports
p2p1 := pm.AllocateP2P()
p2p2 := pm.AllocateP2P()
require.NotEqual(t, p2p1, p2p2)
// Release and reallocate
pm.Release(port1)
port3 := pm.AllocateHTTP()
// May or may not reuse port1 depending on system state
require.NotEqual(t, port3, port2)
})
t.Run("ReservePorts", func(t *testing.T) {
pm := orchestrator.NewPortManager()
// Reserve specific ports
err := pm.Reserve(20000, 20001, 20002)
require.NoError(t, err)
// Try to reserve again - should fail
err = pm.Reserve(20001)
require.Error(t, err)
})
}
func TestMetricsCollector(t *testing.T) {
mc := orchestrator.NewMetricsCollector()
t.Run("RecordAndRetrieve", func(t *testing.T) {
// Record metrics
mc.Record("engine1", map[string]interface{}{
"uptime_seconds": 100.0,
"running": true,
"peers": 5,
})
// Retrieve metrics
metrics := mc.Get("engine1")
require.NotNil(t, metrics)
require.Equal(t, "engine1", metrics.Name)
require.Equal(t, 100.0, metrics.Values["uptime_seconds"])
// Record more for history
for i := 0; i < 5; i++ {
mc.Record("engine1", map[string]interface{}{
"uptime_seconds": float64(100 + i*10),
"running": true,
})
time.Sleep(10 * time.Millisecond)
}
metrics = mc.Get("engine1")
require.Len(t, metrics.History, 6) // Initial + 5 more
})
t.Run("Summary", func(t *testing.T) {
mc := orchestrator.NewMetricsCollector()
mc.Record("engine1", map[string]interface{}{
"uptime_seconds": 100.0,
"running": true,
})
mc.Record("engine2", map[string]interface{}{
"uptime_seconds": 200.0,
"running": true,
})
mc.Record("engine3", map[string]interface{}{
"uptime_seconds": 50.0,
"running": false,
})
summary := mc.Summary()
require.Equal(t, 3, summary["engines"])
require.Equal(t, 350.0, summary["total_uptime_seconds"])
require.Equal(t, 2, summary["healthy_engines"])
})
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package orchestrator
import (
"sync"
"time"
)
// Indexer provides cross-engine blockchain indexing
type Indexer struct {
mu sync.RWMutex
data map[string]*IndexData
}
// IndexData contains indexed blockchain data
type IndexData struct {
Engine string
LastUpdate time.Time
BlockHeight uint64
Transactions uint64
Accounts uint64
Extra map[string]interface{}
}
// NewIndexer creates a new indexer
func NewIndexer() *Indexer {
return &Indexer{
data: make(map[string]*IndexData),
}
}
// Update updates index data for an engine
func (i *Indexer) Update(engine string, data *IndexData) {
i.mu.Lock()
defer i.mu.Unlock()
data.Engine = engine
data.LastUpdate = time.Now()
i.data[engine] = data
}
// Get retrieves index data for an engine
func (i *Indexer) Get(engine string) *IndexData {
i.mu.RLock()
defer i.mu.RUnlock()
return i.data[engine]
}
// GetAll retrieves all index data
func (i *Indexer) GetAll() map[string]*IndexData {
i.mu.RLock()
defer i.mu.RUnlock()
result := make(map[string]*IndexData)
for k, v := range i.data {
result[k] = v
}
return result
}
// Clear clears index data for an engine
func (i *Indexer) Clear(engine string) {
i.mu.Lock()
defer i.mu.Unlock()
delete(i.data, engine)
}
+188
View File
@@ -0,0 +1,188 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package orchestrator
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// StackManifest defines a multi-engine stack configuration
type StackManifest struct {
Name string `yaml:"name" json:"name"`
Version string `yaml:"version" json:"version"`
Description string `yaml:"description" json:"description"`
Engines []EngineConfig `yaml:"engines" json:"engines"`
Bridge *BridgeConfig `yaml:"bridge,omitempty" json:"bridge,omitempty"`
Networks []NetworkConfig `yaml:"networks,omitempty" json:"networks,omitempty"`
}
// EngineConfig defines an engine configuration
type EngineConfig struct {
Name string `yaml:"name" json:"name"`
Type string `yaml:"type" json:"type"` // lux, avalanche, geth, op
Binary string `yaml:"binary,omitempty" json:"binary,omitempty"`
NetworkID uint32 `yaml:"network_id" json:"network_id"`
HTTPPort uint16 `yaml:"http_port,omitempty" json:"http_port,omitempty"`
WSPort uint16 `yaml:"ws_port,omitempty" json:"ws_port,omitempty"`
StakingPort uint16 `yaml:"staking_port,omitempty" json:"staking_port,omitempty"`
DataDir string `yaml:"data_dir,omitempty" json:"data_dir,omitempty"`
LogLevel string `yaml:"log_level,omitempty" json:"log_level,omitempty"`
BootstrapIPs []string `yaml:"bootstrap_ips,omitempty" json:"bootstrap_ips,omitempty"`
Extra map[string]interface{} `yaml:"extra,omitempty" json:"extra,omitempty"`
WaitHealthy bool `yaml:"wait_healthy,omitempty" json:"wait_healthy,omitempty"`
DependsOn []string `yaml:"depends_on,omitempty" json:"depends_on,omitempty"`
}
// BridgeConfig defines bridge configuration between engines
type BridgeConfig struct {
Type string `yaml:"type" json:"type"` // awm, ibc, ccip
Source string `yaml:"source" json:"source"`
Destination string `yaml:"destination" json:"destination"`
RelayerKey string `yaml:"relayer_key,omitempty" json:"relayer_key,omitempty"`
Contracts map[string]string `yaml:"contracts,omitempty" json:"contracts,omitempty"`
}
// NetworkConfig defines network topology
type NetworkConfig struct {
Name string `yaml:"name" json:"name"`
Type string `yaml:"type" json:"type"` // l1, l2, l3
Engine string `yaml:"engine" json:"engine"`
Parent string `yaml:"parent,omitempty" json:"parent,omitempty"`
ChainID uint64 `yaml:"chain_id" json:"chain_id"`
Endpoints []string `yaml:"endpoints,omitempty" json:"endpoints,omitempty"`
}
// Validate validates the manifest
func (m *StackManifest) Validate() error {
if m.Name == "" {
return fmt.Errorf("manifest name is required")
}
if len(m.Engines) == 0 {
return fmt.Errorf("at least one engine is required")
}
// Check engine names are unique
names := make(map[string]bool)
for _, e := range m.Engines {
if e.Name == "" {
return fmt.Errorf("engine name is required")
}
if names[e.Name] {
return fmt.Errorf("duplicate engine name: %s", e.Name)
}
names[e.Name] = true
// Validate engine type
switch e.Type {
case "lux", "avalanche", "geth", "op":
// Valid
default:
return fmt.Errorf("invalid engine type: %s", e.Type)
}
// Check dependencies exist
for _, dep := range e.DependsOn {
depFound := false
for _, other := range m.Engines {
if other.Name == dep {
depFound = true
break
}
}
if !depFound {
return fmt.Errorf("engine %s depends on unknown engine %s", e.Name, dep)
}
}
}
// Validate bridge if present
if m.Bridge != nil {
if m.Bridge.Source == "" || m.Bridge.Destination == "" {
return fmt.Errorf("bridge source and destination are required")
}
// Check source and destination exist
sourceFound := false
destFound := false
for _, e := range m.Engines {
if e.Name == m.Bridge.Source {
sourceFound = true
}
if e.Name == m.Bridge.Destination {
destFound = true
}
}
if !sourceFound {
return fmt.Errorf("bridge source %s not found in engines", m.Bridge.Source)
}
if !destFound {
return fmt.Errorf("bridge destination %s not found in engines", m.Bridge.Destination)
}
}
return nil
}
// LoadManifest loads a manifest from file
func LoadManifest(path string) (*StackManifest, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read manifest: %w", err)
}
var manifest StackManifest
// Try YAML first
if err := yaml.Unmarshal(data, &manifest); err == nil {
return &manifest, nil
}
// Try JSON
if err := json.Unmarshal(data, &manifest); err == nil {
return &manifest, nil
}
return nil, fmt.Errorf("failed to parse manifest as YAML or JSON")
}
// SaveManifest saves a manifest to file
func (m *StackManifest) Save(path string) error {
// Ensure directory exists
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// Marshal based on extension
var data []byte
var err error
ext := filepath.Ext(path)
switch ext {
case ".json":
data, err = json.MarshalIndent(m, "", " ")
case ".yaml", ".yml":
data, err = yaml.Marshal(m)
default:
// Default to YAML
data, err = yaml.Marshal(m)
}
if err != nil {
return fmt.Errorf("failed to marshal manifest: %w", err)
}
if err := ioutil.WriteFile(path, data, 0644); err != nil {
return fmt.Errorf("failed to write manifest: %w", err)
}
return nil
}
+118
View File
@@ -0,0 +1,118 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package orchestrator
import (
"sync"
"time"
)
// MetricsCollector collects metrics from engines
type MetricsCollector struct {
mu sync.RWMutex
metrics map[string]*EngineMetrics
}
// EngineMetrics contains metrics for an engine
type EngineMetrics struct {
Name string
LastUpdated time.Time
Values map[string]interface{}
History []MetricSnapshot
}
// MetricSnapshot is a point-in-time metric snapshot
type MetricSnapshot struct {
Timestamp time.Time
Values map[string]interface{}
}
// NewMetricsCollector creates a new metrics collector
func NewMetricsCollector() *MetricsCollector {
return &MetricsCollector{
metrics: make(map[string]*EngineMetrics),
}
}
// Record records metrics for an engine
func (mc *MetricsCollector) Record(engine string, values map[string]interface{}) {
mc.mu.Lock()
defer mc.mu.Unlock()
em, exists := mc.metrics[engine]
if !exists {
em = &EngineMetrics{
Name: engine,
History: make([]MetricSnapshot, 0, 100),
}
mc.metrics[engine] = em
}
em.LastUpdated = time.Now()
em.Values = values
// Add to history (keep last 100 snapshots)
snapshot := MetricSnapshot{
Timestamp: em.LastUpdated,
Values: values,
}
em.History = append(em.History, snapshot)
if len(em.History) > 100 {
em.History = em.History[1:]
}
}
// Get returns current metrics for an engine
func (mc *MetricsCollector) Get(engine string) *EngineMetrics {
mc.mu.RLock()
defer mc.mu.RUnlock()
return mc.metrics[engine]
}
// GetAll returns all metrics
func (mc *MetricsCollector) GetAll() map[string]*EngineMetrics {
mc.mu.RLock()
defer mc.mu.RUnlock()
result := make(map[string]*EngineMetrics)
for k, v := range mc.metrics {
result[k] = v
}
return result
}
// Clear clears metrics for an engine
func (mc *MetricsCollector) Clear(engine string) {
mc.mu.Lock()
defer mc.mu.Unlock()
delete(mc.metrics, engine)
}
// Summary returns a summary of all metrics
func (mc *MetricsCollector) Summary() map[string]interface{} {
mc.mu.RLock()
defer mc.mu.RUnlock()
summary := make(map[string]interface{})
summary["engines"] = len(mc.metrics)
var totalUptime float64
var healthyCount int
for _, em := range mc.metrics {
if uptime, ok := em.Values["uptime_seconds"].(float64); ok {
totalUptime += uptime
}
if running, ok := em.Values["running"].(bool); ok && running {
healthyCount++
}
}
summary["total_uptime_seconds"] = totalUptime
summary["healthy_engines"] = healthyCount
return summary
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package orchestrator
import (
"fmt"
"net"
"sync"
)
// PortManager manages port allocation for engines
type PortManager struct {
mu sync.Mutex
allocated map[uint16]bool
httpBase uint16
p2pBase uint16
}
// NewPortManager creates a new port manager
func NewPortManager() *PortManager {
return &PortManager{
allocated: make(map[uint16]bool),
httpBase: 9630,
p2pBase: 9631,
}
}
// AllocateHTTP allocates an HTTP port
func (pm *PortManager) AllocateHTTP() uint16 {
pm.mu.Lock()
defer pm.mu.Unlock()
port := pm.httpBase
for pm.isInUse(port) || pm.allocated[port] {
port += 10 // Skip by 10 to avoid conflicts with P2P ports
}
pm.allocated[port] = true
return port
}
// AllocateP2P allocates a P2P port
func (pm *PortManager) AllocateP2P() uint16 {
pm.mu.Lock()
defer pm.mu.Unlock()
port := pm.p2pBase
for pm.isInUse(port) || pm.allocated[port] {
port += 10
}
pm.allocated[port] = true
return port
}
// Release releases a port
func (pm *PortManager) Release(port uint16) {
pm.mu.Lock()
defer pm.mu.Unlock()
delete(pm.allocated, port)
}
// isInUse checks if a port is in use
func (pm *PortManager) isInUse(port uint16) bool {
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return true // Port is in use
}
ln.Close()
return false
}
// Reserve reserves specific ports
func (pm *PortManager) Reserve(ports ...uint16) error {
pm.mu.Lock()
defer pm.mu.Unlock()
// Check all ports are available first
for _, port := range ports {
if pm.allocated[port] {
return fmt.Errorf("port %d already allocated", port)
}
if pm.isInUse(port) {
return fmt.Errorf("port %d already in use", port)
}
}
// Reserve all ports
for _, port := range ports {
pm.allocated[port] = true
}
return nil
}
+45
View File
@@ -0,0 +1,45 @@
name: lux-avalanche-bridge
version: 1.0.0
description: Lux L1 + Avalanche C-Chain with AWM bridge
engines:
- name: lux-l1
type: lux
network_id: 96369
http_port: 9630
staking_port: 9631
log_level: info
wait_healthy: true
extra:
staking-enabled: false
sybil-protection-enabled: false
health-check-frequency: 2s
- name: avalanche-c
type: avalanche
network_id: 43114
http_port: 9650
staking_port: 9651
log_level: info
wait_healthy: true
depends_on:
- lux-l1
bridge:
type: awm
source: avalanche-c
destination: lux-l1
contracts:
messenger: "0x0000000000000000000000000000000000000000"
registry: "0x0000000000000000000000000000000000000000"
networks:
- name: lux-mainnet
type: l1
engine: lux-l1
chain_id: 96369
- name: avalanche-mainnet
type: l1
engine: avalanche-c
chain_id: 43114
+48
View File
@@ -0,0 +1,48 @@
name: lux-l1-l2-local
version: 1.0.0
description: Local Lux L1 with OP-based L2
engines:
- name: lux-l1
type: lux
network_id: 96369
http_port: 9630
staking_port: 9631
log_level: info
wait_healthy: true
extra:
staking-enabled: false
sybil-protection-enabled: false
snow-sample-size: 1
snow-quorum-size: 1
- name: lux-l2-op
type: op
network_id: 200200
http_port: 8545
ws_port: 8546
staking_port: 30303
log_level: info
depends_on:
- lux-l1
extra:
l1-rpc: http://localhost:9630/ext/bc/C/rpc
sequencer: true
sequencer-l1-confs: 0
networks:
- name: lux-local
type: l1
engine: lux-l1
chain_id: 96369
endpoints:
- http://localhost:9630
- name: lux-l2
type: l2
engine: lux-l2-op
parent: lux-local
chain_id: 200200
endpoints:
- http://localhost:8545
- ws://localhost:8546