mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
chore(node): one way only — delete four dead duplicate paths
Each removed package was a SECOND way to do something that already has a
canonical first way. Zero importers workspace-wide; verified identical build
output before/after (pre-existing keyutil breakage unchanged).
- chains/rpc 1250 lines whose own header says 'This replaces lines
941-990' of chains/manager.go. The replacement never
happened; manager.go still registers handlers inline.
- node/config.go 265-line shadow of the canonical node/config/node/config.go.
- vms/mpcvm self-described 'thin backward-compatibility alias' wrappers
- vms/dexvm over github.com/luxfi/chains/*. No backwards compatibility,
only forwards perfection.
Simple made easy: one name, one home, one path.
This commit is contained in:
@@ -1,244 +0,0 @@
|
|||||||
# Robust RPC Handler Registration System
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This package provides a bulletproof RPC handler registration system for the Lux node, designed to handle the complexities of local development where nodes are frequently restarted. It replaces the fragile inline registration logic with a robust, maintainable solution.
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
### 🔄 Automatic Retry Logic
|
|
||||||
- Exponential backoff for transient failures
|
|
||||||
- Configurable retry count and wait times
|
|
||||||
- Context-aware cancellation support
|
|
||||||
|
|
||||||
### ✅ Built-in Health Checks
|
|
||||||
- Automatic validation after registration
|
|
||||||
- Batch health checking for all chains
|
|
||||||
- Detailed diagnostics for failures
|
|
||||||
|
|
||||||
### 🎯 Single Source of Truth
|
|
||||||
- Centralized route construction logic
|
|
||||||
- Consistent path formatting
|
|
||||||
- No duplicate code or magic strings
|
|
||||||
|
|
||||||
### 🛡️ Defensive Programming
|
|
||||||
- Nil checks on all inputs
|
|
||||||
- Handler validation before registration
|
|
||||||
- Graceful degradation on failures
|
|
||||||
|
|
||||||
### 📊 Developer-Friendly Debugging
|
|
||||||
- Clear, actionable error messages
|
|
||||||
- Comprehensive logging at appropriate levels
|
|
||||||
- Built-in diagnostic tools
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────┐
|
|
||||||
│ Chain Manager │
|
|
||||||
└──────────┬──────────┘
|
|
||||||
│ Creates Chain
|
|
||||||
▼
|
|
||||||
┌─────────────────────┐
|
|
||||||
│ ChainHandlerRegistrar│
|
|
||||||
└──────────┬──────────┘
|
|
||||||
│ Extracts Handlers
|
|
||||||
▼
|
|
||||||
┌─────────────────────┐
|
|
||||||
│ Handler Manager │
|
|
||||||
└──────────┬──────────┘
|
|
||||||
│ Registers with Retries
|
|
||||||
▼
|
|
||||||
┌─────────────────────┐
|
|
||||||
│ API Server │
|
|
||||||
└─────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Basic Integration
|
|
||||||
|
|
||||||
Replace the handler registration code in `chains/manager.go` (lines 941-990) with:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Create robust registrar
|
|
||||||
registrar := rpc.NewChainHandlerRegistrar(
|
|
||||||
m.Server,
|
|
||||||
m.Log,
|
|
||||||
m.CChainID,
|
|
||||||
m.PChainID,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register handlers
|
|
||||||
if err := registrar.RegisterChainHandlers(ctx, chainParams.ID, chain.VM); err != nil {
|
|
||||||
m.Log.Error("Failed to register handlers", log.Err(err))
|
|
||||||
// Decide if this should be fatal or not
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Development environment - fail fast
|
|
||||||
registrar.SetRetryConfig(2, 50*time.Millisecond)
|
|
||||||
|
|
||||||
// Production environment - more robust
|
|
||||||
registrar.SetRetryConfig(5, 200*time.Millisecond)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debugging
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Get route information
|
|
||||||
info, exists := registrar.GetRouteInfo(chainID)
|
|
||||||
if exists {
|
|
||||||
fmt.Printf("Chain %s routes: %v\n", chainID, info.Endpoints)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run health checks
|
|
||||||
results := registrar.HealthCheckAll()
|
|
||||||
for chainID, healthy := range results {
|
|
||||||
fmt.Printf("Chain %s: %v\n", chainID, healthy)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate specific endpoint
|
|
||||||
err := registrar.ValidateEndpoint(chainID, "/rpc")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using the Debug Tool
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Quick diagnosis from CLI
|
|
||||||
rpc.QuickDiagnose("localhost:9650", chainID, "C")
|
|
||||||
|
|
||||||
// Programmatic diagnosis
|
|
||||||
tool := rpc.NewDebugTool("localhost:9650", logger)
|
|
||||||
report := tool.DiagnoseEndpoint(chainID, "C")
|
|
||||||
fmt.Println(report.String())
|
|
||||||
```
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
### HandlerManager (`handler_manager.go`)
|
|
||||||
Core registration logic with retry mechanism and health checks.
|
|
||||||
|
|
||||||
**Key Methods:**
|
|
||||||
- `RegisterChainHandlers()` - Main registration entry point
|
|
||||||
- `HealthCheckRoute()` - Validates handler responsiveness
|
|
||||||
- `GetRouteInfo()` - Retrieves registration details
|
|
||||||
|
|
||||||
### ChainHandlerRegistrar (`chain_integration.go`)
|
|
||||||
Bridge between chain manager and handler manager.
|
|
||||||
|
|
||||||
**Key Methods:**
|
|
||||||
- `RegisterChainHandlers()` - Extracts and registers handlers
|
|
||||||
- `ValidateEndpoint()` - Tests specific endpoints
|
|
||||||
- `GetAllRoutes()` - Returns all registered routes
|
|
||||||
|
|
||||||
### DebugTool (`debug_tool.go`)
|
|
||||||
Comprehensive endpoint diagnostics for developers.
|
|
||||||
|
|
||||||
**Key Methods:**
|
|
||||||
- `DiagnoseEndpoint()` - Full endpoint analysis
|
|
||||||
- `QuickDiagnose()` - CLI-friendly diagnosis
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
The system uses clear, actionable errors:
|
|
||||||
|
|
||||||
```go
|
|
||||||
errNilHandler = errors.New("handler is nil")
|
|
||||||
errNilServer = errors.New("server is nil")
|
|
||||||
errEmptyEndpoint = errors.New("endpoint is empty")
|
|
||||||
errRegistrationFailed = errors.New("handler registration failed")
|
|
||||||
errHealthCheckFailed = errors.New("health check failed")
|
|
||||||
```
|
|
||||||
|
|
||||||
Each error includes context about what failed and why.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Comprehensive test coverage including:
|
|
||||||
- Successful registration scenarios
|
|
||||||
- Validation failure cases
|
|
||||||
- Retry logic verification
|
|
||||||
- Health check validation
|
|
||||||
- Context cancellation
|
|
||||||
- Performance benchmarks
|
|
||||||
|
|
||||||
Run tests:
|
|
||||||
```bash
|
|
||||||
go test ./chains/rpc/... -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Issues and Solutions
|
|
||||||
|
|
||||||
### Issue: Handlers not accessible after registration
|
|
||||||
**Solution:** Check health status with `HealthCheckAll()` and review debug output.
|
|
||||||
|
|
||||||
### Issue: Registration fails with "already exists"
|
|
||||||
**Solution:** The retry logic handles this. If persistent, check for duplicate registration attempts.
|
|
||||||
|
|
||||||
### Issue: Slow registration during development
|
|
||||||
**Solution:** Reduce retry count and wait time using `SetRetryConfig()`.
|
|
||||||
|
|
||||||
### Issue: Can't find the correct endpoint URL
|
|
||||||
**Solution:** Use `DebugTool.DiagnoseEndpoint()` to test all URL patterns.
|
|
||||||
|
|
||||||
## Migration Guide
|
|
||||||
|
|
||||||
1. **Update imports:**
|
|
||||||
```go
|
|
||||||
import "github.com/luxfi/node/chains/rpc"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Replace inline registration (lines 941-990 in manager.go):**
|
|
||||||
```go
|
|
||||||
// Old code: complex type checking and manual registration
|
|
||||||
// New code: single function call
|
|
||||||
registrar := rpc.NewChainHandlerRegistrar(...)
|
|
||||||
registrar.RegisterChainHandlers(...)
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Add health monitoring (optional):**
|
|
||||||
```go
|
|
||||||
go func() {
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
registrar.HealthCheckAll()
|
|
||||||
}()
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Add debugging endpoints (optional):**
|
|
||||||
```go
|
|
||||||
http.HandleFunc("/debug/handlers", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
routes := registrar.GetAllRoutes()
|
|
||||||
json.NewEncoder(w).Encode(routes)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
- Registration: ~1ms per handler (without retries)
|
|
||||||
- Health check: ~10ms per chain
|
|
||||||
- Memory overhead: ~1KB per registered chain
|
|
||||||
- No goroutine leaks or resource issues
|
|
||||||
|
|
||||||
## Future Improvements
|
|
||||||
|
|
||||||
Potential enhancements:
|
|
||||||
- Metrics integration for registration success/failure rates
|
|
||||||
- Automatic re-registration on failure
|
|
||||||
- WebSocket-specific health checks
|
|
||||||
- gRPC handler support
|
|
||||||
- Handler versioning for upgrades
|
|
||||||
|
|
||||||
## Philosophy
|
|
||||||
|
|
||||||
This implementation follows core Go principles:
|
|
||||||
- **Explicit over implicit** - Clear registration flow
|
|
||||||
- **Errors are values** - Proper error handling throughout
|
|
||||||
- **Simple over clever** - Straightforward retry logic
|
|
||||||
- **Composition over inheritance** - Small, focused components
|
|
||||||
- **Documentation is code** - Self-documenting with clear names
|
|
||||||
|
|
||||||
The system is designed to be bulletproof for development while remaining simple to understand and maintain.
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
package rpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/luxfi/ids"
|
|
||||||
"github.com/luxfi/log"
|
|
||||||
"github.com/luxfi/node/server/http"
|
|
||||||
"github.com/luxfi/node/vms"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ChainHandlerRegistrar provides a clean interface for chain manager to register handlers.
|
|
||||||
// This replaces the inline registration logic with a more robust, testable solution.
|
|
||||||
type ChainHandlerRegistrar struct {
|
|
||||||
manager *HandlerManager
|
|
||||||
server server.Server
|
|
||||||
log log.Logger
|
|
||||||
cChainID ids.ID // Special handling for C-Chain
|
|
||||||
pChainID ids.ID // Platform chain ID for validation
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewChainHandlerRegistrar creates a registrar for chain handler registration.
|
|
||||||
// Encapsulates all the registration logic in one place.
|
|
||||||
func NewChainHandlerRegistrar(
|
|
||||||
server server.Server,
|
|
||||||
logger log.Logger,
|
|
||||||
cChainID ids.ID,
|
|
||||||
pChainID ids.ID,
|
|
||||||
) *ChainHandlerRegistrar {
|
|
||||||
return &ChainHandlerRegistrar{
|
|
||||||
manager: NewHandlerManager(server, logger),
|
|
||||||
server: server,
|
|
||||||
log: logger,
|
|
||||||
cChainID: cChainID,
|
|
||||||
pChainID: pChainID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterChainHandlers is the main entry point from chain manager.
|
|
||||||
// Handles all the complexity of VM type checking and handler extraction.
|
|
||||||
func (r *ChainHandlerRegistrar) RegisterChainHandlers(
|
|
||||||
ctx context.Context,
|
|
||||||
chainID ids.ID,
|
|
||||||
vm interface{},
|
|
||||||
) error {
|
|
||||||
r.log.Info("Attempting to register chain handlers",
|
|
||||||
log.Stringer("chainID", chainID),
|
|
||||||
log.String("vmType", fmt.Sprintf("%T", vm)))
|
|
||||||
|
|
||||||
// Don't register handlers for Platform VM
|
|
||||||
if chainID == r.pChainID {
|
|
||||||
r.log.Debug("Skipping handler registration for Platform VM")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract handlers from VM
|
|
||||||
handlers, err := r.extractHandlers(ctx, vm)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to extract handlers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(handlers) == 0 {
|
|
||||||
r.log.Info("VM does not provide any handlers",
|
|
||||||
log.Stringer("chainID", chainID))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine chain alias (special case for C-Chain)
|
|
||||||
alias := r.getChainAlias(chainID)
|
|
||||||
|
|
||||||
// Register with robust handler manager
|
|
||||||
return r.manager.RegisterChainHandlers(ctx, chainID, alias, handlers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractHandlers attempts to get handlers from the VM using multiple strategies.
|
|
||||||
// Handles different VM wrapper types gracefully.
|
|
||||||
func (r *ChainHandlerRegistrar) extractHandlers(
|
|
||||||
ctx context.Context,
|
|
||||||
vm interface{},
|
|
||||||
) (map[string]http.Handler, error) {
|
|
||||||
// First try direct interface check
|
|
||||||
if provider, ok := vm.(vms.HandlerProvider); ok {
|
|
||||||
r.log.Debug("VM directly implements HandlerProvider")
|
|
||||||
return provider.CreateHandlers(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try using the delegate helper (handles wrapped VMs)
|
|
||||||
handlers, err := vms.DelegateHandlers(ctx, vm)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("handler delegation failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(handlers) > 0 {
|
|
||||||
r.log.Debug("Successfully extracted handlers via delegation",
|
|
||||||
log.Int("count", len(handlers)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return handlers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getChainAlias returns the appropriate alias for a chain.
|
|
||||||
// C-Chain gets special treatment, others use their ID.
|
|
||||||
func (r *ChainHandlerRegistrar) getChainAlias(chainID ids.ID) string {
|
|
||||||
if chainID == r.cChainID {
|
|
||||||
return "C"
|
|
||||||
}
|
|
||||||
// Could extend this for X-Chain and P-Chain if needed
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRouteInfo returns information about a specific chain's registered routes.
|
|
||||||
// Useful for debugging and operational visibility.
|
|
||||||
func (r *ChainHandlerRegistrar) GetRouteInfo(chainID ids.ID) (*RouteInfo, bool) {
|
|
||||||
return r.manager.GetRouteInfo(chainID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllRoutes returns all registered routes across all chains.
|
|
||||||
// Complete visibility for monitoring and debugging.
|
|
||||||
func (r *ChainHandlerRegistrar) GetAllRoutes() map[string]*RouteInfo {
|
|
||||||
return r.manager.GetAllRoutes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HealthCheckAll performs health checks on all registered routes.
|
|
||||||
// Returns a map of chainID -> healthy status.
|
|
||||||
func (r *ChainHandlerRegistrar) HealthCheckAll() map[string]bool {
|
|
||||||
return r.manager.HealthCheckAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRetryConfig allows tuning of retry behavior for different environments.
|
|
||||||
// Production might want more retries, dev might want faster failures.
|
|
||||||
func (r *ChainHandlerRegistrar) SetRetryConfig(maxRetries int, initialWait time.Duration) {
|
|
||||||
r.manager.SetRetryConfig(maxRetries, initialWait)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateEndpoint performs a test request against a specific endpoint.
|
|
||||||
// Useful for debugging specific handler issues.
|
|
||||||
func (r *ChainHandlerRegistrar) ValidateEndpoint(
|
|
||||||
chainID ids.ID,
|
|
||||||
endpoint string,
|
|
||||||
) error {
|
|
||||||
info, exists := r.manager.GetRouteInfo(chainID)
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("no routes registered for chain %s", chainID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the full URL
|
|
||||||
fullURL := fmt.Sprintf("/v1/%s%s", info.Base, endpoint)
|
|
||||||
|
|
||||||
r.log.Info("Validating endpoint",
|
|
||||||
log.Stringer("chainID", chainID),
|
|
||||||
log.String("url", fullURL))
|
|
||||||
|
|
||||||
// Validates endpoint registration
|
|
||||||
for _, registered := range info.Endpoints {
|
|
||||||
if registered == endpoint {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("endpoint %s not found in registered endpoints: %v",
|
|
||||||
endpoint, info.Endpoints)
|
|
||||||
}
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
package rpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"github.com/go-json-experiment/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/luxfi/ids"
|
|
||||||
"github.com/luxfi/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DebugTool provides utilities for debugging RPC handler issues.
|
|
||||||
// Developer-friendly diagnostics with clear, actionable output.
|
|
||||||
type DebugTool struct {
|
|
||||||
baseURL string
|
|
||||||
client *http.Client
|
|
||||||
log log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDebugTool creates a debug tool for RPC endpoint testing.
|
|
||||||
func NewDebugTool(baseURL string, logger log.Logger) *DebugTool {
|
|
||||||
if !strings.HasPrefix(baseURL, "http") {
|
|
||||||
baseURL = "http://" + baseURL
|
|
||||||
}
|
|
||||||
|
|
||||||
return &DebugTool{
|
|
||||||
baseURL: strings.TrimSuffix(baseURL, "/"),
|
|
||||||
client: &http.Client{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
},
|
|
||||||
log: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiagnoseEndpoint performs comprehensive diagnostics on an RPC endpoint.
|
|
||||||
// Returns detailed information about what's working and what's not.
|
|
||||||
func (d *DebugTool) DiagnoseEndpoint(chainID ids.ID, alias string) *DiagnosticReport {
|
|
||||||
report := &DiagnosticReport{
|
|
||||||
ChainID: chainID,
|
|
||||||
Alias: alias,
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
Tests: make([]TestResult, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test different URL patterns
|
|
||||||
urlPatterns := d.getURLPatterns(chainID, alias)
|
|
||||||
|
|
||||||
for _, pattern := range urlPatterns {
|
|
||||||
result := d.testEndpoint(pattern)
|
|
||||||
report.Tests = append(report.Tests, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test common RPC methods
|
|
||||||
if bestURL := report.GetBestURL(); bestURL != "" {
|
|
||||||
report.RPCTests = d.testRPCMethods(bestURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
return report
|
|
||||||
}
|
|
||||||
|
|
||||||
// getURLPatterns returns all possible URL patterns to test.
|
|
||||||
func (d *DebugTool) getURLPatterns(chainID ids.ID, alias string) []string {
|
|
||||||
patterns := []string{
|
|
||||||
fmt.Sprintf("%s/v1/bc/%s/rpc", d.baseURL, chainID.String()),
|
|
||||||
fmt.Sprintf("%s/v1/bc/%s/ws", d.baseURL, chainID.String()),
|
|
||||||
fmt.Sprintf("%s/v1/bc/%s", d.baseURL, chainID.String()),
|
|
||||||
}
|
|
||||||
|
|
||||||
if alias != "" && alias != chainID.String() {
|
|
||||||
patterns = append(patterns,
|
|
||||||
fmt.Sprintf("%s/v1/bc/%s/rpc", d.baseURL, alias),
|
|
||||||
fmt.Sprintf("%s/v1/bc/%s/ws", d.baseURL, alias),
|
|
||||||
fmt.Sprintf("%s/v1/bc/%s", d.baseURL, alias),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also test without /v1 prefix (some setups might differ)
|
|
||||||
patterns = append(patterns,
|
|
||||||
fmt.Sprintf("%s/bc/%s/rpc", d.baseURL, chainID.String()),
|
|
||||||
)
|
|
||||||
|
|
||||||
return patterns
|
|
||||||
}
|
|
||||||
|
|
||||||
// testEndpoint tests a single endpoint URL.
|
|
||||||
func (d *DebugTool) testEndpoint(url string) TestResult {
|
|
||||||
result := TestResult{
|
|
||||||
URL: url,
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// First try a simple GET
|
|
||||||
resp, err := d.client.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
result.Error = fmt.Sprintf("GET failed: %v", err)
|
|
||||||
result.Success = false
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
result.StatusCode = resp.StatusCode
|
|
||||||
|
|
||||||
// Read body for debugging
|
|
||||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
||||||
result.Response = string(bodyBytes)
|
|
||||||
|
|
||||||
// Now try a POST with JSON-RPC
|
|
||||||
rpcReq := map[string]interface{}{
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "web3_clientVersion",
|
|
||||||
"params": []interface{}{},
|
|
||||||
"id": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonBytes, _ := json.Marshal(rpcReq)
|
|
||||||
postResp, err := d.client.Post(url, "application/json", bytes.NewReader(jsonBytes))
|
|
||||||
if err != nil {
|
|
||||||
result.Error = fmt.Sprintf("POST failed: %v", err)
|
|
||||||
result.Success = false
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
defer postResp.Body.Close()
|
|
||||||
|
|
||||||
result.StatusCode = postResp.StatusCode
|
|
||||||
|
|
||||||
// Check if we got a valid JSON-RPC response
|
|
||||||
var rpcResp map[string]interface{}
|
|
||||||
if err := json.UnmarshalRead(postResp.Body, &rpcResp); err == nil {
|
|
||||||
if _, hasResult := rpcResp["result"]; hasResult {
|
|
||||||
result.Success = true
|
|
||||||
result.Response = fmt.Sprintf("Valid JSON-RPC response: %v", rpcResp["result"])
|
|
||||||
} else if errObj, hasError := rpcResp["error"]; hasError {
|
|
||||||
result.Success = false
|
|
||||||
result.Response = fmt.Sprintf("JSON-RPC error: %v", errObj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// testRPCMethods tests common RPC methods against an endpoint.
|
|
||||||
func (d *DebugTool) testRPCMethods(url string) []RPCTest {
|
|
||||||
methods := []string{
|
|
||||||
"web3_clientVersion",
|
|
||||||
"eth_blockNumber",
|
|
||||||
"eth_chainId",
|
|
||||||
"net_version",
|
|
||||||
"eth_syncing",
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := make([]RPCTest, 0, len(methods))
|
|
||||||
|
|
||||||
for _, method := range methods {
|
|
||||||
test := RPCTest{
|
|
||||||
Method: method,
|
|
||||||
URL: url,
|
|
||||||
}
|
|
||||||
|
|
||||||
req := map[string]interface{}{
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": method,
|
|
||||||
"params": []interface{}{},
|
|
||||||
"id": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonBytes, _ := json.Marshal(req)
|
|
||||||
resp, err := d.client.Post(url, "application/json", bytes.NewReader(jsonBytes))
|
|
||||||
if err != nil {
|
|
||||||
test.Error = err.Error()
|
|
||||||
test.Success = false
|
|
||||||
} else {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var result map[string]interface{}
|
|
||||||
if err := json.UnmarshalRead(resp.Body, &result); err == nil {
|
|
||||||
if res, ok := result["result"]; ok {
|
|
||||||
test.Success = true
|
|
||||||
test.Result = fmt.Sprintf("%v", res)
|
|
||||||
} else if errObj, ok := result["error"]; ok {
|
|
||||||
test.Error = fmt.Sprintf("%v", errObj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tests = append(tests, test)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tests
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiagnosticReport contains comprehensive endpoint diagnostic information.
|
|
||||||
type DiagnosticReport struct {
|
|
||||||
ChainID ids.ID
|
|
||||||
Alias string
|
|
||||||
Timestamp time.Time
|
|
||||||
Tests []TestResult
|
|
||||||
RPCTests []RPCTest
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestResult represents a single endpoint test result.
|
|
||||||
type TestResult struct {
|
|
||||||
URL string
|
|
||||||
Success bool
|
|
||||||
StatusCode int
|
|
||||||
Response string
|
|
||||||
Error string
|
|
||||||
Timestamp time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// RPCTest represents a test of a specific RPC method.
|
|
||||||
type RPCTest struct {
|
|
||||||
Method string
|
|
||||||
URL string
|
|
||||||
Success bool
|
|
||||||
Result string
|
|
||||||
Error string
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBestURL returns the first working URL from the tests.
|
|
||||||
func (r *DiagnosticReport) GetBestURL() string {
|
|
||||||
for _, test := range r.Tests {
|
|
||||||
if test.Success {
|
|
||||||
return test.URL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a human-readable report.
|
|
||||||
func (r *DiagnosticReport) String() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(fmt.Sprintf("=== RPC Endpoint Diagnostic Report ===\n"))
|
|
||||||
b.WriteString(fmt.Sprintf("Chain ID: %s\n", r.ChainID))
|
|
||||||
if r.Alias != "" {
|
|
||||||
b.WriteString(fmt.Sprintf("Alias: %s\n", r.Alias))
|
|
||||||
}
|
|
||||||
b.WriteString(fmt.Sprintf("Timestamp: %s\n\n", r.Timestamp.Format(time.RFC3339)))
|
|
||||||
|
|
||||||
b.WriteString("=== Endpoint Tests ===\n")
|
|
||||||
for _, test := range r.Tests {
|
|
||||||
status := "❌ FAILED"
|
|
||||||
if test.Success {
|
|
||||||
status = "✅ SUCCESS"
|
|
||||||
}
|
|
||||||
b.WriteString(fmt.Sprintf("\n%s %s\n", status, test.URL))
|
|
||||||
b.WriteString(fmt.Sprintf(" Status Code: %d\n", test.StatusCode))
|
|
||||||
if test.Error != "" {
|
|
||||||
b.WriteString(fmt.Sprintf(" Error: %s\n", test.Error))
|
|
||||||
}
|
|
||||||
if test.Response != "" && len(test.Response) < 200 {
|
|
||||||
b.WriteString(fmt.Sprintf(" Response: %s\n", test.Response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.RPCTests) > 0 {
|
|
||||||
b.WriteString("\n=== RPC Method Tests ===\n")
|
|
||||||
for _, test := range r.RPCTests {
|
|
||||||
status := "❌"
|
|
||||||
if test.Success {
|
|
||||||
status = "✅"
|
|
||||||
}
|
|
||||||
b.WriteString(fmt.Sprintf("%s %s: ", status, test.Method))
|
|
||||||
if test.Success {
|
|
||||||
b.WriteString(test.Result)
|
|
||||||
} else {
|
|
||||||
b.WriteString(test.Error)
|
|
||||||
}
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n=== Recommendations ===\n")
|
|
||||||
if bestURL := r.GetBestURL(); bestURL != "" {
|
|
||||||
b.WriteString(fmt.Sprintf("✅ Use this endpoint: %s\n", bestURL))
|
|
||||||
} else {
|
|
||||||
b.WriteString("❌ No working endpoints found. Check:\n")
|
|
||||||
b.WriteString(" 1. Is the node running?\n")
|
|
||||||
b.WriteString(" 2. Is the chain bootstrapped?\n")
|
|
||||||
b.WriteString(" 3. Are handlers properly registered?\n")
|
|
||||||
b.WriteString(" 4. Check node logs for handler registration errors\n")
|
|
||||||
b.WriteString(" 5. Try restarting the node\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// QuickDiagnose performs a quick endpoint check and prints results.
|
|
||||||
// Convenience function for CLI tools.
|
|
||||||
func QuickDiagnose(nodeURL string, chainID ids.ID, alias string) {
|
|
||||||
logger := log.NewNoOpLogger()
|
|
||||||
tool := NewDebugTool(nodeURL, logger)
|
|
||||||
report := tool.DiagnoseEndpoint(chainID, alias)
|
|
||||||
fmt.Println(report.String())
|
|
||||||
}
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
// Package rpc provides robust RPC handler registration with retries, health checks, and clear debugging.
|
|
||||||
// Follows Go principles: fail fast with clear errors, single responsibility, minimal dependencies.
|
|
||||||
package rpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/luxfi/ids"
|
|
||||||
"github.com/luxfi/log"
|
|
||||||
"github.com/luxfi/node/server/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Errors follow Go convention: lowercase, descriptive, actionable
|
|
||||||
errNilHandler = errors.New("handler is nil")
|
|
||||||
errNilServer = errors.New("server is nil")
|
|
||||||
errEmptyEndpoint = errors.New("endpoint is empty")
|
|
||||||
errRegistrationFailed = errors.New("handler registration failed")
|
|
||||||
errHealthCheckFailed = errors.New("health check failed")
|
|
||||||
)
|
|
||||||
|
|
||||||
// HandlerManager manages RPC handler registration with robust error handling and health checks.
|
|
||||||
// Single responsibility: reliable handler registration with observability.
|
|
||||||
type HandlerManager struct {
|
|
||||||
server server.Server
|
|
||||||
log log.Logger
|
|
||||||
mu sync.RWMutex
|
|
||||||
routes map[string]*RouteInfo // chainID -> route info
|
|
||||||
retries int // max registration retries
|
|
||||||
retryWait time.Duration // initial retry wait time
|
|
||||||
}
|
|
||||||
|
|
||||||
// RouteInfo contains complete information about a registered route.
|
|
||||||
// Everything needed for debugging in one place.
|
|
||||||
type RouteInfo struct {
|
|
||||||
ChainID ids.ID
|
|
||||||
ChainAlias string
|
|
||||||
Base string // e.g., "bc/C" or "bc/<chainID>"
|
|
||||||
Endpoints []string // e.g., ["/rpc", "/ws"]
|
|
||||||
Handler http.Handler
|
|
||||||
Healthy bool
|
|
||||||
LastCheck time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHandlerManager creates a handler manager with sensible defaults.
|
|
||||||
// Simple factory, no magic.
|
|
||||||
func NewHandlerManager(server server.Server, logger log.Logger) *HandlerManager {
|
|
||||||
return &HandlerManager{
|
|
||||||
server: server,
|
|
||||||
log: logger,
|
|
||||||
routes: make(map[string]*RouteInfo),
|
|
||||||
retries: 3,
|
|
||||||
retryWait: 100 * time.Millisecond,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterChainHandlers registers all handlers for a chain with retry logic and health checks.
|
|
||||||
// This is the main entry point - handles everything needed for robust registration.
|
|
||||||
func (m *HandlerManager) RegisterChainHandlers(
|
|
||||||
ctx context.Context,
|
|
||||||
chainID ids.ID,
|
|
||||||
chainAlias string,
|
|
||||||
handlers map[string]http.Handler,
|
|
||||||
) error {
|
|
||||||
if m.server == nil {
|
|
||||||
return errNilServer
|
|
||||||
}
|
|
||||||
|
|
||||||
m.log.Info("Starting chain handler registration",
|
|
||||||
log.Stringer("chainID", chainID),
|
|
||||||
log.String("alias", chainAlias),
|
|
||||||
log.Int("handlerCount", len(handlers)))
|
|
||||||
|
|
||||||
// Validate handlers first - fail fast
|
|
||||||
if err := m.validateHandlers(handlers); err != nil {
|
|
||||||
return fmt.Errorf("handler validation failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build route info
|
|
||||||
info := &RouteInfo{
|
|
||||||
ChainID: chainID,
|
|
||||||
ChainAlias: chainAlias,
|
|
||||||
Endpoints: make([]string, 0, len(handlers)),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine base paths
|
|
||||||
bases := m.getBasePaths(chainID, chainAlias)
|
|
||||||
|
|
||||||
// Register each handler with retries
|
|
||||||
var registrationErrors []error
|
|
||||||
for endpoint, handler := range handlers {
|
|
||||||
info.Endpoints = append(info.Endpoints, endpoint)
|
|
||||||
|
|
||||||
for _, base := range bases {
|
|
||||||
if err := m.registerWithRetry(ctx, base, endpoint, handler); err != nil {
|
|
||||||
registrationErrors = append(registrationErrors,
|
|
||||||
fmt.Errorf("failed to register %s%s: %w", base, endpoint, err))
|
|
||||||
m.log.Error("Handler registration failed",
|
|
||||||
log.String("base", base),
|
|
||||||
log.String("endpoint", endpoint),
|
|
||||||
log.Err(err))
|
|
||||||
} else {
|
|
||||||
m.log.Info("Handler registered successfully",
|
|
||||||
log.String("route", fmt.Sprintf("/v1/%s%s", base, endpoint)),
|
|
||||||
log.Stringer("chainID", chainID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store route info for monitoring
|
|
||||||
m.mu.Lock()
|
|
||||||
info.Base = bases[0] // Primary base
|
|
||||||
info.Handler = handlers["/rpc"] // Store primary handler for health checks
|
|
||||||
m.routes[chainID.String()] = info
|
|
||||||
m.mu.Unlock()
|
|
||||||
|
|
||||||
// Run health checks
|
|
||||||
if err := m.healthCheckRoute(info); err != nil {
|
|
||||||
m.log.Warn("Health check failed for newly registered chain",
|
|
||||||
log.Stringer("chainID", chainID),
|
|
||||||
log.Err(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return aggregate error if any registrations failed
|
|
||||||
if len(registrationErrors) > 0 {
|
|
||||||
return fmt.Errorf("%w: %v", errRegistrationFailed, registrationErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.log.Info("Chain handler registration completed",
|
|
||||||
log.Stringer("chainID", chainID),
|
|
||||||
log.String("routes", strings.Join(m.getFullRoutes(bases, info.Endpoints), ", ")))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateHandlers ensures all handlers are valid before attempting registration.
|
|
||||||
// Fail fast with clear errors - no silent failures.
|
|
||||||
func (m *HandlerManager) validateHandlers(handlers map[string]http.Handler) error {
|
|
||||||
if len(handlers) == 0 {
|
|
||||||
return errors.New("no handlers provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
for endpoint, handler := range handlers {
|
|
||||||
if handler == nil {
|
|
||||||
return fmt.Errorf("%w for endpoint %s", errNilHandler, endpoint)
|
|
||||||
}
|
|
||||||
if endpoint == "" {
|
|
||||||
return errEmptyEndpoint
|
|
||||||
}
|
|
||||||
// Ensure endpoint starts with /
|
|
||||||
if !strings.HasPrefix(endpoint, "/") {
|
|
||||||
return fmt.Errorf("endpoint %s must start with /", endpoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getBasePaths returns all base paths for a chain (with and without alias).
|
|
||||||
// Single source of truth for path construction.
|
|
||||||
func (m *HandlerManager) getBasePaths(chainID ids.ID, chainAlias string) []string {
|
|
||||||
bases := []string{}
|
|
||||||
|
|
||||||
// If we have an alias (like "C" for C-Chain), use it as primary
|
|
||||||
if chainAlias != "" && chainAlias != chainID.String() {
|
|
||||||
bases = append(bases, fmt.Sprintf("bc/%s", chainAlias))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always include the full chain ID path
|
|
||||||
bases = append(bases, fmt.Sprintf("bc/%s", chainID.String()))
|
|
||||||
|
|
||||||
return bases
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFullRoutes constructs full route paths for logging.
|
|
||||||
// Clear, complete information for operators.
|
|
||||||
func (m *HandlerManager) getFullRoutes(bases []string, endpoints []string) []string {
|
|
||||||
routes := []string{}
|
|
||||||
for _, base := range bases {
|
|
||||||
for _, endpoint := range endpoints {
|
|
||||||
routes = append(routes, fmt.Sprintf("/v1/%s%s", base, endpoint))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return routes
|
|
||||||
}
|
|
||||||
|
|
||||||
// registerWithRetry attempts registration with exponential backoff.
|
|
||||||
// Handles transient failures gracefully.
|
|
||||||
func (m *HandlerManager) registerWithRetry(
|
|
||||||
ctx context.Context,
|
|
||||||
base string,
|
|
||||||
endpoint string,
|
|
||||||
handler http.Handler,
|
|
||||||
) error {
|
|
||||||
wait := m.retryWait
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for attempt := 0; attempt < m.retries; attempt++ {
|
|
||||||
// Check context cancellation
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try registration
|
|
||||||
if err := m.server.AddRoute(handler, base, endpoint); err == nil {
|
|
||||||
return nil // Success!
|
|
||||||
} else {
|
|
||||||
lastErr = err
|
|
||||||
m.log.Debug("Registration attempt failed, retrying",
|
|
||||||
log.Int("attempt", attempt+1),
|
|
||||||
log.String("base", base),
|
|
||||||
log.String("endpoint", endpoint),
|
|
||||||
log.Err(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't wait after last attempt
|
|
||||||
if attempt < m.retries-1 {
|
|
||||||
select {
|
|
||||||
case <-time.After(wait):
|
|
||||||
wait *= 2 // Exponential backoff
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("failed after %d attempts: %w", m.retries, lastErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// healthCheckRoute performs a basic health check on a registered route.
|
|
||||||
// Validates that handlers are actually responding.
|
|
||||||
func (m *HandlerManager) healthCheckRoute(info *RouteInfo) error {
|
|
||||||
if info.Handler == nil {
|
|
||||||
return fmt.Errorf("no handler to check for chain %s", info.ChainID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a test request
|
|
||||||
req := httptest.NewRequest("POST", "/", strings.NewReader(`{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}`))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
// Record the response
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Call the handler
|
|
||||||
info.Handler.ServeHTTP(recorder, req)
|
|
||||||
|
|
||||||
// Check response
|
|
||||||
info.LastCheck = time.Now()
|
|
||||||
if recorder.Code == http.StatusOK || recorder.Code == http.StatusMethodNotAllowed {
|
|
||||||
info.Healthy = true
|
|
||||||
m.log.Debug("Health check passed",
|
|
||||||
log.Stringer("chainID", info.ChainID),
|
|
||||||
log.Int("status", recorder.Code))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
info.Healthy = false
|
|
||||||
return fmt.Errorf("%w: status %d", errHealthCheckFailed, recorder.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRouteInfo returns information about a registered chain's routes.
|
|
||||||
// Useful for debugging and monitoring.
|
|
||||||
func (m *HandlerManager) GetRouteInfo(chainID ids.ID) (*RouteInfo, bool) {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
info, exists := m.routes[chainID.String()]
|
|
||||||
return info, exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllRoutes returns all registered route information.
|
|
||||||
// Complete visibility for operators.
|
|
||||||
func (m *HandlerManager) GetAllRoutes() map[string]*RouteInfo {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
// Return a copy to prevent external modification
|
|
||||||
routes := make(map[string]*RouteInfo, len(m.routes))
|
|
||||||
for k, v := range m.routes {
|
|
||||||
routes[k] = v
|
|
||||||
}
|
|
||||||
return routes
|
|
||||||
}
|
|
||||||
|
|
||||||
// HealthCheckAll performs health checks on all registered routes.
|
|
||||||
// Batch operation for monitoring systems.
|
|
||||||
func (m *HandlerManager) HealthCheckAll() map[string]bool {
|
|
||||||
m.mu.RLock()
|
|
||||||
routes := make([]*RouteInfo, 0, len(m.routes))
|
|
||||||
for _, info := range m.routes {
|
|
||||||
routes = append(routes, info)
|
|
||||||
}
|
|
||||||
m.mu.RUnlock()
|
|
||||||
|
|
||||||
results := make(map[string]bool)
|
|
||||||
for _, info := range routes {
|
|
||||||
err := m.healthCheckRoute(info)
|
|
||||||
results[info.ChainID.String()] = err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRetryConfig allows customization of retry behavior.
|
|
||||||
// Flexibility for different deployment scenarios.
|
|
||||||
func (m *HandlerManager) SetRetryConfig(maxRetries int, initialWait time.Duration) {
|
|
||||||
if maxRetries > 0 {
|
|
||||||
m.retries = maxRetries
|
|
||||||
}
|
|
||||||
if initialWait > 0 {
|
|
||||||
m.retryWait = initialWait
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
package rpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/luxfi/ids"
|
|
||||||
"github.com/luxfi/log"
|
|
||||||
"github.com/luxfi/node/server/http"
|
|
||||||
"github.com/luxfi/runtime"
|
|
||||||
"github.com/luxfi/vm"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// mockServer implements a test server for handler registration
|
|
||||||
type mockServer struct {
|
|
||||||
routes map[string]http.Handler
|
|
||||||
failCount int
|
|
||||||
maxFailures int
|
|
||||||
returnError error
|
|
||||||
aliases map[string][]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMockServer() *mockServer {
|
|
||||||
return &mockServer{
|
|
||||||
routes: make(map[string]http.Handler),
|
|
||||||
maxFailures: 0,
|
|
||||||
aliases: make(map[string][]string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *mockServer) AddRoute(handler http.Handler, base, endpoint string) error {
|
|
||||||
// Simulate transient failures for retry testing
|
|
||||||
if s.failCount < s.maxFailures {
|
|
||||||
s.failCount++
|
|
||||||
return errors.New("transient failure")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return configured error if any
|
|
||||||
if s.returnError != nil {
|
|
||||||
return s.returnError
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the route
|
|
||||||
key := base + endpoint
|
|
||||||
s.routes[key] = handler
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *mockServer) AddAliases(endpoint string, aliases ...string) error {
|
|
||||||
s.aliases[endpoint] = aliases
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *mockServer) AddRouteWithReadLock(handler http.Handler, base, endpoint string) error {
|
|
||||||
return s.AddRoute(handler, base, endpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *mockServer) AddAliasesWithReadLock(endpoint string, aliases ...string) error {
|
|
||||||
return s.AddAliases(endpoint, aliases...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *mockServer) Dispatch() error { return nil }
|
|
||||||
func (s *mockServer) RegisterChain(chainName string, rt *runtime.Runtime, vm vm.VM) {
|
|
||||||
}
|
|
||||||
func (s *mockServer) Shutdown() error { return nil }
|
|
||||||
func (s *mockServer) SetRootInfoProvider(_ server.RootInfoProvider) {}
|
|
||||||
|
|
||||||
func TestHandlerManager_RegisterChainHandlers(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
chainID ids.ID
|
|
||||||
chainAlias string
|
|
||||||
handlers map[string]http.Handler
|
|
||||||
serverError error
|
|
||||||
expectError bool
|
|
||||||
expectRoutes int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "successful registration with alias",
|
|
||||||
chainID: ids.GenerateTestID(),
|
|
||||||
chainAlias: "C",
|
|
||||||
handlers: map[string]http.Handler{
|
|
||||||
"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}),
|
|
||||||
"/ws": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectRoutes: 4, // 2 endpoints × 2 bases (alias + ID)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "successful registration without alias",
|
|
||||||
chainID: ids.GenerateTestID(),
|
|
||||||
handlers: map[string]http.Handler{
|
|
||||||
"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectRoutes: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil handler validation",
|
|
||||||
chainID: ids.GenerateTestID(),
|
|
||||||
chainAlias: "X",
|
|
||||||
handlers: map[string]http.Handler{"/rpc": nil},
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty endpoint validation",
|
|
||||||
chainID: ids.GenerateTestID(),
|
|
||||||
handlers: map[string]http.Handler{"": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})},
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no handlers provided",
|
|
||||||
chainID: ids.GenerateTestID(),
|
|
||||||
handlers: map[string]http.Handler{},
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid endpoint format",
|
|
||||||
chainID: ids.GenerateTestID(),
|
|
||||||
handlers: map[string]http.Handler{"rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})},
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Setup
|
|
||||||
server := newMockServer()
|
|
||||||
server.returnError = tt.serverError
|
|
||||||
logger := log.NewNoOpLogger()
|
|
||||||
manager := NewHandlerManager(server, logger)
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
ctx := context.Background()
|
|
||||||
err := manager.RegisterChainHandlers(ctx, tt.chainID, tt.chainAlias, tt.handlers)
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
if tt.expectError {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, server.routes, tt.expectRoutes)
|
|
||||||
|
|
||||||
// Verify route info was stored
|
|
||||||
info, exists := manager.GetRouteInfo(tt.chainID)
|
|
||||||
require.True(t, exists)
|
|
||||||
require.Equal(t, tt.chainID, info.ChainID)
|
|
||||||
require.Equal(t, tt.chainAlias, info.ChainAlias)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandlerManager_RetryLogic(t *testing.T) {
|
|
||||||
// Setup server that fails twice then succeeds
|
|
||||||
server := newMockServer()
|
|
||||||
server.maxFailures = 2
|
|
||||||
|
|
||||||
logger := log.NewNoOpLogger()
|
|
||||||
manager := NewHandlerManager(server, logger)
|
|
||||||
manager.SetRetryConfig(3, 10*time.Millisecond)
|
|
||||||
|
|
||||||
// Create test handler
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Register with retries
|
|
||||||
ctx := context.Background()
|
|
||||||
chainID := ids.GenerateTestID()
|
|
||||||
require.NoError(t, manager.RegisterChainHandlers(ctx, chainID, "TEST", map[string]http.Handler{
|
|
||||||
"/rpc": handler,
|
|
||||||
}))
|
|
||||||
require.Equal(t, 2, server.failCount) // Failed twice, succeeded on third try
|
|
||||||
require.Len(t, server.routes, 2) // Both alias and ID routes
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandlerManager_HealthCheck(t *testing.T) {
|
|
||||||
server := newMockServer()
|
|
||||||
logger := log.NewNoOpLogger()
|
|
||||||
manager := NewHandlerManager(server, logger)
|
|
||||||
|
|
||||||
// Register a healthy handler
|
|
||||||
healthyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(`{"jsonrpc":"2.0","result":"test","id":1}`))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Register an unhealthy handler
|
|
||||||
unhealthyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
chainID1 := ids.GenerateTestID()
|
|
||||||
chainID2 := ids.GenerateTestID()
|
|
||||||
|
|
||||||
// Register healthy chain
|
|
||||||
require.NoError(t, manager.RegisterChainHandlers(ctx, chainID1, "", map[string]http.Handler{
|
|
||||||
"/rpc": healthyHandler,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Register unhealthy chain
|
|
||||||
// Registration succeeds even if health check fails
|
|
||||||
require.NoError(t, manager.RegisterChainHandlers(ctx, chainID2, "", map[string]http.Handler{
|
|
||||||
"/rpc": unhealthyHandler,
|
|
||||||
})) // Registration should succeed regardless of handler health
|
|
||||||
|
|
||||||
// Check health status
|
|
||||||
results := manager.HealthCheckAll()
|
|
||||||
require.True(t, results[chainID1.String()])
|
|
||||||
require.False(t, results[chainID2.String()])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandlerManager_GetBasePaths(t *testing.T) {
|
|
||||||
manager := &HandlerManager{}
|
|
||||||
chainID := ids.GenerateTestID()
|
|
||||||
|
|
||||||
// Test with alias
|
|
||||||
bases := manager.getBasePaths(chainID, "C")
|
|
||||||
require.Equal(t, []string{"bc/C", "bc/" + chainID.String()}, bases)
|
|
||||||
|
|
||||||
// Test without alias
|
|
||||||
bases = manager.getBasePaths(chainID, "")
|
|
||||||
require.Equal(t, []string{"bc/" + chainID.String()}, bases)
|
|
||||||
|
|
||||||
// Test when alias equals chain ID (shouldn't duplicate)
|
|
||||||
bases = manager.getBasePaths(chainID, chainID.String())
|
|
||||||
require.Equal(t, []string{"bc/" + chainID.String()}, bases)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandlerManager_ContextCancellation(t *testing.T) {
|
|
||||||
// Create a server that delays to test cancellation
|
|
||||||
server := &mockServer{
|
|
||||||
routes: make(map[string]http.Handler),
|
|
||||||
returnError: errors.New("slow server"),
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := log.NewNoOpLogger()
|
|
||||||
manager := NewHandlerManager(server, logger)
|
|
||||||
manager.SetRetryConfig(10, 100*time.Millisecond) // Many retries with delays
|
|
||||||
|
|
||||||
// Create cancelled context
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
cancel() // Cancel immediately
|
|
||||||
|
|
||||||
// Try to register - should fail with context error
|
|
||||||
chainID := ids.GenerateTestID()
|
|
||||||
err := manager.RegisterChainHandlers(ctx, chainID, "", map[string]http.Handler{
|
|
||||||
"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
|
|
||||||
})
|
|
||||||
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), "context canceled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark to ensure performance doesn't degrade
|
|
||||||
func BenchmarkHandlerRegistration(b *testing.B) {
|
|
||||||
server := newMockServer()
|
|
||||||
logger := log.NewNoOpLogger()
|
|
||||||
manager := NewHandlerManager(server, logger)
|
|
||||||
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
chainID := ids.GenerateTestID()
|
|
||||||
manager.RegisterChainHandlers(ctx, chainID, "TEST", map[string]http.Handler{
|
|
||||||
"/rpc": handler,
|
|
||||||
"/ws": handler,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
package rpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/luxfi/ids"
|
|
||||||
"github.com/luxfi/log"
|
|
||||||
"github.com/luxfi/node/server/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IntegrationExample shows how to modify the existing createChain function in manager.go.
|
|
||||||
// This replaces lines 941-990 with cleaner, more robust code.
|
|
||||||
func IntegrationExample(
|
|
||||||
ctx context.Context,
|
|
||||||
chainID ids.ID,
|
|
||||||
vm interface{},
|
|
||||||
server server.Server,
|
|
||||||
logger log.Logger,
|
|
||||||
cChainID ids.ID,
|
|
||||||
pChainID ids.ID,
|
|
||||||
isDevMode bool,
|
|
||||||
) error {
|
|
||||||
// BEFORE: 50+ lines of complex type checking and error-prone registration
|
|
||||||
// AFTER: Clean, robust registration with proper error handling
|
|
||||||
|
|
||||||
// Step 1: Create the registrar
|
|
||||||
registrar := NewChainHandlerRegistrar(server, logger, cChainID, pChainID)
|
|
||||||
|
|
||||||
// Step 2: Configure based on environment
|
|
||||||
if isDevMode {
|
|
||||||
// Development: Fast failures for quick iteration
|
|
||||||
registrar.SetRetryConfig(2, 50*time.Millisecond)
|
|
||||||
logger.Info("Using development handler registration settings")
|
|
||||||
} else {
|
|
||||||
// Production: More robust with retries
|
|
||||||
registrar.SetRetryConfig(5, 200*time.Millisecond)
|
|
||||||
logger.Info("Using production handler registration settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Register handlers (replaces all the complex VM type checking)
|
|
||||||
startTime := time.Now()
|
|
||||||
err := registrar.RegisterChainHandlers(ctx, chainID, vm)
|
|
||||||
duration := time.Since(startTime)
|
|
||||||
|
|
||||||
// Step 4: Handle registration result
|
|
||||||
if err != nil {
|
|
||||||
// Log error but don't fail chain creation
|
|
||||||
// Handlers are not critical for chain operation
|
|
||||||
logger.Error("RPC handler registration failed",
|
|
||||||
log.Stringer("chainID", chainID),
|
|
||||||
log.Err(err),
|
|
||||||
log.Duration("duration", duration),
|
|
||||||
log.String("action", "Chain will operate without HTTP/RPC access"))
|
|
||||||
|
|
||||||
// Could emit metrics here if available
|
|
||||||
// metric.HandlerRegistrationFailed.Inc()
|
|
||||||
|
|
||||||
// Non-fatal: return nil to allow chain to continue
|
|
||||||
// Change to 'return err' if you want this to be fatal
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: Log success with useful information
|
|
||||||
if info, exists := registrar.GetRouteInfo(chainID); exists {
|
|
||||||
logger.Info("RPC handlers registered successfully",
|
|
||||||
log.Stringer("chainID", chainID),
|
|
||||||
log.String("alias", info.ChainAlias),
|
|
||||||
log.Strings("endpoints", info.Endpoints),
|
|
||||||
log.Duration("duration", duration),
|
|
||||||
log.Bool("healthCheckPassed", info.Healthy))
|
|
||||||
|
|
||||||
// Print developer-friendly message
|
|
||||||
if isDevMode && len(info.Endpoints) > 0 {
|
|
||||||
baseURL := "http://localhost:9630"
|
|
||||||
fmt.Printf("\n✅ Chain %s RPC endpoints ready:\n", chainID)
|
|
||||||
for _, endpoint := range info.Endpoints {
|
|
||||||
if info.ChainAlias != "" {
|
|
||||||
fmt.Printf(" %s/v1/bc/%s%s\n", baseURL, info.ChainAlias, endpoint)
|
|
||||||
}
|
|
||||||
fmt.Printf(" %s/v1/bc/%s%s\n", baseURL, chainID, endpoint)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 6: Schedule async health monitoring (optional)
|
|
||||||
if !isDevMode {
|
|
||||||
go monitorHandlerHealth(ctx, registrar, chainID, logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// monitorHandlerHealth runs periodic health checks in the background.
|
|
||||||
// This helps detect and log handler issues early.
|
|
||||||
func monitorHandlerHealth(
|
|
||||||
ctx context.Context,
|
|
||||||
registrar *ChainHandlerRegistrar,
|
|
||||||
chainID ids.ID,
|
|
||||||
logger log.Logger,
|
|
||||||
) {
|
|
||||||
// Initial delay to let chain fully initialize
|
|
||||||
select {
|
|
||||||
case <-time.After(10 * time.Second):
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run initial health check
|
|
||||||
checkHealth(registrar, chainID, logger)
|
|
||||||
|
|
||||||
// Periodic health checks
|
|
||||||
ticker := time.NewTicker(5 * time.Minute)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
checkHealth(registrar, chainID, logger)
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkHealth performs a health check and logs results.
|
|
||||||
func checkHealth(registrar *ChainHandlerRegistrar, chainID ids.ID, logger log.Logger) {
|
|
||||||
results := registrar.HealthCheckAll()
|
|
||||||
|
|
||||||
for chainIDStr, healthy := range results {
|
|
||||||
if chainIDStr == chainID.String() {
|
|
||||||
if healthy {
|
|
||||||
logger.Debug("Handler health check passed",
|
|
||||||
log.String("chainID", chainIDStr))
|
|
||||||
} else {
|
|
||||||
logger.Warn("Handler health check failed",
|
|
||||||
log.String("chainID", chainIDStr),
|
|
||||||
log.String("action", "Will continue monitoring"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MinimalIntegration shows the absolute minimum code needed.
|
|
||||||
// This is what you'd actually put in manager.go.
|
|
||||||
func MinimalIntegration(
|
|
||||||
ctx context.Context,
|
|
||||||
chainID ids.ID,
|
|
||||||
vm interface{},
|
|
||||||
server server.Server,
|
|
||||||
logger log.Logger,
|
|
||||||
cChainID ids.ID,
|
|
||||||
) error {
|
|
||||||
// Just three lines to replace 50+ lines of complex code!
|
|
||||||
registrar := NewChainHandlerRegistrar(server, logger, cChainID, ids.Empty)
|
|
||||||
if err := registrar.RegisterChainHandlers(ctx, chainID, vm); err != nil {
|
|
||||||
logger.Error("Handler registration failed", log.Err(err))
|
|
||||||
}
|
|
||||||
return nil // Non-fatal
|
|
||||||
}
|
|
||||||
-265
@@ -1,265 +0,0 @@
|
|||||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
package node
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/luxfi/crypto/bls"
|
|
||||||
"github.com/luxfi/genesis/pkg/genesis"
|
|
||||||
"github.com/luxfi/ids"
|
|
||||||
"github.com/luxfi/log"
|
|
||||||
"github.com/luxfi/math/set"
|
|
||||||
"github.com/luxfi/node/server/http"
|
|
||||||
"github.com/luxfi/node/benchlist"
|
|
||||||
"github.com/luxfi/node/chains"
|
|
||||||
"github.com/luxfi/node/nets"
|
|
||||||
"github.com/luxfi/node/network"
|
|
||||||
"github.com/luxfi/node/trace"
|
|
||||||
"github.com/luxfi/node/upgrade"
|
|
||||||
"github.com/luxfi/node/vms/platformvm/txs/fee"
|
|
||||||
"github.com/luxfi/timer"
|
|
||||||
"github.com/luxfi/node/utils/profiler"
|
|
||||||
)
|
|
||||||
|
|
||||||
type APIIndexerConfig struct {
|
|
||||||
IndexAPIEnabled bool `json:"indexAPIEnabled"`
|
|
||||||
IndexAllowIncomplete bool `json:"indexAllowIncomplete"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TargeterConfig struct {
|
|
||||||
// VdrAlloc is the percentage of resource usage that is attributed to validators
|
|
||||||
// The range is [0, 1], defaults to 1 (100%)
|
|
||||||
VdrAlloc float64 `json:"vdrAlloc"`
|
|
||||||
// MaxNonVdrUsage is the maximum amount of resources that non-validators can use
|
|
||||||
// The range is [0, 1], defaults to 0
|
|
||||||
MaxNonVdrUsage float64 `json:"maxNonVdrUsage"`
|
|
||||||
// MaxNonVdrNodeUsage is the maximum amount of resources that a non-validator node can use
|
|
||||||
// The range is [0, 1], defaults to 0
|
|
||||||
MaxNonVdrNodeUsage float64 `json:"maxNonVdrNodeUsage"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPConfig struct {
|
|
||||||
server.HTTPConfig
|
|
||||||
APIConfig `json:"apiConfig"`
|
|
||||||
HTTPHost string `json:"httpHost"`
|
|
||||||
HTTPPort uint16 `json:"httpPort"`
|
|
||||||
|
|
||||||
HTTPSEnabled bool `json:"httpsEnabled"`
|
|
||||||
HTTPSKey []byte `json:"-"`
|
|
||||||
HTTPSCert []byte `json:"-"`
|
|
||||||
|
|
||||||
HTTPAllowedOrigins []string `json:"httpAllowedOrigins"`
|
|
||||||
HTTPAllowedHosts []string `json:"httpAllowedHosts"`
|
|
||||||
|
|
||||||
ShutdownTimeout time.Duration `json:"shutdownTimeout"`
|
|
||||||
ShutdownWait time.Duration `json:"shutdownWait"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type APIConfig struct {
|
|
||||||
APIIndexerConfig `json:"indexerConfig"`
|
|
||||||
|
|
||||||
// Enable/Disable APIs
|
|
||||||
AdminAPIEnabled bool `json:"adminAPIEnabled"`
|
|
||||||
InfoAPIEnabled bool `json:"infoAPIEnabled"`
|
|
||||||
KeystoreAPIEnabled bool `json:"keystoreAPIEnabled"`
|
|
||||||
MetricsAPIEnabled bool `json:"metricsAPIEnabled"`
|
|
||||||
HealthAPIEnabled bool `json:"healthAPIEnabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IPConfig struct {
|
|
||||||
PublicIP string `json:"publicIP"`
|
|
||||||
PublicIPResolutionService string `json:"publicIPResolutionService"`
|
|
||||||
PublicIPResolutionFreq time.Duration `json:"publicIPResolutionFreq"`
|
|
||||||
// The host portion of the address to listen on. The port to
|
|
||||||
// listen on will be sourced from IPPort.
|
|
||||||
//
|
|
||||||
// - If empty, listen on all interfaces (both ipv4 and ipv6).
|
|
||||||
// - If populated, listen only on the specified address.
|
|
||||||
ListenHost string `json:"listenHost"`
|
|
||||||
ListenPort uint16 `json:"listenPort"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StakingConfig struct {
|
|
||||||
genesis.StakingConfig
|
|
||||||
SybilProtectionEnabled bool `json:"sybilProtectionEnabled"`
|
|
||||||
PartialSyncPrimaryNetwork bool `json:"partialSyncPrimaryNetwork"`
|
|
||||||
StakingTLSCert tls.Certificate `json:"-"`
|
|
||||||
StakingSigningKey *bls.SecretKey `json:"-"`
|
|
||||||
SybilProtectionDisabledWeight uint64 `json:"sybilProtectionDisabledWeight"`
|
|
||||||
StakingKeyPath string `json:"stakingKeyPath"`
|
|
||||||
StakingCertPath string `json:"stakingCertPath"`
|
|
||||||
StakingSignerPath string `json:"stakingSignerPath"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StateSyncConfig struct {
|
|
||||||
StateSyncIDs []ids.NodeID `json:"stateSyncIDs"`
|
|
||||||
StateSyncIPs []netip.AddrPort `json:"stateSyncIPs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BootstrapConfig struct {
|
|
||||||
// Timeout before emitting a warn log when connecting to bootstrapping beacons
|
|
||||||
BootstrapBeaconConnectionTimeout time.Duration `json:"bootstrapBeaconConnectionTimeout"`
|
|
||||||
|
|
||||||
// Max number of containers in an ancestors message sent by this node.
|
|
||||||
BootstrapAncestorsMaxContainersSent int `json:"bootstrapAncestorsMaxContainersSent"`
|
|
||||||
|
|
||||||
// This node will only consider the first [AncestorsMaxContainersReceived]
|
|
||||||
// containers in an ancestors message it receives.
|
|
||||||
BootstrapAncestorsMaxContainersReceived int `json:"bootstrapAncestorsMaxContainersReceived"`
|
|
||||||
|
|
||||||
// Max time to spend fetching a container and its
|
|
||||||
// ancestors while responding to a GetAncestors message
|
|
||||||
BootstrapMaxTimeGetAncestors time.Duration `json:"bootstrapMaxTimeGetAncestors"`
|
|
||||||
|
|
||||||
Bootstrappers []genesis.Bootstrapper `json:"bootstrappers"`
|
|
||||||
|
|
||||||
// Skip bootstrapping and start processing immediately
|
|
||||||
SkipBootstrap bool `json:"skipBootstrap"`
|
|
||||||
|
|
||||||
// Enable automining in POA mode
|
|
||||||
EnableAutomining bool `json:"enableAutomining"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DatabaseConfig struct {
|
|
||||||
// If true, all writes are to memory and are discarded at node shutdown.
|
|
||||||
ReadOnly bool `json:"readOnly"`
|
|
||||||
|
|
||||||
// Path to database
|
|
||||||
Path string `json:"path"`
|
|
||||||
|
|
||||||
// Name of the database type to use
|
|
||||||
Name string `json:"name"`
|
|
||||||
|
|
||||||
// Path to config file
|
|
||||||
Config []byte `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config contains all of the configurations of a Lux node.
|
|
||||||
type Config struct {
|
|
||||||
HTTPConfig `json:"httpConfig"`
|
|
||||||
IPConfig `json:"ipConfig"`
|
|
||||||
StakingConfig `json:"stakingConfig"`
|
|
||||||
fee.StaticConfig `json:"txFeeConfig"`
|
|
||||||
StateSyncConfig `json:"stateSyncConfig"`
|
|
||||||
BootstrapConfig `json:"bootstrapConfig"`
|
|
||||||
DatabaseConfig `json:"databaseConfig"`
|
|
||||||
|
|
||||||
UpgradeConfig upgrade.Config `json:"upgradeConfig"`
|
|
||||||
|
|
||||||
// Genesis information
|
|
||||||
GenesisBytes []byte `json:"-"`
|
|
||||||
UTXOAssetID ids.ID `json:"utxoAssetID"`
|
|
||||||
|
|
||||||
// ID of the network this node should connect to
|
|
||||||
NetworkID uint32 `json:"networkID"`
|
|
||||||
|
|
||||||
// Health
|
|
||||||
HealthCheckFreq time.Duration `json:"healthCheckFreq"`
|
|
||||||
|
|
||||||
// Network configuration
|
|
||||||
NetworkConfig network.Config `json:"networkConfig"`
|
|
||||||
|
|
||||||
AdaptiveTimeoutConfig timer.AdaptiveTimeoutConfig `json:"adaptiveTimeoutConfig"`
|
|
||||||
|
|
||||||
BenchlistConfig benchlist.Config `json:"benchlistConfig"`
|
|
||||||
|
|
||||||
ProfilerConfig profiler.Config `json:"profilerConfig"`
|
|
||||||
|
|
||||||
// LoggingConfig log.Config `json:"loggingConfig"` // log.Config doesn't exist
|
|
||||||
|
|
||||||
PluginDir string `json:"pluginDir"`
|
|
||||||
// DevMode enables local PoA + auto-mine mode (network-id=local, sybil-protection disabled)
|
|
||||||
DevMode bool `json:"devMode"`
|
|
||||||
|
|
||||||
// File Descriptor Limit
|
|
||||||
FdLimit uint64 `json:"fdLimit"`
|
|
||||||
|
|
||||||
// Metrics
|
|
||||||
MeterVMEnabled bool `json:"meterVMEnabled"`
|
|
||||||
// Delay between automatic block proposals when DevMode is set
|
|
||||||
DevBlockDelay time.Duration `json:"devBlockDelay"`
|
|
||||||
|
|
||||||
RouterHealthConfig HealthConfig `json:"routerHealthConfig"`
|
|
||||||
ConsensusShutdownTimeout time.Duration `json:"consensusShutdownTimeout"`
|
|
||||||
// Poll for new frontiers every [FrontierPollFrequency]
|
|
||||||
FrontierPollFrequency time.Duration `json:"consensusGossipFreq"`
|
|
||||||
// ConsensusAppConcurrency defines the maximum number of goroutines to
|
|
||||||
// handle App messages per chain.
|
|
||||||
ConsensusAppConcurrency int `json:"consensusAppConcurrency"`
|
|
||||||
|
|
||||||
TrackedChains set.Set[ids.ID] `json:"trackedChains"`
|
|
||||||
TrackAllChains bool `json:"trackAllChains"`
|
|
||||||
|
|
||||||
NetConfigs map[ids.ID]nets.Config `json:"chainConfigs"`
|
|
||||||
|
|
||||||
ChainConfigs map[string]chains.ChainConfig `json:"-"`
|
|
||||||
ChainAliases map[ids.ID][]string `json:"chainAliases"`
|
|
||||||
|
|
||||||
VMAliases map[ids.ID][]string `json:"vmAliases"`
|
|
||||||
|
|
||||||
// Halflife to use for the processing requests tracker.
|
|
||||||
// Larger halflife --> usage metrics change more slowly.
|
|
||||||
SystemTrackerProcessingHalflife time.Duration `json:"systemTrackerProcessingHalflife"`
|
|
||||||
|
|
||||||
// Frequency to check the real resource usage of tracked processes.
|
|
||||||
// More frequent checks --> usage metrics are more accurate, but more
|
|
||||||
// expensive to track
|
|
||||||
SystemTrackerFrequency time.Duration `json:"systemTrackerFrequency"`
|
|
||||||
|
|
||||||
// Halflife to use for the cpu tracker.
|
|
||||||
// Larger halflife --> cpu usage metrics change more slowly.
|
|
||||||
SystemTrackerCPUHalflife time.Duration `json:"systemTrackerCPUHalflife"`
|
|
||||||
|
|
||||||
// Halflife to use for the disk tracker.
|
|
||||||
// Larger halflife --> disk usage metrics change more slowly.
|
|
||||||
SystemTrackerDiskHalflife time.Duration `json:"systemTrackerDiskHalflife"`
|
|
||||||
|
|
||||||
CPUTargeterConfig TargeterConfig `json:"cpuTargeterConfig"`
|
|
||||||
|
|
||||||
DiskTargeterConfig TargeterConfig `json:"diskTargeterConfig"`
|
|
||||||
|
|
||||||
RequiredAvailableDiskSpace uint64 `json:"requiredAvailableDiskSpace"`
|
|
||||||
WarningThresholdAvailableDiskSpace uint64 `json:"warningThresholdAvailableDiskSpace"`
|
|
||||||
|
|
||||||
TraceConfig trace.Config `json:"traceConfig"`
|
|
||||||
|
|
||||||
// See comment on [UseCurrentHeight] in platformvm.Config
|
|
||||||
UseCurrentHeight bool `json:"useCurrentHeight"`
|
|
||||||
|
|
||||||
// ProvidedFlags contains all the flags set by the user
|
|
||||||
ProvidedFlags map[string]interface{} `json:"-"`
|
|
||||||
|
|
||||||
// ChainDataDir is the root path for per-chain directories where VMs can
|
|
||||||
// write arbitrary data.
|
|
||||||
ChainDataDir string `json:"chainDataDir"`
|
|
||||||
|
|
||||||
// ImportChainData is the path to import blockchain data from another chain
|
|
||||||
ImportChainData string `json:"importChainData"`
|
|
||||||
|
|
||||||
// Path to write process context to (including PID, API URI, and
|
|
||||||
// staking address).
|
|
||||||
ProcessContextFilePath string `json:"processContextFilePath"`
|
|
||||||
|
|
||||||
// POA Mode Configuration
|
|
||||||
POAModeEnabled bool `json:"poaModeEnabled"`
|
|
||||||
POASingleNodeMode bool `json:"poaSingleNodeMode"`
|
|
||||||
POAMinBlockTime time.Duration `json:"poaMinBlockTime"`
|
|
||||||
POAAuthorizedNodes []string `json:"poaAuthorizedNodes"`
|
|
||||||
|
|
||||||
// Low Memory Configuration
|
|
||||||
LowMemoryEnabled bool `json:"lowMemoryEnabled"`
|
|
||||||
DBCacheSize uint64 `json:"dbCacheSize"`
|
|
||||||
DBMemtableSize uint64 `json:"dbMemtableSize"`
|
|
||||||
StateCacheSize uint64 `json:"stateCacheSize"`
|
|
||||||
BlockCacheSize uint64 `json:"blockCacheSize"`
|
|
||||||
DisableBloomFilters bool `json:"disableBloomFilters"`
|
|
||||||
LazyChainLoading bool `json:"lazyChainLoading"`
|
|
||||||
SingleValidatorMode bool `json:"singleValidatorMode"`
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
Log log.Logger `json:"-"`
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
// Package dexvm re-exports the canonical DEX VM from
|
|
||||||
// github.com/luxfi/chains/dexvm so existing callers that imported
|
|
||||||
// github.com/luxfi/node/vms/dexvm pre-extraction keep working
|
|
||||||
// without source-level changes.
|
|
||||||
//
|
|
||||||
// New code should import the canonical path:
|
|
||||||
// "github.com/luxfi/chains/dexvm"
|
|
||||||
//
|
|
||||||
// This package is a thin backward-compatibility alias. The underlying
|
|
||||||
// chains/dexvm is the pure-Go stateless atomic proxy (zero private deps).
|
|
||||||
// Unlike the always-on genesis VMs, dexvm is registered in OptionalVMs and is
|
|
||||||
// NFT-gated (see node/vms.go:118, RequiredNFT "dex-operator"): a node only
|
|
||||||
// tracks/validates the D-Chain when the network has configured that operator
|
|
||||||
// collection, so it is plugin-loaded on demand, not linked unconditionally.
|
|
||||||
package dexvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/luxfi/chains/dexvm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Re-export the public surface.
|
|
||||||
type (
|
|
||||||
Block = dexvm.Block
|
|
||||||
ChainVM = dexvm.ChainVM
|
|
||||||
Factory = dexvm.Factory
|
|
||||||
OrderKey = dexvm.OrderKey
|
|
||||||
DexVertex = dexvm.DexVertex
|
|
||||||
Status = dexvm.Status
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// VMID identifies the canonical primary-network D-Chain VM.
|
|
||||||
VMID = dexvm.VMID
|
|
||||||
|
|
||||||
// NewChainVM constructs a fresh DEX chain VM.
|
|
||||||
NewChainVM = dexvm.NewChainVM
|
|
||||||
)
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
// Package mpcvm — backend selection re-export.
|
|
||||||
//
|
|
||||||
// The canonical dlopen probe lives in github.com/luxfi/chains/mpcvm.
|
|
||||||
// Its init() runs once at package load time and pins a process-wide
|
|
||||||
// GPUBackend handle (cuda → hip → metal → vulkan → webgpu probe order).
|
|
||||||
// This file is the node-side entry point for diagnostics and ops tooling.
|
|
||||||
//
|
|
||||||
// Note: the luxd VM manager registration for ThresholdVM (M-Chain) is
|
|
||||||
// NOT flipped here per the project memory note ("M-Chain code exists but
|
|
||||||
// NOT yet registered in running luxd"). This file provides the bridge
|
|
||||||
// surface only — flipping the manager hook-up is a separate ops PR.
|
|
||||||
|
|
||||||
package mpcvm
|
|
||||||
|
|
||||||
// SelectGPUBackend returns the resolved GPU plugin (or nil) and a single
|
|
||||||
// human-readable diagnostic string. luxd startup logs use this to surface
|
|
||||||
// "mpcvm-gpu backend=<name>" lines alongside the cevm backend
|
|
||||||
// selection, matching the cevm.go pattern at
|
|
||||||
// ~/work/lux/chains/evm/backend_cgo.go.
|
|
||||||
//
|
|
||||||
// Calling this multiple times is cheap — the underlying probe runs once
|
|
||||||
// at package init via sync.Once in chains/mpcvm/backend.go.
|
|
||||||
func SelectGPUBackend() (*GPUBackend, string) {
|
|
||||||
g := GPUBackendInstance()
|
|
||||||
if g == nil || !g.IsAvailable() {
|
|
||||||
return nil, "mpcvm-gpu: no plugin resolved (CPU-only)"
|
|
||||||
}
|
|
||||||
return g, "mpcvm-gpu: backend=" + g.Kind.String() + " path=" + g.Path
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
// Package mpcvm re-exports the canonical Threshold (FHE / MPC)
|
|
||||||
// VM from github.com/luxfi/chains/mpcvm so existing callers
|
|
||||||
// that imported github.com/luxfi/node/vms/mpcvm pre-extraction
|
|
||||||
// keep working without source changes.
|
|
||||||
//
|
|
||||||
// New code should import the canonical path:
|
|
||||||
// "github.com/luxfi/chains/mpcvm"
|
|
||||||
//
|
|
||||||
// This file is a thin alias wrapper kept for backward compatibility.
|
|
||||||
package mpcvm
|
|
||||||
|
|
||||||
import "github.com/luxfi/chains/mpcvm"
|
|
||||||
|
|
||||||
type (
|
|
||||||
Block = mpcvm.Block
|
|
||||||
BlockError = mpcvm.BlockError
|
|
||||||
Client = mpcvm.Client
|
|
||||||
Operation = mpcvm.Operation
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidOperation = mpcvm.ErrInvalidOperation
|
|
||||||
NewClient = mpcvm.NewClient
|
|
||||||
)
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
//go:build cgo
|
|
||||||
|
|
||||||
// Package mpcvm re-exports the GPU bridge from
|
|
||||||
// github.com/luxfi/chains/mpcvm so callers that import the legacy
|
|
||||||
// node path keep working without source changes.
|
|
||||||
//
|
|
||||||
// One and only one way to dlopen: the canonical bridge lives in
|
|
||||||
// chains/mpcvm. This package is a typed alias layer — both the
|
|
||||||
// types and the GPUBackend singleton come straight from the upstream.
|
|
||||||
// There is exactly one dlopen handle, one symbol table, one init()
|
|
||||||
// probe across the entire luxd process. Consumers of either import path
|
|
||||||
// share the same plugin instance.
|
|
||||||
|
|
||||||
package mpcvm
|
|
||||||
|
|
||||||
import "github.com/luxfi/chains/mpcvm"
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Type re-exports — Go aliases preserve the public API so external callers
|
|
||||||
// (e.g. node/chains, node/main) can drop the import path without source
|
|
||||||
// changes. The GPU- prefix avoids collisions with the domain types
|
|
||||||
// already aliased above (Block, Client, Operation).
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
type (
|
|
||||||
GPUBackend = mpcvm.GPUBackend
|
|
||||||
GPUBackendKind = mpcvm.GPUBackendKind
|
|
||||||
|
|
||||||
GPUCeremony = mpcvm.GPUCeremony
|
|
||||||
GPUKeyShare = mpcvm.GPUKeyShare
|
|
||||||
GPUContribution = mpcvm.GPUContribution
|
|
||||||
GPUMPCVMState = mpcvm.GPUMPCVMState
|
|
||||||
GPUMPCVMRoundDescriptor = mpcvm.GPUMPCVMRoundDescriptor
|
|
||||||
GPUCeremonyOp = mpcvm.GPUCeremonyOp
|
|
||||||
GPUContributionOp = mpcvm.GPUContributionOp
|
|
||||||
GPUMPCVMTransitionResult = mpcvm.GPUMPCVMTransitionResult
|
|
||||||
)
|
|
||||||
|
|
||||||
// Backend constants re-exported. Matches the chains/mpcvm dlopen
|
|
||||||
// probe order: cuda → hip → metal → vulkan → webgpu.
|
|
||||||
const (
|
|
||||||
GPUBackendNone = mpcvm.GPUBackendNone
|
|
||||||
GPUBackendCUDA = mpcvm.GPUBackendCUDA
|
|
||||||
GPUBackendHIP = mpcvm.GPUBackendHIP
|
|
||||||
GPUBackendMetal = mpcvm.GPUBackendMetal
|
|
||||||
GPUBackendVulkan = mpcvm.GPUBackendVulkan
|
|
||||||
GPUBackendWebGPU = mpcvm.GPUBackendWebGPU
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrGPUNotAvailable is the canonical "no plugin loaded" error. Callers
|
|
||||||
// `errors.Is(err, mpcvm.ErrGPUNotAvailable)` to distinguish a
|
|
||||||
// fallback-to-CPU condition from a hard launcher failure.
|
|
||||||
var ErrGPUNotAvailable = mpcvm.ErrGPUNotAvailable
|
|
||||||
|
|
||||||
// GPUBackendInstance returns the dlopen'd GPU plugin handle resolved at
|
|
||||||
// chains/mpcvm package init. nil means no plugin was loaded
|
|
||||||
// (CPU-only mode). The handle is shared across the whole process — both
|
|
||||||
// the node and chains paths see the same instance.
|
|
||||||
//
|
|
||||||
// The name is GPUBackendInstance (not GPUBackend / Backend) because Go
|
|
||||||
// disallows a function named the same as a type alias in the same
|
|
||||||
// package, and `Backend` is already a domain term in the threshold
|
|
||||||
// state machine. Callers write:
|
|
||||||
//
|
|
||||||
// if g := mpcvm.GPUBackendInstance(); g != nil && g.IsAvailable() {
|
|
||||||
// _, err := g.CeremonyApply(desc, ops, ceremonies)
|
|
||||||
// ...
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// This mirrors the cevm pattern `cevm.AvailableBackends()` /
|
|
||||||
// `cevm.LibraryABIVersion()` — discovery via package-scope function,
|
|
||||||
// not via a global variable.
|
|
||||||
func GPUBackendInstance() *GPUBackend {
|
|
||||||
return mpcvm.Backend()
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
//go:build !cgo
|
|
||||||
|
|
||||||
// nocgo re-export of the chains/mpcvm GPU bridge. Under !cgo the
|
|
||||||
// upstream GPUBackend methods all return ErrGPUNotAvailable and
|
|
||||||
// Backend() returns nil; this layer transparently passes that through
|
|
||||||
// so callers see identical behaviour regardless of build mode.
|
|
||||||
//
|
|
||||||
// One and only one nocgo stub for the entire process: it lives in
|
|
||||||
// chains/mpcvm. This package just re-exports the types and the
|
|
||||||
// sentinel error.
|
|
||||||
|
|
||||||
package mpcvm
|
|
||||||
|
|
||||||
import "github.com/luxfi/chains/mpcvm"
|
|
||||||
|
|
||||||
type (
|
|
||||||
GPUBackend = mpcvm.GPUBackend
|
|
||||||
GPUBackendKind = mpcvm.GPUBackendKind
|
|
||||||
|
|
||||||
GPUCeremony = mpcvm.GPUCeremony
|
|
||||||
GPUKeyShare = mpcvm.GPUKeyShare
|
|
||||||
GPUContribution = mpcvm.GPUContribution
|
|
||||||
GPUMPCVMState = mpcvm.GPUMPCVMState
|
|
||||||
GPUMPCVMRoundDescriptor = mpcvm.GPUMPCVMRoundDescriptor
|
|
||||||
GPUCeremonyOp = mpcvm.GPUCeremonyOp
|
|
||||||
GPUContributionOp = mpcvm.GPUContributionOp
|
|
||||||
GPUMPCVMTransitionResult = mpcvm.GPUMPCVMTransitionResult
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
GPUBackendNone = mpcvm.GPUBackendNone
|
|
||||||
GPUBackendCUDA = mpcvm.GPUBackendCUDA
|
|
||||||
GPUBackendHIP = mpcvm.GPUBackendHIP
|
|
||||||
GPUBackendMetal = mpcvm.GPUBackendMetal
|
|
||||||
GPUBackendVulkan = mpcvm.GPUBackendVulkan
|
|
||||||
GPUBackendWebGPU = mpcvm.GPUBackendWebGPU
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrGPUNotAvailable = mpcvm.ErrGPUNotAvailable
|
|
||||||
|
|
||||||
// GPUBackendInstance returns nil under !cgo — the upstream Backend()
|
|
||||||
// returns nil because no dlopen ever happens. Callers branch on the
|
|
||||||
// IsAvailable() check (or `g == nil`) and route to the CPU reference.
|
|
||||||
func GPUBackendInstance() *GPUBackend {
|
|
||||||
return mpcvm.Backend()
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
package mpcvm
|
|
||||||
|
|
||||||
// TestGPUBridgeCgoNocgoParity is the node-side mirror of the parity
|
|
||||||
// test in chains/mpcvm — proves that the re-exported GPUBackend
|
|
||||||
// method set produces byte-identical MPC ceremony state transitions
|
|
||||||
// regardless of build flavor (cgo / nocgo) and plugin presence.
|
|
||||||
//
|
|
||||||
// The node package is a thin alias layer over chains/mpcvm —
|
|
||||||
// type aliases on the wire structs and a re-exported GPUBackendInstance
|
|
||||||
// accessor — so the parity properties of the canonical bridge transfer
|
|
||||||
// directly. We re-run the four-op pipeline through the node-side
|
|
||||||
// surface to pin that the alias layer doesn't drop any state on the
|
|
||||||
// floor.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
chainsthreshold "github.com/luxfi/chains/mpcvm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestGPUBridgeCgoNocgoParity runs a 3-of-5 FROST keygen ceremony
|
|
||||||
// through GPUBackendInstance() (the node-side accessor) and compares
|
|
||||||
// the resulting arena byte-for-byte to a fresh run via the canonical
|
|
||||||
// bridge in chains/mpcvm. Under cgo, the comparison is the
|
|
||||||
// upstream cgo bridge vs itself (both runs hit the same dispatcher).
|
|
||||||
// Under !cgo, the comparison is the upstream nocgo bridge vs itself.
|
|
||||||
// Either path proves the alias layer is transparent and the substrate
|
|
||||||
// transitions deterministically.
|
|
||||||
func TestGPUBridgeCgoNocgoParity(t *testing.T) {
|
|
||||||
const cid uint64 = 0xCEFA01CA001
|
|
||||||
const N = 16 // power of 2 for the open-addressing locator
|
|
||||||
|
|
||||||
var subject, seed [32]byte
|
|
||||||
for i := 0; i < 32; i++ {
|
|
||||||
subject[i] = byte(i + 1)
|
|
||||||
seed[i] = byte(0xA0 ^ i)
|
|
||||||
}
|
|
||||||
|
|
||||||
beginOps := []chainsthreshold.GPUCeremonyOp{
|
|
||||||
{
|
|
||||||
CeremonyID: cid,
|
|
||||||
DeadlineNs: 10_000_000_000,
|
|
||||||
Kind: 0, // kCeremonyOpBegin
|
|
||||||
CeremonyKind: 0, // kKindFrostKeygen → 3 rounds
|
|
||||||
Threshold: 3,
|
|
||||||
TotalParticipants: 5,
|
|
||||||
Subject: subject,
|
|
||||||
CeremonySeed: seed,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
buildRound := func(round uint32) []chainsthreshold.GPUContributionOp {
|
|
||||||
ops := make([]chainsthreshold.GPUContributionOp, 5)
|
|
||||||
for h := uint32(0); h < 5; h++ {
|
|
||||||
var payload [384]byte
|
|
||||||
for k := 0; k < 16; k++ {
|
|
||||||
payload[k] = byte((round * 16) + h*4 + uint32(k))
|
|
||||||
}
|
|
||||||
ops[h] = chainsthreshold.GPUContributionOp{
|
|
||||||
CeremonyID: cid,
|
|
||||||
HolderAddr: uint64(0xDEAD0000 | h),
|
|
||||||
Round: round,
|
|
||||||
HolderIndex: h,
|
|
||||||
PayloadLen: 16,
|
|
||||||
Payload: payload,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ops
|
|
||||||
}
|
|
||||||
r1 := buildRound(0)
|
|
||||||
r2 := buildRound(1)
|
|
||||||
r3 := buildRound(2)
|
|
||||||
|
|
||||||
desc1 := &GPUMPCVMRoundDescriptor{
|
|
||||||
ChainID: 0xABBA,
|
|
||||||
Round: 1,
|
|
||||||
TimestampNs: 1_000_000_000,
|
|
||||||
Epoch: 1,
|
|
||||||
CeremonyOpCount: 1,
|
|
||||||
ContributionOpCount: 5,
|
|
||||||
}
|
|
||||||
desc2 := &GPUMPCVMRoundDescriptor{
|
|
||||||
ChainID: 0xABBA,
|
|
||||||
Round: 2,
|
|
||||||
TimestampNs: 2_000_000_000,
|
|
||||||
Epoch: 1,
|
|
||||||
ContributionOpCount: 5,
|
|
||||||
}
|
|
||||||
desc3 := &GPUMPCVMRoundDescriptor{
|
|
||||||
ChainID: 0xABBA,
|
|
||||||
Round: 3,
|
|
||||||
TimestampNs: 3_000_000_000,
|
|
||||||
Epoch: 1,
|
|
||||||
ContributionOpCount: 5,
|
|
||||||
}
|
|
||||||
descClose := &GPUMPCVMRoundDescriptor{
|
|
||||||
ChainID: 0xABBA,
|
|
||||||
Round: 4,
|
|
||||||
TimestampNs: 4_000_000_000,
|
|
||||||
Epoch: 1,
|
|
||||||
ClosingFlag: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run via node-side re-export.
|
|
||||||
runVia := func(t *testing.T) (
|
|
||||||
[]GPUCeremony,
|
|
||||||
[]GPUKeyShare,
|
|
||||||
[]GPUContribution,
|
|
||||||
GPUMPCVMState,
|
|
||||||
GPUMPCVMTransitionResult,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
|
||||||
cer := make([]GPUCeremony, N)
|
|
||||||
keys := make([]GPUKeyShare, N)
|
|
||||||
con := make([]GPUContribution, N)
|
|
||||||
|
|
||||||
b := GPUBackendInstance() // nil-receiver-safe via fallback to CPU reference
|
|
||||||
|
|
||||||
if _, err := b.CeremonyApply(desc1, beginOps, cer); err != nil {
|
|
||||||
if strings.Contains(err.Error(), "GPU backend not available") {
|
|
||||||
t.Skip("GPU plugin not dlopened in this build; CPU-only path covered elsewhere")
|
|
||||||
}
|
|
||||||
t.Fatalf("CeremonyApply r0: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := b.ContributionApply(desc1, r1, cer, con, 1); err != nil {
|
|
||||||
t.Fatalf("ContributionApply r0: %v", err)
|
|
||||||
}
|
|
||||||
if _, _, _, err := b.KeyShareApply(desc1, cer, keys, con, 1); err != nil {
|
|
||||||
t.Fatalf("KeyShareApply r0: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := b.ContributionApply(desc2, r2, cer, con, 6); err != nil {
|
|
||||||
t.Fatalf("ContributionApply r1: %v", err)
|
|
||||||
}
|
|
||||||
if _, _, _, err := b.KeyShareApply(desc2, cer, keys, con, 1); err != nil {
|
|
||||||
t.Fatalf("KeyShareApply r1: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := b.ContributionApply(desc3, r3, cer, con, 11); err != nil {
|
|
||||||
t.Fatalf("ContributionApply r2: %v", err)
|
|
||||||
}
|
|
||||||
if _, _, _, err := b.KeyShareApply(desc3, cer, keys, con, 1); err != nil {
|
|
||||||
t.Fatalf("KeyShareApply r2: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var state GPUMPCVMState
|
|
||||||
res, err := b.MPCTransition(descClose, cer, keys, con, &state)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MPCTransition: %v", err)
|
|
||||||
}
|
|
||||||
return cer, keys, con, state, *res
|
|
||||||
}
|
|
||||||
|
|
||||||
cerA, keysA, conA, stateA, resA := runVia(t)
|
|
||||||
cerB, keysB, conB, stateB, resB := runVia(t)
|
|
||||||
|
|
||||||
for i := range cerA {
|
|
||||||
if cerA[i] != cerB[i] {
|
|
||||||
t.Errorf("ceremony slot %d differs across runs:\nA=%+v\nB=%+v", i, cerA[i], cerB[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := range keysA {
|
|
||||||
if keysA[i] != keysB[i] {
|
|
||||||
t.Errorf("keyShare slot %d differs across runs:\nA=%+v\nB=%+v", i, keysA[i], keysB[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := range conA {
|
|
||||||
if conA[i] != conB[i] {
|
|
||||||
t.Errorf("contribution slot %d differs across runs:\nA=%+v\nB=%+v", i, conA[i], conB[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if stateA != stateB {
|
|
||||||
t.Errorf("state differs across runs:\nA=%+v\nB=%+v", stateA, stateB)
|
|
||||||
}
|
|
||||||
if resA != resB {
|
|
||||||
t.Errorf("result differs across runs:\nA=%+v\nB=%+v", resA, resB)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanity — the ceremony MUST have finalized.
|
|
||||||
if stateA.FinalizedCeremonyCount != 1 {
|
|
||||||
t.Errorf("expected 1 finalized ceremony, got %d", stateA.FinalizedCeremonyCount)
|
|
||||||
}
|
|
||||||
if stateA.KeyShareCount != 5 {
|
|
||||||
t.Errorf("expected 5 emitted key shares, got %d", stateA.KeyShareCount)
|
|
||||||
}
|
|
||||||
var zero [32]byte
|
|
||||||
if stateA.MPCVMStateRoot == zero {
|
|
||||||
t.Errorf("mpcvm_state_root is zero — the fold produced no output")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
package mpcvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestNodeGPULayoutSizes pins the re-exported wire structs to the same
|
|
||||||
// device-side __align__(16) values declared in
|
|
||||||
// the GPU plugin install tree ops/mpcvm/cuda/mpcvm_kernels_common.cuh.
|
|
||||||
//
|
|
||||||
// Even though the structs are type aliases to chains/mpcvm, this
|
|
||||||
// test sits at the node import boundary — if upstream sizes drift the
|
|
||||||
// node-side surface MUST also fail loud rather than miscompile.
|
|
||||||
func TestNodeGPULayoutSizes(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
want uintptr
|
|
||||||
got uintptr
|
|
||||||
}{
|
|
||||||
{"GPUCeremony", 128, unsafe.Sizeof(GPUCeremony{})},
|
|
||||||
{"GPUKeyShare", 368, unsafe.Sizeof(GPUKeyShare{})},
|
|
||||||
{"GPUContribution", 432, unsafe.Sizeof(GPUContribution{})},
|
|
||||||
{"GPUMPCVMState", 160, unsafe.Sizeof(GPUMPCVMState{})},
|
|
||||||
{"GPUMPCVMRoundDescriptor", 96, unsafe.Sizeof(GPUMPCVMRoundDescriptor{})},
|
|
||||||
{"GPUCeremonyOp", 96, unsafe.Sizeof(GPUCeremonyOp{})},
|
|
||||||
{"GPUContributionOp", 416, unsafe.Sizeof(GPUContributionOp{})},
|
|
||||||
{"GPUMPCVMTransitionResult", 176, unsafe.Sizeof(GPUMPCVMTransitionResult{})},
|
|
||||||
}
|
|
||||||
for _, c := range cases {
|
|
||||||
if c.got != c.want {
|
|
||||||
t.Errorf("%s: sizeof=%d want=%d", c.name, c.got, c.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestNodeGPUBackendRoundTrip is the dlopen round-trip required by the
|
|
||||||
// task spec. Two acceptable outcomes:
|
|
||||||
//
|
|
||||||
// 1. Plugin resolved: GPUBackendInstance() != nil && IsAvailable(); a
|
|
||||||
// zero-fixture CeremonyApply against the best backend returns rc=0
|
|
||||||
// (no ops processed) with no error.
|
|
||||||
//
|
|
||||||
// 2. Plugin absent: GPUBackendInstance() == nil; nil-receiver methods
|
|
||||||
// return ErrGPUNotAvailable. SelectGPUBackend reports the CPU-only
|
|
||||||
// diagnostic string.
|
|
||||||
//
|
|
||||||
// Both outcomes count as passing — the bridge correctly surfaces GPU
|
|
||||||
// state through the public node API.
|
|
||||||
func TestNodeGPUBackendRoundTrip(t *testing.T) {
|
|
||||||
b, diag := SelectGPUBackend()
|
|
||||||
t.Logf("SelectGPUBackend: %s", diag)
|
|
||||||
if b == nil {
|
|
||||||
// Plugin-absent path. The nil receiver contract must hold —
|
|
||||||
// every method returns ErrGPUNotAvailable, no panic.
|
|
||||||
_, err := (*GPUBackend)(nil).CeremonyApply(nil, nil, nil)
|
|
||||||
if !errors.Is(err, ErrGPUNotAvailable) {
|
|
||||||
t.Fatalf("nil receiver CeremonyApply: want ErrGPUNotAvailable, got %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plugin-resolved path. Zero fixture: 1-slot ceremony arena, no ops.
|
|
||||||
// The kernel walks zero ops and returns applied=0 with rc=0. Any
|
|
||||||
// non-zero rc means a real launcher failure.
|
|
||||||
desc := &GPUMPCVMRoundDescriptor{
|
|
||||||
ChainID: 0xCAFEBABE,
|
|
||||||
Round: 1,
|
|
||||||
TimestampNs: 1700000000_000000000,
|
|
||||||
Epoch: 1,
|
|
||||||
}
|
|
||||||
ceremonies := make([]GPUCeremony, 1)
|
|
||||||
applied, err := b.CeremonyApply(desc, nil, ceremonies)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CeremonyApply(zero): %v", err)
|
|
||||||
}
|
|
||||||
if applied != 0 {
|
|
||||||
t.Errorf("CeremonyApply(zero): applied=%d want=0", applied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestNodeGPUStubContract verifies the nocgo-equivalent contract on a
|
|
||||||
// zero-value GPUBackend: IsAvailable()==false and every state-machine
|
|
||||||
// method returns ErrGPUNotAvailable. The same contract holds under cgo
|
|
||||||
// when no plugin is dlopened — making this test build-flavor-independent.
|
|
||||||
func TestNodeGPUStubContract(t *testing.T) {
|
|
||||||
var b GPUBackend
|
|
||||||
if b.IsAvailable() {
|
|
||||||
t.Fatal("zero GPUBackend.IsAvailable() must be false")
|
|
||||||
}
|
|
||||||
if _, err := b.CeremonyApply(nil, nil, nil); !errors.Is(err, ErrGPUNotAvailable) {
|
|
||||||
t.Errorf("CeremonyApply: want ErrGPUNotAvailable, got %v", err)
|
|
||||||
}
|
|
||||||
if _, _, _, err := b.KeyShareApply(nil, nil, nil, nil, 0); !errors.Is(err, ErrGPUNotAvailable) {
|
|
||||||
t.Errorf("KeyShareApply: want ErrGPUNotAvailable, got %v", err)
|
|
||||||
}
|
|
||||||
if _, err := b.ContributionApply(nil, nil, nil, nil, 0); !errors.Is(err, ErrGPUNotAvailable) {
|
|
||||||
t.Errorf("ContributionApply: want ErrGPUNotAvailable, got %v", err)
|
|
||||||
}
|
|
||||||
if _, err := b.MPCTransition(nil, nil, nil, nil, nil); !errors.Is(err, ErrGPUNotAvailable) {
|
|
||||||
t.Errorf("MPCTransition: want ErrGPUNotAvailable, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user