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:
zeekay
2026-07-25 12:26:36 -07:00
parent 011c3df6bd
commit e22009db91
14 changed files with 0 additions and 2286 deletions
-244
View File
@@ -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.
-168
View File
@@ -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)
}
-302
View File
@@ -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())
}
-324
View File
@@ -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
}
}
-291
View File
@@ -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,
})
}
}
-165
View File
@@ -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
View File
@@ -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:"-"`
}
-40
View File
@@ -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
)
-32
View File
@@ -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
}
-27
View File
@@ -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
)
-78
View File
@@ -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()
}
-49
View File
@@ -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()
}
-194
View File
@@ -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")
}
}
-107
View File
@@ -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)
}
}