mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:06:23 +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