Improve genesis loading and snapshot management

- Enhance genesis.go with LoadCanonicalGenesis for mainnet/testnet
- Add LoadGenesisForNetwork to load from external paths
- Improve snapshot.go with better incremental backup support
- Fix network ID handling in genesis_config.go (use constants.MainnetID not ChainID)
- Update API client interfaces for better type safety
- Fix server network handling for proper genesis selection
- Update dependencies to latest versions
This commit is contained in:
Zach Kelling
2026-01-20 00:03:35 -08:00
parent 160e25c144
commit 2157b410c6
17 changed files with 916 additions and 381 deletions
+9 -9
View File
@@ -3,12 +3,12 @@ package api
import (
"fmt"
"github.com/luxfi/sdk/api/exchangevm"
"github.com/luxfi/sdk/api/platformvm"
"github.com/luxfi/sdk/api/admin"
"github.com/luxfi/sdk/api/health"
"github.com/luxfi/sdk/api/indexer"
"github.com/luxfi/sdk/api/info"
"github.com/luxfi/sdk/exchangevm"
"github.com/luxfi/sdk/platformvm"
"github.com/luxfi/sdk/admin"
"github.com/luxfi/sdk/health"
"github.com/luxfi/sdk/indexer"
sdkinfo "github.com/luxfi/sdk/info"
// evmclient "github.com/luxfi/evm/plugin/evm/client"
)
@@ -25,7 +25,7 @@ type APIClient struct {
xChainWallet *exchangevm.WalletClient
cChain interface{} // evmclient.Client
cChainEth EthClient
info *info.Client
info *sdkinfo.Client
health *health.Client
admin *admin.Client
pindex *indexer.Client
@@ -44,7 +44,7 @@ func NewAPIClient(ipAddr string, port uint16) Client {
platformClient := platformvm.NewClient(uri)
xChainClient := exchangevm.NewClient(uri, "X")
xChainWalletClient := exchangevm.NewWalletClient(uri, "X")
infoClient := info.NewClient(uri)
infoClient := sdkinfo.NewClient(uri)
healthClient := health.NewClient(uri)
adminClient := admin.NewClient(uri)
pindexClient := indexer.NewClient(uri + "/ext/index/P/block")
@@ -84,7 +84,7 @@ func (c APIClient) CChainEthAPI() EthClient {
return c.cChainEth
}
func (c APIClient) InfoAPI() *info.Client {
func (c APIClient) InfoAPI() *sdkinfo.Client {
return c.info
}
+10 -10
View File
@@ -7,21 +7,21 @@ import (
ethereum "github.com/luxfi/geth"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/sdk/api/exchangevm"
"github.com/luxfi/sdk/api/platformvm"
"github.com/luxfi/sdk/exchangevm"
"github.com/luxfi/sdk/platformvm"
"github.com/luxfi/rpc"
"github.com/luxfi/sdk/api/admin"
"github.com/luxfi/sdk/api/health"
"github.com/luxfi/sdk/api/indexer"
"github.com/luxfi/sdk/api/info"
"github.com/luxfi/sdk/admin"
apihealth "github.com/luxfi/api/health"
"github.com/luxfi/sdk/indexer"
sdkinfo "github.com/luxfi/sdk/info"
// evmclient "github.com/luxfi/evm/plugin/evm/client"
)
// HealthClient is the interface for health API client operations
type HealthClient interface {
Readiness(ctx context.Context, tags []string, options ...rpc.Option) (*health.APIReply, error)
Health(ctx context.Context, tags []string, options ...rpc.Option) (*health.APIReply, error)
Liveness(ctx context.Context, tags []string, options ...rpc.Option) (*health.APIReply, error)
Readiness(ctx context.Context, tags []string, options ...rpc.Option) (*apihealth.APIReply, error)
Health(ctx context.Context, tags []string, options ...rpc.Option) (*apihealth.APIReply, error)
Liveness(ctx context.Context, tags []string, options ...rpc.Option) (*apihealth.APIReply, error)
}
// EthClient is the interface for ethereum client operations
@@ -70,7 +70,7 @@ type Client interface {
XChainWalletAPI() *exchangevm.WalletClient
CChainAPI() interface{} // evmclient.Client
CChainEthAPI() EthClient // ethclient websocket wrapper that adds mutexed calls, and lazy conn init (on first call)
InfoAPI() *info.Client
InfoAPI() *sdkinfo.Client
HealthAPI() HealthClient
AdminAPI() *admin.Client
PChainIndexAPI() *indexer.Client
+9 -9
View File
@@ -4,17 +4,17 @@ package mocks
import (
api "github.com/luxfi/netrunner/api"
admin "github.com/luxfi/sdk/api/admin"
admin "github.com/luxfi/sdk/admin"
exchangevm "github.com/luxfi/sdk/api/exchangevm"
exchangevm "github.com/luxfi/sdk/exchangevm"
indexer "github.com/luxfi/sdk/api/indexer"
indexer "github.com/luxfi/sdk/indexer"
info "github.com/luxfi/sdk/api/info"
sdkinfo "github.com/luxfi/sdk/info"
mock "github.com/stretchr/testify/mock"
platformvm "github.com/luxfi/sdk/api/platformvm"
platformvm "github.com/luxfi/sdk/platformvm"
)
// Client is an autogenerated mock type for the Client type
@@ -101,15 +101,15 @@ func (_m *Client) HealthAPI() api.HealthClient {
}
// InfoAPI provides a mock function with given fields:
func (_m *Client) InfoAPI() *info.Client {
func (_m *Client) InfoAPI() *sdkinfo.Client {
ret := _m.Called()
var r0 *info.Client
if rf, ok := ret.Get(0).(func() *info.Client); ok {
var r0 *sdkinfo.Client
if rf, ok := ret.Get(0).(func() *sdkinfo.Client); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*info.Client)
r0 = ret.Get(0).(*sdkinfo.Client)
}
}
+9 -8
View File
@@ -13,8 +13,9 @@ import (
"github.com/luxfi/ids"
"github.com/luxfi/netrunner/engines"
"github.com/luxfi/node/api/health"
"github.com/luxfi/sdk/api/info"
apiinfo "github.com/luxfi/api/info"
sdkhealth "github.com/luxfi/sdk/health"
sdkinfo "github.com/luxfi/sdk/info"
)
func init() {
@@ -33,8 +34,8 @@ type LuxEngine struct {
dataDir string
config *engines.NodeConfig
process *exec.Cmd
infoClient *info.Client
healthClient *health.Client
infoClient *sdkinfo.Client
healthClient *sdkhealth.Client
startTime time.Time
// Cached info
@@ -115,8 +116,8 @@ func (e *LuxEngine) Start(ctx context.Context, config *engines.NodeConfig) error
e.startTime = time.Now()
// Setup RPC clients
e.infoClient = info.NewClient(e.RPCEndpoint())
e.healthClient = health.NewClient(e.RPCEndpoint())
e.infoClient = sdkinfo.NewClient(e.RPCEndpoint())
e.healthClient = sdkhealth.NewClient(e.RPCEndpoint())
// Wait for node to be responsive
ticker := time.NewTicker(500 * time.Millisecond)
@@ -188,13 +189,13 @@ func (e *LuxEngine) Health(ctx context.Context) (*engines.HealthStatus, error) {
// Get peers
peers, err := e.infoClient.Peers(ctx, nil)
if err != nil {
peers = []info.Peer{}
peers = []apiinfo.Peer{}
}
// Get version
versions, err := e.infoClient.GetNodeVersion(ctx)
if err != nil {
versions = &info.GetNodeVersionReply{}
versions = &apiinfo.GetNodeVersionReply{}
}
return &engines.HealthStatus{
+28 -24
View File
@@ -12,28 +12,30 @@ require (
github.com/dgraph-io/badger/v4 v4.9.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4
github.com/luxfi/address v1.0.1
github.com/luxfi/atomic v0.0.1
github.com/luxfi/compress v0.0.3
github.com/luxfi/config v1.1.1
github.com/luxfi/consensus v1.22.53
github.com/luxfi/constants v1.4.3
github.com/luxfi/crypto v1.17.39
github.com/luxfi/crypto v1.17.40
github.com/luxfi/genesis v1.5.21
github.com/luxfi/geth v1.16.69
github.com/luxfi/go-bip39 v1.1.2
github.com/luxfi/ids v1.2.9
github.com/luxfi/keys v1.0.7
github.com/luxfi/log v1.2.1
github.com/luxfi/log v1.3.0
github.com/luxfi/math v1.2.3
github.com/luxfi/metric v1.4.10
github.com/luxfi/node v1.22.81
github.com/luxfi/p2p v1.18.7
github.com/luxfi/protocol v0.0.2
github.com/luxfi/sdk v1.16.44
github.com/luxfi/sdk/api v0.0.2
github.com/luxfi/utxo v0.0.1
github.com/luxfi/version v1.0.1
github.com/onsi/ginkgo/v2 v2.27.3
github.com/onsi/gomega v1.38.3
github.com/otiai10/copy v1.14.1
github.com/prometheus/client_golang v1.23.2
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/spf13/cobra v1.9.1
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.46.0
@@ -50,16 +52,22 @@ require (
github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/luxfi/codec v1.1.2 // indirect
github.com/luxfi/compress v0.0.3 // indirect
github.com/luxfi/concurrent v0.0.2 // indirect
github.com/luxfi/container v0.0.2 // indirect
github.com/luxfi/formatting v1.0.0 // indirect
github.com/luxfi/pubsub v1.0.0 // indirect
github.com/luxfi/codec v1.1.3 // indirect
github.com/luxfi/concurrent v0.0.3 // indirect
github.com/luxfi/consensus v1.22.53 // indirect
github.com/luxfi/const v1.4.1 // indirect
github.com/luxfi/container v0.0.4 // indirect
github.com/luxfi/formatting v1.0.1 // indirect
github.com/luxfi/precompile v0.4.5 // indirect
github.com/luxfi/sdk/api v0.0.2 // indirect
github.com/luxfi/staking v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/melbahja/goph v1.4.0 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pkg/sftp v1.13.5 // indirect
github.com/posthog/posthog-go v1.6.1 // indirect
github.com/posthog/posthog-go v1.8.2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
)
require (
@@ -94,22 +102,22 @@ require (
github.com/google/btree v1.1.3 // indirect
github.com/google/flatbuffers v25.12.19+incompatible // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/renameio/v2 v2.0.0 // indirect
github.com/google/renameio/v2 v2.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/rpc v1.2.1 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/luxfi/cache v1.2.0 // indirect
github.com/luxfi/database v1.17.38 // indirect
github.com/luxfi/evm v0.8.30 // indirect
github.com/luxfi/go-bip32 v1.0.2 // indirect
github.com/luxfi/keychain v1.0.1 // indirect
github.com/luxfi/keychain v1.0.2 // indirect
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/math/safe v0.0.1 // indirect
github.com/luxfi/mock v0.1.0 // indirect
github.com/luxfi/mock v0.1.1 // indirect
github.com/luxfi/rpc v1.0.0
github.com/luxfi/sampler v1.0.0 // indirect
github.com/luxfi/tls v1.0.3
@@ -164,10 +172,6 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
)
replace (
github.com/luxfi/atomic => ../atomic
github.com/luxfi/consensus => ../consensus
github.com/luxfi/protocol => ../protocol
github.com/luxfi/genesis => ../genesis
github.com/luxfi/log => github.com/luxfi/logger v1.3.1
)
replace github.com/luxfi/log => ../log
replace github.com/luxfi/genesis => ../genesis
+41 -32
View File
@@ -183,8 +183,8 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f h1:HU1RgM6NALf/KW9HEY6zry3ADbDKcmpQ+hJedoNGQYQ=
github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f/go.mod h1:67FPmZWbr+KDT/VlpWtw6sO9XSjpJmLuHpoLmWiTGgY=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/google/renameio/v2 v2.0.1 h1:HyOM6qd9gF9sf15AvhbptGHUnaLTpEI9akAFFU3VyW0=
github.com/google/renameio/v2 v2.0.1/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@@ -230,6 +230,8 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
@@ -244,34 +246,34 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/luxfi/address v1.0.1 h1:Sc4keyuVzBIvHr7uVeYZf2/WY9YDGUgDi/iiWenj49g=
github.com/luxfi/address v1.0.1/go.mod h1:5j3Eh66v9zvv1GbNdZwt+23krV8JlSDaRzmWZU8ZRM0=
github.com/luxfi/atomic v0.0.1 h1:18onjDbda8zGEzk6xO9Tclu+Hb3zq5kxzE//YA4WDvI=
github.com/luxfi/atomic v0.0.1/go.mod h1:UzFk1K7VXjJhjm2c4Yiu5HydKbWi36kFtrlDKbm8MG8=
github.com/luxfi/cache v1.2.0 h1:VEk/ue13oi8w3sKBB/Dsv9fnjmfdXgb0NMibcPLVbss=
github.com/luxfi/cache v1.2.0/go.mod h1:Esn48WMB3JL/+UOdCEV2Chyjr1MG3SSckXWLhB/P080=
github.com/luxfi/codec v1.1.2 h1:uTp4xhKq7DFzsAHG0J+LxOmzagAFmfdvssaHgH9IjcQ=
github.com/luxfi/codec v1.1.2/go.mod h1:T+DV0Jau5bi+8wMcaoj51zS1rgt80VovQKzaN2CV5Mg=
github.com/luxfi/compress v0.0.2 h1:tPZltN+efOjCvooVNJvNYHslVIm50BDD2ocKMk4chdA=
github.com/luxfi/compress v0.0.2/go.mod h1:cfa7L3STdZdsMwJc9faZML1modLNh1n2u72n/DOfuNU=
github.com/luxfi/codec v1.1.3 h1:hK/pijaD/b3xP5ZNkGdDq26jRgFPQtYBLmAwOgxzw4c=
github.com/luxfi/codec v1.1.3/go.mod h1:7CPyGAk8cjSYYOfSbMLfSnXXV7ucuRK6oG8Dvb/ZdWc=
github.com/luxfi/compress v0.0.3 h1:oqub1Mn4F2mDAANl2Y8JHdEAGmS9Bl6yWMHgWuZAkdY=
github.com/luxfi/compress v0.0.3/go.mod h1:XrmmXFvCgXLS74UtArjUIUeAQYDJiy/VzH66vMGeCws=
github.com/luxfi/concurrent v0.0.2 h1:DwGQEKOVenqf38t6BAKfoDsAqjiwrpSKm+h22CiT008=
github.com/luxfi/concurrent v0.0.2/go.mod h1:SwEDrQXVAXgBiObXVg0pfmLzU1xsa9ZeBRptkBdyNUU=
github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
github.com/luxfi/concurrent v0.0.3/go.mod h1:Aj/FR5NpM0cB2P4Nt3+tz9+dV6V+LUW4HuMgSjwq5hw=
github.com/luxfi/config v1.1.1 h1:4ExjqCFgbRaA8ahl6WSmTkqCkDbm6LN0138fAP6rYcY=
github.com/luxfi/config v1.1.1/go.mod h1:HAp0PjB0SGxWM3/bK9xQzeG5B0hWh5pZLA2/8jQ3Vtw=
github.com/luxfi/consensus v1.22.53 h1:iwrnImZJ7Ur6oV7rx0WSrR7n3X92pdnOX3mivCBCuZ4=
github.com/luxfi/consensus v1.22.53/go.mod h1:GicEGtNrPPG6063k/aRFusv87e17i0UwmgeY4D8Vn6s=
github.com/luxfi/const v1.4.1 h1:nd9A/9PaN2XBMPExHo+mgkDt60vrxRoQ/e+UKqskbDg=
github.com/luxfi/const v1.4.1/go.mod h1:qWfecjdImPY3kXsy3UAe4ZVpMcsp2/iUlHwUMPVREoI=
github.com/luxfi/constants v1.4.3 h1:bcqUO8twMOhHWCgN/RbjfWulVPbvUQcxDkssaRrCx4g=
github.com/luxfi/constants v1.4.3/go.mod h1:ENkJ121cmDEkwQPDiKK4QhnTnW9u37PGpepbrdVcAmc=
github.com/luxfi/container v0.0.2 h1:UNt3C5LGlkxU1OJeQUoxRA0AYO0tG35x1ieY9vHdon0=
github.com/luxfi/container v0.0.2/go.mod h1:nnK+TxHmGry0mGnfun4jx0tLqWJgV/GbZXTETMuFJn4=
github.com/luxfi/crypto v1.17.39 h1:dDmktYOD/sU6WjIpitIfuHp7mRbc3izOsyJrQ1c5eOQ=
github.com/luxfi/crypto v1.17.39/go.mod h1:mChLWmW4CLR1wAN6CeJTveCzUv0DTzGQnYgq01x3W0U=
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
github.com/luxfi/crypto v1.17.40 h1:+hDkGKuzYPac2vpbenFN+gKcPvFv3fXQ4ZAEHYDDt/Y=
github.com/luxfi/crypto v1.17.40/go.mod h1:yz/86mgfby+aaf9hsHg8KIhHENueDsZpI9nnV+G4nW8=
github.com/luxfi/database v1.17.38 h1:HsjHoUyQI+vn61HNFyyWo/fw+wkd/I7E8LAdIvmN7BU=
github.com/luxfi/database v1.17.38/go.mod h1:q6BJNN+47VtkmkGrswQmC0ikERYAdlmZCYhGTXkF4mM=
github.com/luxfi/evm v0.8.30 h1:Z3YfZmcmnn8UghmRex3B6gVZ7zblqgEBNeNj7Tcvedw=
github.com/luxfi/evm v0.8.30/go.mod h1:S0fChMeuWV4XqY4OB6RozqvyHcm+imJCKyqsMY713hY=
github.com/luxfi/formatting v1.0.0 h1:0MSuD4Yhv8HBxsabAd/FsZnLnqI5rvbRRMpOIU+sP6o=
github.com/luxfi/formatting v1.0.0/go.mod h1:lswh6dPQ4E9Q+nZLwispV+Jdthr5R1PkYTBA/LhuSgE=
github.com/luxfi/genesis v1.5.21 h1:UU4N4mXB4EdSJavipqde6qD1ysmjAjXV1g0+mF+XoL8=
github.com/luxfi/genesis v1.5.21/go.mod h1:XI1T2oMh/VS/f+LWMWPDQIEhEneCNVHodhn2Qr3rFMU=
github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo=
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
github.com/luxfi/geth v1.16.69 h1:CHO6xTZ+A+3itk94ts4uyVRJajNVP3RxWTjJp5qGOlk=
github.com/luxfi/geth v1.16.69/go.mod h1:8eEO1hW5sa6OH2VeCMaCPnRz28JBxFvCPBCWPLsU2ck=
github.com/luxfi/go-bip32 v1.0.2 h1:7vFbb+Wr4Z499q2tuCLdd7wWjtn8sH+HWBlx76mhH9Y=
@@ -280,12 +282,10 @@ github.com/luxfi/go-bip39 v1.1.2 h1:p+wLMPGs6MLQh7q0YIsmy2EhHL7LHiELEGTJko6t/Jg=
github.com/luxfi/go-bip39 v1.1.2/go.mod h1:96de9VkR2kY/ASAnhMtvt3TSh+PZkAFAngNj0GjRGDo=
github.com/luxfi/ids v1.2.9 h1:+yjdhXW99drnd2Zlp1u/p8k3G23W3/1btJQ4ogHawUI=
github.com/luxfi/ids v1.2.9/go.mod h1:khJOEdOPxd22yn0jcVrnbX1ADa0GHn5Y74gvCzN5BYc=
github.com/luxfi/keychain v1.0.1 h1:8tTzPnZSi0M6UtKRBCypnGFrLNry9BvWfS5ol33yDF0=
github.com/luxfi/keychain v1.0.1/go.mod h1:X7HLk5QRZ1VHJxovAyBL6z7f/nlosoXlveEtvUGhoaw=
github.com/luxfi/keychain v1.0.2 h1:uQgmjs37/VBIALEiYrrszTpxvtqr07/YvS9TnmxGafs=
github.com/luxfi/keychain v1.0.2/go.mod h1:q/4ULgZBlstKkwzOzG/0T6y73BDPgnkrcibbJyTvmbU=
github.com/luxfi/keys v1.0.7 h1:kFuiSQqgStHAN4UDAUt2AVJfcl+zZqFra9q2OfYMZuU=
github.com/luxfi/keys v1.0.7/go.mod h1:Faz87SfMIzVpE+2uIsYQnw+36rRhQNKydd/3mEEXPbg=
github.com/luxfi/log v1.2.1 h1:L2JM8MjGPF8rBnY+EXODS9x5OQTGSgeQ1SPFmeB+oPY=
github.com/luxfi/log v1.2.1/go.mod h1:0vJzb4Hl9fvgwWDMKcbCD/SYH3bO0iSmgZMcYdfdl1o=
github.com/luxfi/math v1.2.3 h1:BgvIFw/srPXFLbcqtoDhLJOfmBsn86GPA1iWgsoyUb4=
github.com/luxfi/math v1.2.3/go.mod h1:C8STnF2H+D6rqBPt248CiWY2TGuJgdtv/+4UqrT15iM=
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
@@ -294,14 +294,16 @@ github.com/luxfi/math/safe v0.0.1 h1:GfSBINV9mOFgHzd32JbgfHSLhlNn0BwnP43rteYEosc
github.com/luxfi/math/safe v0.0.1/go.mod h1:EejrmOJHh03YAD8+Zww8cPcMR1K3Q2I7w1dX4sMloeo=
github.com/luxfi/metric v1.4.10 h1:uWeLupMLxK7YK1c99gY23cT+hornWg7OmZgHNK4HNbw=
github.com/luxfi/metric v1.4.10/go.mod h1:3OIQ+e6mciesm3cik3Q4CSOAvTlBy4JA0ZoKzCB9APo=
github.com/luxfi/mock v0.1.0 h1:IwElfNu+T9sXvzFX6tudPDx1vqPuACRSRdxpD5lxW+o=
github.com/luxfi/mock v0.1.0/go.mod h1:izF+9K0gGzFC9zERn6Po37v46eLdPB+EIsDjL3GLk+U=
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
github.com/luxfi/node v1.22.81 h1:JnPpIwYZcCdu4ehEe6CfRXPOsgJMT3KFG3qfwCfF+D0=
github.com/luxfi/node v1.22.81/go.mod h1:Pe5qlXmverZbrBvYRD8uxtYtIj1huG6/5gnYad9pVBE=
github.com/luxfi/p2p v1.18.7 h1:TIgSSoSdRa0g61CbvAFovWgb3OwWGfOTh4RWi8en/fw=
github.com/luxfi/p2p v1.18.7/go.mod h1:X829+oxzPyY2e5S9CyyMFiOtk6nyuNnxZSaSxGxrpKw=
github.com/luxfi/precompile v0.4.3 h1:zCgBDm+kqAPKJtK8sbxJp5Ak5UlPSOIpkERCTLrrVc4=
github.com/luxfi/precompile v0.4.3/go.mod h1:42j+cmr1cN6cbpNWLFXyWOEKXjg9DOkHN5X+cd8ydIo=
github.com/luxfi/pubsub v1.0.0 h1:mjxA7+qkHVwEyUDazYBt2Kh4GKo+P57PIqzlZB+ezvw=
github.com/luxfi/pubsub v1.0.0/go.mod h1:lCVBPBK/fhD0Ffg57E9R44CEI6Bo1fqb9ISJrBlj4ug=
github.com/luxfi/precompile v0.4.5 h1:oTuWOYS0G3PpkNCzhy0zNLxcbWPEEGHGzQi8L2t5VXQ=
github.com/luxfi/precompile v0.4.5/go.mod h1:k/QpDHGeL1OM9T4Hawx8DMYghohHya3dH6LfkOaBy44=
github.com/luxfi/protocol v0.0.2 h1:TAuXMm1K4VDpG3B3brq1EE9Lb7XlbhQmVBObskgNhmc=
github.com/luxfi/protocol v0.0.2/go.mod h1:Yjn2VRwn5PXim0+JTalAyrVG6YbmuSnYOfeUXSbAsqA=
github.com/luxfi/rpc v1.0.0 h1:G8o89gT+CT3SOj08A5FS5G92fovKrWxs941FmUz28CA=
github.com/luxfi/rpc v1.0.0/go.mod h1:dhxzM5rFqNpEOOHB7+8YBQ9d/V4haa6/id9AQTIIJvQ=
github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
@@ -312,12 +314,16 @@ github.com/luxfi/sdk/api v0.0.2 h1:zpOTBJUAhuQiw+47xkcVrrgxiTxrauYPsSv75Ztvj2I=
github.com/luxfi/sdk/api v0.0.2/go.mod h1:p0fv13IlUKFTx16y//rtpp6D/qBZa8xX9gYkKp6j/9g=
github.com/luxfi/staking v1.1.0 h1:oB1f86WG9duxONe6unISHpUlelnmVg8+BOg4qP83q6Y=
github.com/luxfi/staking v1.1.0/go.mod h1:xwYacmreoKoy31ELT6QPno6b5M8q7bg4zJrmmt524X4=
github.com/luxfi/timer v1.0.1 h1:3lRf3j0L/fA3Ud0j67cm/BW2In1kz+jxd1HqgKyglYY=
github.com/luxfi/tls v1.0.3 h1:rK3nxSAxrUOOSHOZnKChwV4f6UJ+cfOl8KWJXAQx/SI=
github.com/luxfi/tls v1.0.3/go.mod h1:dQqSiGE7YxXUxOwICoReUuIitBms9DYOaCeteBwmIWw=
github.com/luxfi/trace v0.1.4 h1:ttCRyXGwWuz232se+lIUqhWHBoTuvPLhHH/hLWyqtaM=
github.com/luxfi/trace v0.1.4/go.mod h1:Az7HWh+PCuPftXjQu+ssjv51nKauaFu+q2un7bmZYBA=
github.com/luxfi/upgrade v1.0.0 h1:mM6YXvU8VzoclA7IdtuE5bmnMuFquYxjKUUW84LJz54=
github.com/luxfi/upgrade v1.0.0/go.mod h1:DZF7dO4erhUEKf907B2e65k4/vGkCPkUngpvIEUS3eI=
github.com/luxfi/utils v1.1.3 h1:yDEuuJ6bm9H8DzOzzveXI1AZlu/AeSpHNRrY1akbyc0=
github.com/luxfi/utxo v0.0.1 h1:yJTFhenjM2kZmHL6f2SHfWRmDNKeInSuFptrDbGQw60=
github.com/luxfi/utxo v0.0.1/go.mod h1:cwkUv/J6zNohRI7+xrj03wi39bm6CYkek+6XRhQrPTU=
github.com/luxfi/version v1.0.1 h1:T/1KYWEMmsrNQk7pN7PFPAwh/7XbeX7cFAKLBqI37Sk=
github.com/luxfi/version v1.0.1/go.mod h1:Y5fPkQ2DB0XRBCxgSPXp4ISzL1/jptKnmFknShRJCyg=
github.com/luxfi/vm v1.0.16 h1:IOTuMZUAKJ5i40GcWHs9FqhnC4Dd/j2Rq34kdPPYb1U=
@@ -387,6 +393,8 @@ github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQp
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
@@ -394,8 +402,8 @@ github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfx
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posthog/posthog-go v1.6.1 h1:3ZspN1rTqaK3WBWwLr+rAzDMeeolmdGDT3BxzNweFrc=
github.com/posthog/posthog-go v1.6.1/go.mod h1:ZPCind3bz8xDLK0Zhvpv1fQav6WfRcQDqTMfMXmna98=
github.com/posthog/posthog-go v1.8.2 h1:v/ajsM8lq+2Z3OlQbTVWqiHI+hyh9Cd4uiQt1wFlehE=
github.com/posthog/posthog-go v1.8.2/go.mod h1:ueZiJCmHezyDHI/swIR1RmOfktLehnahJnFxEvQ9mnQ=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
@@ -422,9 +430,9 @@ github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
@@ -463,7 +471,7 @@ github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYI
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
@@ -571,6 +579,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+2 -2
View File
@@ -43,8 +43,8 @@ import (
"github.com/luxfi/netrunner/utils"
"github.com/luxfi/protocol/p/txs"
"github.com/luxfi/protocol/p/signer"
"github.com/luxfi/sdk/api/admin"
"github.com/luxfi/sdk/api/platformvm"
"github.com/luxfi/sdk/admin"
"github.com/luxfi/sdk/platformvm"
pwallet "github.com/luxfi/sdk/wallet/chain/p"
pwalletwallet "github.com/luxfi/sdk/wallet/chain/p/wallet"
"github.com/luxfi/utxo/secp256k1fx"
+74
View File
@@ -570,6 +570,80 @@ func NewConfigForNetworkWithCustomGenesis(binaryPath string, numNodes uint32, ge
return netConfig, nil
}
// NewConfigFromExistingSnapshot loads network config from an existing snapshot.
// This is used when resuming from a snapshot to preserve the original staking keys
// and genesis, ensuring NodeIDs match what's in the genesis validator set.
//
// Parameters:
// - binaryPath: Path to the luxd binary
// - rootDataDir: Path to the extracted snapshot (e.g., ~/.lux/runs/mainnet/current)
// - existingGenesis: The genesis JSON string from the snapshot
// - numNodes: Number of nodes to configure
// - portBase: Starting port for HTTP (staking port = HTTP + 1)
//
// The rootDataDir should contain node directories (node1, node2, ...) with:
// - luxtls.key: TLS private key
// - luxtls.crt: TLS certificate
// - signer.key: BLS signer key (optional)
func NewConfigFromExistingSnapshot(binaryPath string, rootDataDir string, existingGenesis string, numNodes uint32, portBase int) (network.Config, error) {
netConfig := NewDefaultConfig(binaryPath)
netConfig.Genesis = existingGenesis
fmt.Printf("📂 Loading config from existing snapshot at %s\n", rootDataDir)
nodeConfigs := make([]node.Config, numNodes)
for i := uint32(0); i < numNodes; i++ {
nodeName := fmt.Sprintf("node%d", i+1)
nodeDir := filepath.Join(rootDataDir, nodeName)
// Load staking key
stakingKeyPath := filepath.Join(nodeDir, "luxtls.key")
stakingKey, err := os.ReadFile(stakingKeyPath)
if err != nil {
return network.Config{}, fmt.Errorf("failed to read staking key for %s from %s: %w", nodeName, stakingKeyPath, err)
}
// Load staking cert
stakingCertPath := filepath.Join(nodeDir, "luxtls.crt")
stakingCert, err := os.ReadFile(stakingCertPath)
if err != nil {
return network.Config{}, fmt.Errorf("failed to read staking cert for %s from %s: %w", nodeName, stakingCertPath, err)
}
// Load BLS signer key (optional - may not exist in older snapshots)
signerKeyPath := filepath.Join(nodeDir, "signer.key")
var signerKeyB64 string
if signerKey, err := os.ReadFile(signerKeyPath); err == nil {
signerKeyB64 = base64.StdEncoding.EncodeToString(signerKey)
}
port := portBase + int(i)*2
nodeConfigs[i] = node.Config{
Flags: map[string]interface{}{
config.HTTPPortKey: port,
config.StakingPortKey: port + 1,
},
StakingKey: string(stakingKey),
StakingCert: string(stakingCert),
StakingSigningKey: signerKeyB64,
IsBeacon: true,
ChainConfigFiles: map[string]string{},
UpgradeConfigFiles: map[string]string{},
PChainConfigFiles: map[string]string{},
}
// Compute and log NodeID for verification
nodeID, err := utils.ToNodeID(stakingKey, stakingCert)
if err == nil {
fmt.Printf(" %s: NodeID=%s (from snapshot)\n", nodeName, nodeID.String())
}
}
netConfig.NodeConfigs = nodeConfigs
fmt.Printf("✅ Loaded %d node configs from existing snapshot\n", numNodes)
return netConfig, nil
}
// NewConfigWithPreExistingKeys creates a network config using pre-existing validator keys.
// This is useful for:
// - Maintaining consistent NodeIDs across network restarts
+16 -16
View File
@@ -5,7 +5,7 @@ package mocks
import (
context "context"
health "github.com/luxfi/sdk/api/health"
apihealth "github.com/luxfi/api/health"
mock "github.com/stretchr/testify/mock"
rpc "github.com/luxfi/rpc"
@@ -17,7 +17,7 @@ type Client struct {
}
// Health provides a mock function with given fields: ctx, tags, options
func (_m *Client) Health(ctx context.Context, tags []string, options ...rpc.Option) (*health.APIReply, error) {
func (_m *Client) Health(ctx context.Context, tags []string, options ...rpc.Option) (*apihealth.APIReply, error) {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
@@ -27,16 +27,16 @@ func (_m *Client) Health(ctx context.Context, tags []string, options ...rpc.Opti
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *health.APIReply
var r0 *apihealth.APIReply
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, []string, ...rpc.Option) (*health.APIReply, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, []string, ...rpc.Option) (*apihealth.APIReply, error)); ok {
return rf(ctx, tags, options...)
}
if rf, ok := ret.Get(0).(func(context.Context, []string, ...rpc.Option) *health.APIReply); ok {
if rf, ok := ret.Get(0).(func(context.Context, []string, ...rpc.Option) *apihealth.APIReply); ok {
r0 = rf(ctx, tags, options...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*health.APIReply)
r0 = ret.Get(0).(*apihealth.APIReply)
}
}
@@ -50,7 +50,7 @@ func (_m *Client) Health(ctx context.Context, tags []string, options ...rpc.Opti
}
// Liveness provides a mock function with given fields: ctx, tags, options
func (_m *Client) Liveness(ctx context.Context, tags []string, options ...rpc.Option) (*health.APIReply, error) {
func (_m *Client) Liveness(ctx context.Context, tags []string, options ...rpc.Option) (*apihealth.APIReply, error) {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
@@ -60,16 +60,16 @@ func (_m *Client) Liveness(ctx context.Context, tags []string, options ...rpc.Op
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *health.APIReply
var r0 *apihealth.APIReply
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, []string, ...rpc.Option) (*health.APIReply, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, []string, ...rpc.Option) (*apihealth.APIReply, error)); ok {
return rf(ctx, tags, options...)
}
if rf, ok := ret.Get(0).(func(context.Context, []string, ...rpc.Option) *health.APIReply); ok {
if rf, ok := ret.Get(0).(func(context.Context, []string, ...rpc.Option) *apihealth.APIReply); ok {
r0 = rf(ctx, tags, options...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*health.APIReply)
r0 = ret.Get(0).(*apihealth.APIReply)
}
}
@@ -83,7 +83,7 @@ func (_m *Client) Liveness(ctx context.Context, tags []string, options ...rpc.Op
}
// Readiness provides a mock function with given fields: ctx, tags, options
func (_m *Client) Readiness(ctx context.Context, tags []string, options ...rpc.Option) (*health.APIReply, error) {
func (_m *Client) Readiness(ctx context.Context, tags []string, options ...rpc.Option) (*apihealth.APIReply, error) {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
@@ -93,16 +93,16 @@ func (_m *Client) Readiness(ctx context.Context, tags []string, options ...rpc.O
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *health.APIReply
var r0 *apihealth.APIReply
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, []string, ...rpc.Option) (*health.APIReply, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, []string, ...rpc.Option) (*apihealth.APIReply, error)); ok {
return rf(ctx, tags, options...)
}
if rf, ok := ret.Get(0).(func(context.Context, []string, ...rpc.Option) *health.APIReply); ok {
if rf, ok := ret.Get(0).(func(context.Context, []string, ...rpc.Option) *apihealth.APIReply); ok {
r0 = rf(ctx, tags, options...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*health.APIReply)
r0 = ret.Get(0).(*apihealth.APIReply)
}
}
+27 -5
View File
@@ -15,6 +15,7 @@ import (
"os/user"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"time"
@@ -752,7 +753,7 @@ func (ln *localNetwork) addNode(nodeConfig node.Config) (node.Node, error) {
// so this node won't try to use itself as a beacon.
if !isPausedNode && nodeConfig.IsBeacon {
err = ln.bootstraps.Add(beacon.New(nodeID, netip.AddrPortFrom(
netip.IPv6Loopback(),
netip.MustParseAddr("127.0.0.1"),
nodeData.p2pPort,
)))
}
@@ -800,14 +801,22 @@ func (ln *localNetwork) healthy(ctx context.Context) error {
errGr.Go(func() error {
// Every [healthCheckFreq], query node for health status.
// Do this until ctx timeout or network closed.
retryCount := 0
ln.logger.Info("health check goroutine started", log.String("node", nodeName))
for {
if node.Status() != status.Running {
retryCount++
ln.logger.Info("health check iteration", log.String("node", nodeName), log.Int("retry", retryCount))
nodeStatus := node.Status()
if nodeStatus != status.Running {
// If we had stopped this node ourselves, it wouldn't be in [ln.nodes].
// Since it is, it means the node stopped unexpectedly.
return fmt.Errorf("node %q stopped unexpectedly", nodeName)
ln.logger.Warn("node not running during health check", log.String("node", nodeName), log.String("status", string(nodeStatus)))
return fmt.Errorf("node %q stopped unexpectedly (status=%s)", nodeName, nodeStatus)
}
healthClient := node.client.HealthAPI()
if healthClient == nil {
ln.logger.Error("health client is nil", log.String("node", nodeName))
return fmt.Errorf("health client is nil for node %v", nodeName)
}
// Use Readiness instead of Health for local testnets
@@ -815,13 +824,20 @@ func (ln *localNetwork) healthy(ctx context.Context) error {
// which is expected behavior for isolated test networks
health, err := healthClient.Readiness(ctx, nil)
if err == nil && health.Healthy {
ln.logger.Debug("node became ready", log.String("name", nodeName))
ln.logger.Info("node became ready", log.String("name", nodeName))
return nil
}
if err != nil {
ln.logger.Info("health check error (will retry)", log.String("node", nodeName), log.Err(err))
} else {
ln.logger.Info("health check unhealthy (will retry)", log.String("node", nodeName), log.Bool("healthy", health.Healthy))
}
select {
case <-ctx.Done():
return fmt.Errorf("node %q failed to become healthy within timeout, or network stopped", nodeName)
ln.logger.Warn("context done during health check - RETURNING ERROR", log.String("node", nodeName), log.Int("retries", retryCount), log.Err(ctx.Err()))
return fmt.Errorf("node %q failed to become healthy within timeout, or network stopped (retries=%d, ctxErr=%v)", nodeName, retryCount, ctx.Err())
case <-time.After(healthCheckFreq):
ln.logger.Info("will retry health check", log.String("node", nodeName))
}
}
})
@@ -875,6 +891,12 @@ func (ln *localNetwork) GetAllNodes() (map[string]node.Node, error) {
}
func (ln *localNetwork) Stop(ctx context.Context) error {
ln.logger.Info("Stop() called, capturing stack trace")
// Log stack trace to understand who's calling Stop
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
ln.logger.Info("Stop() stack trace", log.String("stack", string(buf[:n])))
err := network.ErrStopped
ln.stopOnce.Do(
func() {
+8 -4
View File
@@ -13,6 +13,7 @@ import (
"testing"
"time"
apihealth "github.com/luxfi/api/health"
"github.com/luxfi/config"
"github.com/luxfi/ids"
log "github.com/luxfi/log"
@@ -27,7 +28,6 @@ import (
"github.com/luxfi/p2p/message"
"github.com/luxfi/p2p/peer"
"github.com/luxfi/rpc"
"github.com/luxfi/sdk/api/health"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
@@ -98,7 +98,7 @@ func (*localTestFlagCheckProcessCreator) GetNodeVersion(_ node.Config) (string,
// TODO have this method return an API Client that has all
// APIs and methods implemented
func newMockAPISuccessful(string, uint16) api.Client {
healthReply := &health.APIReply{Healthy: true}
healthReply := &apihealth.APIReply{Healthy: true}
healthClient := &healthmocks.Client{}
// Mock Readiness (used by local network health check)
healthClient.On("Readiness", mock.Anything, mock.Anything).Return(healthReply, nil)
@@ -113,7 +113,7 @@ func newMockAPISuccessful(string, uint16) api.Client {
// Returns an API client where the Health API's Readiness method always returns unhealthy
func newMockAPIUnhealthy(string, uint16) api.Client {
healthReply := &health.APIReply{Healthy: false}
healthReply := &apihealth.APIReply{Healthy: false}
healthClient := &healthmocks.Client{}
// Mock Readiness (used by local network health check)
healthClient.On("Readiness", mock.Anything, mock.Anything).Return(healthReply, nil)
@@ -528,6 +528,10 @@ func TestGenerateDefaultNetwork(t *testing.T) {
require := require.New(t)
binaryPath := "pepito"
networkConfig := NewDefaultConfig(binaryPath)
for i := range networkConfig.NodeConfigs {
delete(networkConfig.NodeConfigs[i].Flags, config.HTTPPortKey)
delete(networkConfig.NodeConfigs[i].Flags, config.StakingPortKey)
}
// Use reassignPortsIfUsed=true to avoid port conflicts in CI/parallel tests
net, err := newNetwork(log.NoLog{}, newMockAPISuccessful, &localTestSuccessfulNodeProcessCreator{}, "", "", true)
require.NoError(err)
@@ -1307,7 +1311,7 @@ func newMockAPIHealthyBlocks(string, uint16) api.Client {
healthClient.On("Readiness", mock.MatchedBy(func(_ context.Context) bool {
return true
}), mock.Anything, mock.Anything).Return(
func(ctx context.Context, _ []string, _ ...rpc.Option) *health.APIReply {
func(ctx context.Context, _ []string, _ ...rpc.Option) *apihealth.APIReply {
<-ctx.Done()
return nil
},
+473 -150
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
@@ -14,6 +15,7 @@ import (
"maps"
"slices"
"github.com/klauspost/compress/zstd"
luxconfig "github.com/luxfi/config"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
@@ -24,8 +26,7 @@ import (
"github.com/luxfi/netrunner/utils"
"github.com/luxfi/config"
"github.com/luxfi/sdk/api/admin"
dircopy "github.com/otiai10/copy"
"github.com/luxfi/sdk/admin"
)
// NetworkState defines dynamic network information not available on chain db
@@ -35,26 +36,39 @@ type NetworkState struct {
}
const (
hotSnapshotManifestName = "hot_snapshot.json"
hotSnapshotVersion = 1
// Manifest file name for native backup snapshots
manifestFileName = "manifest.json"
// Current manifest version
manifestVersion = 2
)
type hotSnapshotManifest struct {
Version int `json:"version"`
Network string `json:"network"`
Nodes map[string]hotSnapshotNode `json:"nodes"`
}
type hotSnapshotNode struct {
LastVersion uint64 `json:"last_version"`
Backups []hotSnapshotBackup `json:"backups"`
}
type hotSnapshotBackup struct {
Since uint64 `json:"since"`
Version uint64 `json:"version"`
Path string `json:"path"`
// snapshotManifest stores metadata for native database backup snapshots.
// Supports incremental backups with version tracking.
type snapshotManifest struct {
// Manifest format version
Version int `json:"version"`
// Network name (e.g., "mainnet", "testnet", "local")
Network string `json:"network"`
// Unix timestamp when snapshot was created
Timestamp int64 `json:"timestamp"`
// Human-readable timestamp
CreatedAt string `json:"created_at"`
// Per-node backup metadata
Nodes map[string]nodeBackupInfo `json:"nodes"`
}
// nodeBackupInfo stores backup metadata for a single node.
type nodeBackupInfo struct {
// Database version after backup
DBVersion uint64 `json:"db_version"`
// Version this backup is incremental from (0 = full backup)
IncrementalFrom uint64 `json:"incremental_from"`
// Path to compressed backup file (relative to snapshot dir)
BackupFile string `json:"backup_file"`
// Size of compressed backup in bytes
CompressedSize int64 `json:"compressed_size"`
// Temp path during backup (not serialized)
tmpPath string `json:"-"`
}
// NewNetwork returns a new network from the given snapshot
@@ -100,8 +114,9 @@ func NewNetworkFromSnapshot(
return net, err
}
// Save network snapshot
// Network is stopped in order to do a safe preservation
// SaveSnapshot saves a network snapshot using native database backup.
// Network is stopped to ensure consistent backup.
// Supports incremental backups when a previous snapshot exists.
func (ln *localNetwork) SaveSnapshot(ctx context.Context, snapshotName string) (string, error) {
ln.lock.Lock()
defer ln.lock.Unlock()
@@ -112,30 +127,39 @@ func (ln *localNetwork) SaveSnapshot(ctx context.Context, snapshotName string) (
if len(snapshotName) == 0 {
return "", fmt.Errorf("invalid snapshotName %q", snapshotName)
}
// check if snapshot already exists
snapshotDir := filepath.Join(ln.snapshotsDir, snapshotPrefix+snapshotName)
networkName := constants.NetworkName(ln.networkID)
// Check for existing snapshot to enable incremental backup
var previousManifest *snapshotManifest
if _, err := os.Stat(snapshotDir); err == nil {
return "", fmt.Errorf("snapshot %q already exists", snapshotName)
manifestPath := filepath.Join(snapshotDir, manifestFileName)
previousManifest, err = loadManifest(manifestPath)
if err != nil {
return "", fmt.Errorf("snapshot %q exists but manifest is invalid: %w", snapshotName, err)
}
if previousManifest.Network != networkName {
return "", fmt.Errorf("snapshot network mismatch: got %q want %q", previousManifest.Network, networkName)
}
}
// keep copy of node info that will be removed by stop
// Collect node info before stopping
nodesConfig := map[string]node.Config{}
nodesDBDir := map[string]string{}
for nodeName, node := range ln.nodes {
nodeConfig := node.config
// depending on how the user generated the config, different nodes config flags
// may point to the same map, so we made a copy to avoid always modifying the same value
nodeConfig.Flags = maps.Clone(nodeConfig.Flags)
nodesConfig[nodeName] = nodeConfig
nodesDBDir[nodeName] = node.GetDbDir()
}
// we change nodeConfig.Flags so as to preserve in snapshot the current node ports
// Preserve current node ports
for nodeName, nodeConfig := range nodesConfig {
nodeConfig.Flags[config.HTTPPortKey] = ln.nodes[nodeName].GetAPIPort()
nodeConfig.Flags[config.StakingPortKey] = ln.nodes[nodeName].GetP2PPort()
}
// make copy of network flags
// Make copy of network flags, remove data/log dir references
networkConfigFlags := maps.Clone(ln.flags)
// remove all data dir, log dir references
delete(networkConfigFlags, config.DataDirKey)
delete(networkConfigFlags, config.LogsDirKey)
for nodeName, nodeConfig := range nodesConfig {
@@ -151,29 +175,69 @@ func (ln *localNetwork) SaveSnapshot(ctx context.Context, snapshotName string) (
nodesConfig[nodeName] = nodeConfig
}
// stop network to safely save snapshot
// Request backup from each node while still running
nodeBackups := make(map[string]nodeBackupInfo)
for _, nodeConfig := range nodesConfig {
localNode, ok := ln.nodes[nodeConfig.Name]
if !ok {
return "", fmt.Errorf("node %q not found for snapshot", nodeConfig.Name)
}
adminClient := localNode.GetAPIClient().AdminAPI()
if adminClient == nil {
return "", fmt.Errorf("admin API client is nil for node %q", nodeConfig.Name)
}
// Determine incremental base version
var since uint64
if previousManifest != nil {
if prevNode, ok := previousManifest.Nodes[nodeConfig.Name]; ok {
since = prevNode.DBVersion
}
}
// Create temp file for backup
tmpBackupPath := filepath.Join(os.TempDir(), fmt.Sprintf("lux-backup-%s-%d.tmp", nodeConfig.Name, time.Now().UnixNano()))
defer os.Remove(tmpBackupPath)
// Request native backup via admin API
version, err := requestAdminSnapshot(ctx, adminClient, tmpBackupPath, since)
if err != nil {
return "", fmt.Errorf("failed to backup node %q: %w", nodeConfig.Name, err)
}
nodeBackups[nodeConfig.Name] = nodeBackupInfo{
DBVersion: version,
IncrementalFrom: since,
BackupFile: fmt.Sprintf("%s.backup.zst", nodeConfig.Name),
tmpPath: tmpBackupPath,
}
}
// Stop network for consistent state
if err := ln.stop(ctx); err != nil {
return "", err
}
syncFilesystem()
// create main snapshot dirs
snapshotDBDir := filepath.Join(snapshotDir, defaultDBSubdir)
if err := os.MkdirAll(snapshotDBDir, os.ModePerm); err != nil {
// Create snapshot directory
if err := os.MkdirAll(snapshotDir, os.ModePerm); err != nil {
return "", err
}
// save db
for _, nodeConfig := range nodesConfig {
sourceDBDir, ok := nodesDBDir[nodeConfig.Name]
if !ok {
return "", fmt.Errorf("failure obtaining db path for node %q", nodeConfig.Name)
}
sourceDBDir = filepath.Join(sourceDBDir, constants.NetworkName(ln.networkID))
targetDBDir := filepath.Join(filepath.Join(snapshotDBDir, nodeConfig.Name), constants.NetworkName(ln.networkID))
if err := dircopy.Copy(sourceDBDir, targetDBDir); err != nil {
return "", fmt.Errorf("failure saving node %q db dir: %w", nodeConfig.Name, err)
// Compress and write backup files
for nodeName, backupInfo := range nodeBackups {
destPath := filepath.Join(snapshotDir, backupInfo.BackupFile)
size, err := compressFile(backupInfo.tmpPath, destPath)
if err != nil {
return "", fmt.Errorf("failed to compress backup for node %q: %w", nodeName, err)
}
backupInfo.CompressedSize = size
backupInfo.tmpPath = "" // Clear temp path
nodeBackups[nodeName] = backupInfo
}
// save network conf
// Save network config
networkConfig := network.Config{
Genesis: string(ln.genesis),
Flags: networkConfigFlags,
@@ -184,8 +248,6 @@ func (ln *localNetwork) SaveSnapshot(ctx context.Context, snapshotName string) (
UpgradeConfigFiles: ln.upgradeConfigFiles,
PChainConfigFiles: ln.pChainConfigFiles,
}
// no need to save this, will be generated automatically on snapshot load
networkConfig.NodeConfigs = append(networkConfig.NodeConfigs, slices.Collect(maps.Values(nodesConfig))...)
networkConfigJSON, err := json.MarshalIndent(networkConfig, "", " ")
if err != nil {
@@ -194,7 +256,8 @@ func (ln *localNetwork) SaveSnapshot(ctx context.Context, snapshotName string) (
if err := createFileAndWrite(filepath.Join(snapshotDir, "network.json"), networkConfigJSON); err != nil {
return "", err
}
// save dynamic part of network not available on blockchain
// Save network state
chainID2ElasticChainID := map[string]string{}
for chainID, elasticChainID := range ln.chainID2ElasticChainID {
chainID2ElasticChainID[chainID.String()] = elasticChainID.String()
@@ -209,10 +272,29 @@ func (ln *localNetwork) SaveSnapshot(ctx context.Context, snapshotName string) (
if err := createFileAndWrite(filepath.Join(snapshotDir, "state.json"), networkStateJSON); err != nil {
return "", err
}
// Save manifest
manifest := &snapshotManifest{
Version: manifestVersion,
Network: networkName,
Timestamp: time.Now().Unix(),
CreatedAt: time.Now().UTC().Format(time.RFC3339),
Nodes: nodeBackups,
}
if err := saveManifest(filepath.Join(snapshotDir, manifestFileName), manifest); err != nil {
return "", err
}
ln.logger.Info("Snapshot saved",
log.String("snapshot", snapshotName),
log.String("path", snapshotDir),
log.Int("nodes", len(nodeBackups)),
)
return snapshotDir, nil
}
// start network from snapshot
// loadSnapshot restores network from a snapshot using native database restore.
func (ln *localNetwork) loadSnapshot(
ctx context.Context,
snapshotName string,
@@ -227,16 +309,22 @@ func (ln *localNetwork) loadSnapshot(
defer ln.lock.Unlock()
snapshotDir := filepath.Join(ln.snapshotsDir, snapshotPrefix+snapshotName)
snapshotDBDir := filepath.Join(snapshotDir, defaultDBSubdir)
_, err := os.Stat(snapshotDir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return ErrSnapshotNotFound
} else {
return fmt.Errorf("failure accessing snapshot %q: %w", snapshotName, err)
}
return fmt.Errorf("failure accessing snapshot %q: %w", snapshotName, err)
}
// load network config
// Load manifest to determine snapshot format
manifestPath := filepath.Join(snapshotDir, manifestFileName)
manifest, err := loadManifest(manifestPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to load manifest: %w", err)
}
// Load network config
networkConfigJSON, err := os.ReadFile(filepath.Join(snapshotDir, "network.json"))
if err != nil {
return fmt.Errorf("failure reading network config file from snapshot: %w", err)
@@ -245,19 +333,18 @@ func (ln *localNetwork) loadSnapshot(
if err := json.Unmarshal(networkConfigJSON, &networkConfig); err != nil {
return fmt.Errorf("failure unmarshaling network config from snapshot: %w", err)
}
// add flags
// Add flags
for i := range networkConfig.NodeConfigs {
for k, v := range flags {
networkConfig.NodeConfigs[i].Flags[k] = v
}
}
hotManifestPath := filepath.Join(snapshotDir, hotSnapshotManifestName)
hotManifest, err := loadHotSnapshotManifest(hotManifestPath)
if err != nil {
return err
}
if hotManifest != nil {
if err := ln.prepareHotSnapshotDirs(networkConfig.NodeConfigs); err != nil {
// Prepare DB directories and restore backups
if manifest != nil {
// Native backup format - decompress and prepare for admin.load
if err := ln.prepareNativeBackupDirs(networkConfig.NodeConfigs); err != nil {
return err
}
for i := range networkConfig.NodeConfigs {
@@ -265,23 +352,42 @@ func (ln *localNetwork) loadSnapshot(
networkConfig.NodeConfigs[i].Flags[config.DBPathKey] = targetDBDir
}
} else {
// load db from full directory copies
for _, nodeConfig := range networkConfig.NodeConfigs {
sourceDBDir := filepath.Join(snapshotDBDir, nodeConfig.Name)
targetDBDir := filepath.Join(filepath.Join(ln.rootDir, nodeConfig.Name), defaultDBSubdir)
if err := dircopy.Copy(sourceDBDir, targetDBDir); err != nil {
return fmt.Errorf("failure loading node %q db dir: %w", nodeConfig.Name, err)
// Legacy format - check for old hot snapshot manifest
hotManifestPath := filepath.Join(snapshotDir, "hot_snapshot.json")
hotManifest, err := loadHotSnapshotManifest(hotManifestPath)
if err != nil {
return err
}
if hotManifest != nil {
if err := ln.prepareHotSnapshotDirs(networkConfig.NodeConfigs); err != nil {
return err
}
for i := range networkConfig.NodeConfigs {
targetDBDir := filepath.Join(filepath.Join(ln.rootDir, networkConfig.NodeConfigs[i].Name), defaultDBSubdir)
networkConfig.NodeConfigs[i].Flags[config.DBPathKey] = targetDBDir
}
} else {
// Legacy directory copy format
snapshotDBDir := filepath.Join(snapshotDir, defaultDBSubdir)
for _, nodeConfig := range networkConfig.NodeConfigs {
sourceDBDir := filepath.Join(snapshotDBDir, nodeConfig.Name)
targetDBDir := filepath.Join(filepath.Join(ln.rootDir, nodeConfig.Name), defaultDBSubdir)
if err := copyDir(sourceDBDir, targetDBDir); err != nil {
return fmt.Errorf("failure loading node %q db dir: %w", nodeConfig.Name, err)
}
nodeConfig.Flags[config.DBPathKey] = targetDBDir
}
nodeConfig.Flags[config.DBPathKey] = targetDBDir
}
}
// replace binary path
// Replace binary path
if binaryPath != "" {
for i := range networkConfig.NodeConfigs {
networkConfig.NodeConfigs[i].BinaryPath = binaryPath
}
}
// set plugin dir
// Set plugin dir
resolvedPluginDir := pluginDir
if resolvedPluginDir == "" {
resolvedPluginDir = luxconfig.ResolvePluginDir()
@@ -289,7 +395,8 @@ func (ln *localNetwork) loadSnapshot(
for i := range networkConfig.NodeConfigs {
networkConfig.NodeConfigs[i].Flags[config.PluginDirKey] = resolvedPluginDir
}
// add chain configs and upgrade configs
// Add chain configs and upgrade configs
for i := range networkConfig.NodeConfigs {
if networkConfig.NodeConfigs[i].ChainConfigFiles == nil {
networkConfig.NodeConfigs[i].ChainConfigFiles = map[string]string{}
@@ -297,20 +404,15 @@ func (ln *localNetwork) loadSnapshot(
if networkConfig.NodeConfigs[i].UpgradeConfigFiles == nil {
networkConfig.NodeConfigs[i].UpgradeConfigFiles = map[string]string{}
}
if networkConfig.NodeConfigs[i].ChainConfigFiles == nil {
networkConfig.NodeConfigs[i].ChainConfigFiles = map[string]string{}
}
for k, v := range chainConfigs {
networkConfig.NodeConfigs[i].ChainConfigFiles[k] = v
}
for k, v := range upgradeConfigs {
networkConfig.NodeConfigs[i].UpgradeConfigFiles[k] = v
}
for k, v := range chainConfigs {
networkConfig.NodeConfigs[i].ChainConfigFiles[k] = v
}
}
// load network state not available at blockchain db
// Load network state
networkStateJSON, err := os.ReadFile(filepath.Join(snapshotDir, "state.json"))
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
@@ -335,23 +437,35 @@ func (ln *localNetwork) loadSnapshot(
ln.chainID2ElasticChainID[chainID] = elasticChainID
}
}
// Load network config (starts nodes)
if err := ln.loadConfig(ctx, networkConfig); err != nil {
return err
}
if hotManifest != nil {
if err := ln.applyHotSnapshot(ctx, snapshotDir, networkConfig.NodeConfigs, hotManifest); err != nil {
// Apply native backups after nodes start
if manifest != nil {
if err := ln.applyNativeBackups(ctx, snapshotDir, networkConfig.NodeConfigs, manifest); err != nil {
return err
}
} else {
// Check for legacy hot snapshot
hotManifestPath := filepath.Join(snapshotDir, "hot_snapshot.json")
hotManifest, _ := loadHotSnapshotManifest(hotManifestPath)
if hotManifest != nil {
if err := ln.applyHotSnapshot(ctx, snapshotDir, networkConfig.NodeConfigs, hotManifest); err != nil {
return err
}
}
}
return nil
}
// SaveHotSnapshot saves a snapshot without stopping the network.
// Uses the admin snapshot API to stream a consistent backup while nodes continue serving.
// Uses native database backup API for consistent snapshots.
func (ln *localNetwork) SaveHotSnapshot(ctx context.Context, snapshotName string) (string, error) {
ln.lock.RLock() // Read lock - doesn't block network operations
ln.lock.RLock()
defer ln.lock.RUnlock()
if ln.stopCalled() {
@@ -360,18 +474,24 @@ func (ln *localNetwork) SaveHotSnapshot(ctx context.Context, snapshotName string
if len(snapshotName) == 0 {
return "", fmt.Errorf("invalid snapshotName %q", snapshotName)
}
snapshotDir := filepath.Join(ln.snapshotsDir, snapshotPrefix+snapshotName)
networkName := constants.NetworkName(ln.networkID)
snapshotExists := true
if _, err := os.Stat(snapshotDir); err != nil {
if errors.Is(err, os.ErrNotExist) {
snapshotExists = false
} else {
return "", fmt.Errorf("failed to access snapshot dir: %w", err)
// Check for existing snapshot
var previousManifest *snapshotManifest
if _, err := os.Stat(snapshotDir); err == nil {
manifestPath := filepath.Join(snapshotDir, manifestFileName)
previousManifest, err = loadManifest(manifestPath)
if err != nil {
return "", fmt.Errorf("snapshot %q exists but manifest is invalid: %w", snapshotName, err)
}
if previousManifest.Network != networkName {
return "", fmt.Errorf("snapshot network mismatch: got %q want %q", previousManifest.Network, networkName)
}
}
// Collect node info (read-only, safe during operation)
// Collect node info
nodesConfig := map[string]node.Config{}
for nodeName, node := range ln.nodes {
nodeConfig := node.config
@@ -402,30 +522,11 @@ func (ln *localNetwork) SaveHotSnapshot(ctx context.Context, snapshotName string
nodesConfig[nodeName] = nodeConfig
}
// Create main snapshot dirs
snapshotDBDir := filepath.Join(snapshotDir, defaultDBSubdir)
if err := os.MkdirAll(snapshotDBDir, os.ModePerm); err != nil {
// Create snapshot directory
if err := os.MkdirAll(snapshotDir, os.ModePerm); err != nil {
return "", err
}
manifestPath := filepath.Join(snapshotDir, hotSnapshotManifestName)
manifest, err := loadHotSnapshotManifest(manifestPath)
if err != nil {
return "", err
}
if manifest == nil {
if snapshotExists {
return "", fmt.Errorf("snapshot %q exists without hot manifest; choose a new name or remove it", snapshotName)
}
manifest = &hotSnapshotManifest{
Version: hotSnapshotVersion,
Network: networkName,
Nodes: map[string]hotSnapshotNode{},
}
} else if manifest.Network != networkName {
return "", fmt.Errorf("hot snapshot network mismatch: got %q want %q", manifest.Network, networkName)
}
// Save network config
networkConfig := network.Config{
Genesis: string(ln.genesis),
@@ -446,7 +547,7 @@ func (ln *localNetwork) SaveHotSnapshot(ctx context.Context, snapshotName string
return "", err
}
// Save dynamic network state
// Save network state
chainID2ElasticChainID := map[string]string{}
for chainID, elasticChainID := range ln.chainID2ElasticChainID {
chainID2ElasticChainID[chainID.String()] = elasticChainID.String()
@@ -462,63 +563,84 @@ func (ln *localNetwork) SaveHotSnapshot(ctx context.Context, snapshotName string
return "", err
}
// Request backup from each node and compress
nodeBackups := make(map[string]nodeBackupInfo)
for _, nodeConfig := range nodesConfig {
localNode, ok := ln.nodes[nodeConfig.Name]
if !ok {
return "", fmt.Errorf("node %q not found for hot snapshot", nodeConfig.Name)
}
adminClient := localNode.GetAPIClient().AdminAPI()
if adminClient == nil {
return "", fmt.Errorf("admin API client is nil for node %q", nodeConfig.Name)
}
nodeManifest := manifest.Nodes[nodeConfig.Name]
since := nodeManifest.LastVersion
nodeSnapshotDir := filepath.Join(snapshotDBDir, nodeConfig.Name, networkName, "backups")
if err := os.MkdirAll(nodeSnapshotDir, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create backup dir for node %q: %w", nodeConfig.Name, err)
// Determine incremental base version
var since uint64
if previousManifest != nil {
if prevNode, ok := previousManifest.Nodes[nodeConfig.Name]; ok {
since = prevNode.DBVersion
}
}
timestamp := time.Now().UTC().Format("20060102T150405Z")
backupFile := fmt.Sprintf("backup_since_%d_%s.bak", since, timestamp)
backupPath := filepath.Join(nodeSnapshotDir, backupFile)
// Create temp file for backup
tmpBackupPath := filepath.Join(os.TempDir(), fmt.Sprintf("lux-backup-%s-%d.tmp", nodeConfig.Name, time.Now().UnixNano()))
version, err := requestAdminSnapshot(ctx, adminClient, backupPath, since)
// Request native backup via admin API
version, err := requestAdminSnapshot(ctx, adminClient, tmpBackupPath, since)
if err != nil {
_ = os.Remove(backupPath)
return "", fmt.Errorf("failed to snapshot node %q: %w", nodeConfig.Name, err)
os.Remove(tmpBackupPath)
return "", fmt.Errorf("failed to backup node %q: %w", nodeConfig.Name, err)
}
relativePath := filepath.ToSlash(filepath.Join(defaultDBSubdir, nodeConfig.Name, networkName, "backups", backupFile))
nodeManifest.Backups = append(nodeManifest.Backups, hotSnapshotBackup{
Since: since,
Version: version,
Path: relativePath,
CreatedAt: time.Now().UTC().Format(time.RFC3339),
})
nodeManifest.LastVersion = version
manifest.Nodes[nodeConfig.Name] = nodeManifest
// Compress backup
backupFileName := fmt.Sprintf("%s.backup.zst", nodeConfig.Name)
destPath := filepath.Join(snapshotDir, backupFileName)
size, err := compressFile(tmpBackupPath, destPath)
os.Remove(tmpBackupPath)
if err != nil {
return "", fmt.Errorf("failed to compress backup for node %q: %w", nodeConfig.Name, err)
}
nodeBackups[nodeConfig.Name] = nodeBackupInfo{
DBVersion: version,
IncrementalFrom: since,
BackupFile: backupFileName,
CompressedSize: size,
}
}
if err := saveHotSnapshotManifest(manifestPath, manifest); err != nil {
// Save manifest
manifest := &snapshotManifest{
Version: manifestVersion,
Network: networkName,
Timestamp: time.Now().Unix(),
CreatedAt: time.Now().UTC().Format(time.RFC3339),
Nodes: nodeBackups,
}
if err := saveManifest(filepath.Join(snapshotDir, manifestFileName), manifest); err != nil {
return "", err
}
ln.logger.Info("Hot snapshot saved", log.String("snapshot", snapshotName), log.String("path", snapshotDir))
ln.logger.Info("Hot snapshot saved",
log.String("snapshot", snapshotName),
log.String("path", snapshotDir),
log.Int("nodes", len(nodeBackups)),
)
return snapshotDir, nil
}
// Remove network snapshot
// RemoveSnapshot removes a network snapshot
func (ln *localNetwork) RemoveSnapshot(snapshotName string) error {
snapshotDir := filepath.Join(ln.snapshotsDir, snapshotPrefix+snapshotName)
_, err := os.Stat(snapshotDir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return ErrSnapshotNotFound
} else {
return fmt.Errorf("failure accessing snapshot %q: %w", snapshotName, err)
}
return fmt.Errorf("failure accessing snapshot %q: %w", snapshotName, err)
}
if err := os.RemoveAll(snapshotDir); err != nil {
return fmt.Errorf("failure removing snapshot path %q: %w", snapshotDir, err)
@@ -526,15 +648,14 @@ func (ln *localNetwork) RemoveSnapshot(snapshotName string) error {
return nil
}
// Get network snapshots
// GetSnapshotNames returns all snapshot names
func (ln *localNetwork) GetSnapshotNames() ([]string, error) {
_, err := os.Stat(ln.snapshotsDir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("snapshots dir %q does not exists", ln.snapshotsDir)
} else {
return nil, fmt.Errorf("failure accessing snapshots dir %q: %w", ln.snapshotsDir, err)
}
return nil, fmt.Errorf("failure accessing snapshots dir %q: %w", ln.snapshotsDir, err)
}
matches, err := filepath.Glob(filepath.Join(ln.snapshotsDir, snapshotPrefix+"*"))
if err != nil {
@@ -547,6 +668,215 @@ func (ln *localNetwork) GetSnapshotNames() ([]string, error) {
return snapshots, nil
}
// prepareNativeBackupDirs creates empty DB directories for native backup restore
func (ln *localNetwork) prepareNativeBackupDirs(nodeConfigs []node.Config) error {
for _, nodeConfig := range nodeConfigs {
targetDBDir := filepath.Join(ln.rootDir, nodeConfig.Name, defaultDBSubdir)
if err := os.RemoveAll(targetDBDir); err != nil {
return fmt.Errorf("failed to clear db dir for node %q: %w", nodeConfig.Name, err)
}
if err := os.MkdirAll(targetDBDir, 0o750); err != nil {
return fmt.Errorf("failed to create db dir for node %q: %w", nodeConfig.Name, err)
}
}
return nil
}
// applyNativeBackups decompresses and loads native database backups
func (ln *localNetwork) applyNativeBackups(
ctx context.Context,
snapshotDir string,
nodeConfigs []node.Config,
manifest *snapshotManifest,
) error {
for _, nodeConfig := range nodeConfigs {
nodeName := nodeConfig.Name
nodeManifest, ok := manifest.Nodes[nodeName]
if !ok {
return fmt.Errorf("missing backup metadata for node %q", nodeName)
}
localNode, ok := ln.nodes[nodeName]
if !ok {
return fmt.Errorf("node %q not found while applying backup", nodeName)
}
adminClient := localNode.GetAPIClient().AdminAPI()
if adminClient == nil {
return fmt.Errorf("admin API client is nil for node %q", nodeName)
}
// Decompress backup to temp file
compressedPath := filepath.Join(snapshotDir, nodeManifest.BackupFile)
tmpPath := filepath.Join(os.TempDir(), fmt.Sprintf("lux-restore-%s-%d.tmp", nodeName, time.Now().UnixNano()))
if err := decompressFile(compressedPath, tmpPath); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("failed to decompress backup for node %q: %w", nodeName, err)
}
// Load backup via admin API
if err := requestAdminLoad(ctx, adminClient, tmpPath); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("failed to load backup for node %q: %w", nodeName, err)
}
os.Remove(tmpPath)
}
return nil
}
// loadManifest loads a snapshot manifest from disk
func loadManifest(path string) (*snapshotManifest, error) {
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, os.ErrNotExist
}
return nil, fmt.Errorf("failed to read manifest: %w", err)
}
manifest := &snapshotManifest{}
if err := json.Unmarshal(data, manifest); err != nil {
return nil, fmt.Errorf("failed to parse manifest: %w", err)
}
if manifest.Version > manifestVersion {
return nil, fmt.Errorf("unsupported manifest version %d (max supported: %d)", manifest.Version, manifestVersion)
}
if manifest.Nodes == nil {
manifest.Nodes = map[string]nodeBackupInfo{}
}
return manifest, nil
}
// saveManifest writes a snapshot manifest to disk
func saveManifest(path string, manifest *snapshotManifest) error {
data, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal manifest: %w", err)
}
return createFileAndWrite(path, data)
}
// compressFile compresses src to dst using zstd, returns compressed size
func compressFile(src, dst string) (int64, error) {
srcFile, err := os.Open(src)
if err != nil {
return 0, fmt.Errorf("failed to open source: %w", err)
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return 0, fmt.Errorf("failed to create destination: %w", err)
}
defer dstFile.Close()
encoder, err := zstd.NewWriter(dstFile, zstd.WithEncoderLevel(zstd.SpeedBetterCompression))
if err != nil {
return 0, fmt.Errorf("failed to create zstd encoder: %w", err)
}
if _, err := io.Copy(encoder, srcFile); err != nil {
encoder.Close()
return 0, fmt.Errorf("failed to compress: %w", err)
}
if err := encoder.Close(); err != nil {
return 0, fmt.Errorf("failed to finalize compression: %w", err)
}
stat, err := dstFile.Stat()
if err != nil {
return 0, fmt.Errorf("failed to stat compressed file: %w", err)
}
return stat.Size(), nil
}
// decompressFile decompresses src to dst using zstd
func decompressFile(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open source: %w", err)
}
defer srcFile.Close()
decoder, err := zstd.NewReader(srcFile)
if err != nil {
return fmt.Errorf("failed to create zstd decoder: %w", err)
}
defer decoder.Close()
dstFile, err := os.Create(dst)
if err != nil {
return fmt.Errorf("failed to create destination: %w", err)
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, decoder); err != nil {
return fmt.Errorf("failed to decompress: %w", err)
}
return nil
}
// copyDir copies a directory recursively (legacy support)
func copyDir(src, dst string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(src, path)
if err != nil {
return err
}
dstPath := filepath.Join(dst, relPath)
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
}
srcFile, err := os.Open(path)
if err != nil {
return err
}
defer srcFile.Close()
if err := os.MkdirAll(filepath.Dir(dstPath), 0o750); err != nil {
return err
}
dstFile, err := os.Create(dstPath)
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
return err
})
}
// Legacy hot snapshot support for backwards compatibility
const (
hotSnapshotManifestName = "hot_snapshot.json"
hotSnapshotVersion = 1
)
type hotSnapshotManifest struct {
Version int `json:"version"`
Network string `json:"network"`
Nodes map[string]hotSnapshotNode `json:"nodes"`
}
type hotSnapshotNode struct {
LastVersion uint64 `json:"last_version"`
Backups []hotSnapshotBackup `json:"backups"`
}
type hotSnapshotBackup struct {
Since uint64 `json:"since"`
Version uint64 `json:"version"`
Path string `json:"path"`
CreatedAt string `json:"created_at"`
}
func loadHotSnapshotManifest(path string) (*hotSnapshotManifest, error) {
data, err := os.ReadFile(path)
if err != nil {
@@ -568,14 +898,6 @@ func loadHotSnapshotManifest(path string) (*hotSnapshotManifest, error) {
return manifest, nil
}
func saveHotSnapshotManifest(path string, manifest *hotSnapshotManifest) error {
data, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal hot snapshot manifest: %w", err)
}
return createFileAndWrite(path, data)
}
func (ln *localNetwork) prepareHotSnapshotDirs(nodeConfigs []node.Config) error {
for _, nodeConfig := range nodeConfigs {
targetDBDir := filepath.Join(ln.rootDir, nodeConfig.Name, defaultDBSubdir)
@@ -624,10 +946,11 @@ func (ln *localNetwork) applyHotSnapshot(
}
}
}
return nil
}
// Admin API request types and helpers
type adminSnapshotArgs struct {
Path string `json:"path"`
Since uint64 `json:"since"`
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"context"
"github.com/luxfi/protocol/p/txs"
"github.com/luxfi/sdk/api/platformvm"
"github.com/luxfi/sdk/platformvm"
pwalletwallet "github.com/luxfi/sdk/wallet/chain/p/wallet"
"github.com/luxfi/sdk/wallet/primary/common"
)
+95 -34
View File
@@ -4,52 +4,113 @@ import (
_ "embed"
"encoding/json"
"fmt"
"os"
"path/filepath"
geth_params "github.com/luxfi/geth/params"
"github.com/luxfi/constants"
)
//go:embed default/genesis.json
var genesisBytes []byte
// LoadLocalGenesis loads the local network genesis from disk
// and returns it as a map[string]interface{}
// LoadLocalGenesis loads the embedded local/dev genesis from disk.
// Returns map[string]interface{} for legacy compatibility.
//
// IMPORTANT: This function is for LOCAL/DEV networks only.
// For canonical networks (mainnet/testnet/devnet), use the genesis configs
// from luxfi/genesis/configs package via local.NewCanonicalXxxConfig().
//
// The embedded genesis.json MUST have cChainGenesis as a JSON string (not object).
// This function does NOT modify cChainGenesis - it preserves it exactly as embedded.
func LoadLocalGenesis() (map[string]interface{}, error) {
var (
genesisMap map[string]interface{}
err error
)
if err = json.Unmarshal(genesisBytes, &genesisMap); err != nil {
var genesisMap map[string]interface{}
if err := json.Unmarshal(genesisBytes, &genesisMap); err != nil {
return nil, err
}
cChainGenesis := genesisMap["cChainGenesis"]
// set the cchain genesis directly from geth
// Use TestChainConfig as local config
gethCChainGenesis := geth_params.TestChainConfig
// Handle both string (already serialized) and map cases
var cChainGenesisMap map[string]interface{}
switch v := cChainGenesis.(type) {
case string:
// cChainGenesis is already a JSON string, parse it
if err := json.Unmarshal([]byte(v), &cChainGenesisMap); err != nil {
return nil, fmt.Errorf("failed to parse cChainGenesis string: %w", err)
// Validate cChainGenesis type - MUST be string in embedded genesis
if cChainGenesis, ok := genesisMap["cChainGenesis"]; ok {
switch cChainGenesis.(type) {
case string:
// Correct type - keep as-is, do not modify
case map[string]interface{}:
// Wrong type - embedded genesis.json is malformed
return nil, fmt.Errorf(
"embedded genesis has cChainGenesis as object, but it must be a JSON string. " +
"Fix the embedded network/default/genesis.json file")
default:
return nil, fmt.Errorf(
"expected cChainGenesis to be string, got %T", cChainGenesis)
}
case map[string]interface{}:
cChainGenesisMap = v
default:
return nil, fmt.Errorf(
"expected field 'cChainGenesis' to be string or map[string]interface{}, but got %T", cChainGenesis)
}
// set the `config` key to the actual geth object
cChainGenesisMap["config"] = gethCChainGenesis
// and then marshal everything into a string
configBytes, err := json.Marshal(cChainGenesisMap)
if err != nil {
return nil, err
}
// this way the whole of `cChainGenesis` is a properly escaped string
genesisMap["cChainGenesis"] = string(configBytes)
return genesisMap, nil
}
// LoadCanonicalGenesis loads canonical genesis for mainnet/testnet WITHOUT modifying configs.
// This preserves the exact cChainGenesis to ensure correct genesis hash computation.
// For custom/local networks, falls back to LoadLocalGenesis behavior.
func LoadCanonicalGenesis(networkID uint32) (map[string]interface{}, error) {
// For local/custom networks, use the standard local genesis with TestChainConfig
if networkID != constants.MainnetID && networkID != constants.TestnetID {
return LoadLocalGenesis()
}
// For mainnet/testnet, load canonical genesis from filesystem
genesisData, err := LoadGenesisForNetwork(networkID)
if err != nil {
return nil, fmt.Errorf("failed to load canonical genesis for network %d: %w", networkID, err)
}
var genesisMap map[string]interface{}
if err = json.Unmarshal(genesisData, &genesisMap); err != nil {
return nil, fmt.Errorf("failed to parse canonical genesis: %w", err)
}
// IMPORTANT: Do NOT modify cChainGenesis config for canonical networks.
// The canonical genesis must be used as-is to produce the correct genesis hash.
return genesisMap, nil
}
// LoadGenesisForNetwork loads genesis config from standard paths based on network ID.
// For mainnet (1) and testnet (2), it loads from luxfi/genesis configs.
// For other network IDs, it falls back to local genesis.
func LoadGenesisForNetwork(networkID uint32) ([]byte, error) {
var networkName string
switch networkID {
case constants.MainnetID:
networkName = "mainnet"
case constants.TestnetID:
networkName = "testnet"
default:
// For custom/local networks, use embedded genesis
return genesisBytes, nil
}
// Try standard genesis locations
home, _ := os.UserHomeDir()
candidates := []string{
filepath.Join(home, "work/lux/genesis/configs", networkName, "genesis.json"),
filepath.Join(home, ".lux/genesis", networkName, "genesis.json"),
filepath.Join("/etc/lux/genesis", networkName, "genesis.json"),
}
for _, path := range candidates {
data, err := os.ReadFile(path)
if err == nil {
return data, nil
}
}
return nil, fmt.Errorf("genesis config not found for network %s (ID: %d)", networkName, networkID)
}
// LoadMainnetGenesis loads the canonical mainnet genesis configuration.
func LoadMainnetGenesis() ([]byte, error) {
return LoadGenesisForNetwork(constants.MainnetID)
}
// LoadTestnetGenesis loads the canonical testnet genesis configuration.
func LoadTestnetGenesis() ([]byte, error) {
return LoadGenesisForNetwork(constants.TestnetID)
}
+108 -73
View File
@@ -12,6 +12,7 @@ import (
"sort"
"strings"
"sync"
"time"
"maps"
"slices"
@@ -173,87 +174,87 @@ func (lc *localNetwork) createConfig() error {
}
lc.log.Info("createConfig networkID parsed", "networkID", networkID, "MainnetID", constants.MainnetID, "TestnetID", constants.TestnetID)
fmt.Fprintf(os.Stderr, "DEBUG: rootDataDir=%s\n", lc.options.rootDataDir)
// CRITICAL: Check if we're resuming from existing state.
// If node1/genesis.json exists AND node1/db has data, we MUST use the existing genesis
// to avoid "db contains invalid genesis hash" errors. JSON serialization is non-deterministic
// due to Go map iteration order, so regenerating genesis produces different bytes.
// AND the existing staking keys to avoid validator mismatch errors.
existingGenesis, isResume := lc.checkForExistingGenesis()
fmt.Fprintf(os.Stderr, "DEBUG: checkForExistingGenesis returned isResume=%v, genesisLen=%d\n", isResume, len(existingGenesis))
if isResume {
ux.Print(lc.log, log.Green.Wrap("📂 RESUME DETECTED: Using existing genesis from %s"), lc.options.rootDataDir)
fmt.Fprintf(os.Stderr, "📂 RESUME DETECTED: Using existing genesis from %s\n", lc.options.rootDataDir)
ux.Print(lc.log, log.Green.Wrap("📂 RESUME DETECTED: Loading existing genesis AND staking keys from %s"), lc.options.rootDataDir)
fmt.Fprintf(os.Stderr, "📂 RESUME DETECTED: Loading existing genesis AND staking keys from %s\n", lc.options.rootDataDir)
// CRITICAL: When resuming from a snapshot, we MUST use the existing staking keys
// from the snapshot, not generate new ones from ~/.lux/keys/. The snapshot's genesis
// contains NodeIDs that match the snapshot's staking keys. Using different keys
// causes validator mismatch and nodes fail to start.
cfg, err = local.NewConfigFromExistingSnapshot(
lc.options.execPath,
lc.options.rootDataDir,
existingGenesis,
lc.options.numNodes,
9630, // default port base
)
if err != nil {
return fmt.Errorf("failed to load config from existing snapshot: %w", err)
}
} else {
fmt.Fprintf(os.Stderr, "DEBUG: Not resuming - checkForExistingGenesis returned false (rootDataDir=%s)\n", lc.options.rootDataDir)
}
fmt.Fprintf(os.Stderr, "DEBUG: Fresh start - generating new config (rootDataDir=%s)\n", lc.options.rootDataDir)
// Use the appropriate genesis configuration based on network ID
// CRITICAL: For mainnet/testnet, ALWAYS use canonical genesis to ensure byte-for-byte
// deterministic output. This prevents "db contains invalid genesis hash" errors on restart.
// The canonical genesis files are pre-serialized and never re-generated.
// Use the appropriate genesis configuration based on network ID
// CRITICAL: For mainnet/testnet, ALWAYS use canonical genesis to ensure byte-for-byte
// deterministic output. This prevents "db contains invalid genesis hash" errors on restart.
// The canonical genesis files are pre-serialized and never re-generated.
// Check if LUX_MNEMONIC is set - if so, use mnemonic-based config to generate
// genesis with funds allocated to the mnemonic-derived address
mnemonic := os.Getenv("LUX_MNEMONIC")
useMnemonic := mnemonic != ""
// Check if LUX_MNEMONIC is set - if so, use mnemonic-based config to generate
// genesis with funds allocated to the mnemonic-derived address
mnemonic := os.Getenv("LUX_MNEMONIC")
useMnemonic := mnemonic != ""
switch networkID {
case constants.MainnetID: // LUX Mainnet (1)
if useMnemonic {
ux.Print(lc.log, "%s", log.Green.Wrap("Loading mainnet genesis FROM MNEMONIC (funds allocated to derived address)"))
cfg, err = local.NewMainnetConfigFromMnemonic(lc.options.execPath, lc.options.numNodes)
} else {
// ALWAYS use canonical genesis for mainnet - never regenerate
ux.Print(lc.log, "%s", log.Green.Wrap("Loading CANONICAL mainnet genesis (deterministic bytes)"))
cfg, err = local.NewCanonicalMainnetConfig(lc.options.execPath, lc.options.numNodes)
}
case constants.TestnetID: // LUX Testnet (2)
if useMnemonic {
ux.Print(lc.log, "%s", log.Green.Wrap("Loading testnet genesis FROM MNEMONIC (funds allocated to derived address)"))
cfg, err = local.NewTestnetConfigFromMnemonic(lc.options.execPath, lc.options.numNodes)
} else {
// ALWAYS use canonical genesis for testnet - never regenerate
ux.Print(lc.log, "%s", log.Green.Wrap("Loading CANONICAL testnet genesis (deterministic bytes)"))
cfg, err = local.NewCanonicalTestnetConfig(lc.options.execPath, lc.options.numNodes)
}
case constants.DevnetID: // LUX Devnet (3)
if useMnemonic {
ux.Print(lc.log, "%s", log.Green.Wrap("Loading devnet genesis FROM MNEMONIC (funds allocated to derived address)"))
cfg, err = local.NewDevnetConfigFromMnemonic(lc.options.execPath, lc.options.numNodes)
} else {
ux.Print(lc.log, "%s", log.Green.Wrap("Loading CANONICAL devnet genesis (deterministic bytes)"))
cfg, err = local.NewCanonicalDevnetConfig(lc.options.execPath, lc.options.numNodes)
}
case constants.CustomID: // Custom/Local (1337)
if useMnemonic {
ux.Print(lc.log, "%s", log.Green.Wrap("Loading custom genesis FROM MNEMONIC (funds allocated to derived address)"))
cfg, err = local.NewLocalConfigFromMnemonic(lc.options.execPath, lc.options.numNodes)
} else {
// Use canonical genesis for custom network
ux.Print(lc.log, "%s", log.Green.Wrap("Loading CANONICAL custom genesis (deterministic bytes)"))
cfg, err = local.NewCanonicalCustomConfig(lc.options.execPath, lc.options.numNodes)
}
default:
// Fallback for unknown network IDs
ux.Print(lc.log, "%s", log.Orange.Wrap(fmt.Sprintf("Unknown network ID %d, using default config", networkID)))
cfg, err = local.NewDefaultConfigNNodes(lc.options.execPath, lc.options.numNodes)
}
if err != nil {
return err
}
// For resume scenarios, verify genesis matches (but don't override for mainnet/testnet
// since canonical genesis is already deterministic)
if isResume && existingGenesis != "" {
if cfg.Genesis != existingGenesis {
if networkID == constants.MainnetID || networkID == constants.TestnetID {
// Log warning but use canonical - this shouldn't happen with proper setup
ux.Print(lc.log, "%s", log.Orange.Wrap("WARNING: Existing genesis differs from canonical. Using canonical."))
fmt.Fprintf(os.Stderr, "WARNING: Genesis mismatch detected. Using canonical genesis.\n")
switch networkID {
case constants.MainnetID: // LUX Mainnet (1)
if useMnemonic {
ux.Print(lc.log, "%s", log.Green.Wrap("Loading mainnet genesis FROM MNEMONIC (funds allocated to derived address)"))
cfg, err = local.NewMainnetConfigFromMnemonic(lc.options.execPath, lc.options.numNodes)
} else {
// For custom networks, use existing genesis to preserve state
ux.Print(lc.log, "%s", log.Green.Wrap("Using existing genesis for custom network"))
cfg.Genesis = existingGenesis
// ALWAYS use canonical genesis for mainnet - never regenerate
ux.Print(lc.log, "%s", log.Green.Wrap("Loading CANONICAL mainnet genesis (deterministic bytes)"))
cfg, err = local.NewCanonicalMainnetConfig(lc.options.execPath, lc.options.numNodes)
}
case constants.TestnetID: // LUX Testnet (2)
if useMnemonic {
ux.Print(lc.log, "%s", log.Green.Wrap("Loading testnet genesis FROM MNEMONIC (funds allocated to derived address)"))
cfg, err = local.NewTestnetConfigFromMnemonic(lc.options.execPath, lc.options.numNodes)
} else {
// ALWAYS use canonical genesis for testnet - never regenerate
ux.Print(lc.log, "%s", log.Green.Wrap("Loading CANONICAL testnet genesis (deterministic bytes)"))
cfg, err = local.NewCanonicalTestnetConfig(lc.options.execPath, lc.options.numNodes)
}
case constants.DevnetID: // LUX Devnet (3)
if useMnemonic {
ux.Print(lc.log, "%s", log.Green.Wrap("Loading devnet genesis FROM MNEMONIC (funds allocated to derived address)"))
cfg, err = local.NewDevnetConfigFromMnemonic(lc.options.execPath, lc.options.numNodes)
} else {
ux.Print(lc.log, "%s", log.Green.Wrap("Loading CANONICAL devnet genesis (deterministic bytes)"))
cfg, err = local.NewCanonicalDevnetConfig(lc.options.execPath, lc.options.numNodes)
}
case constants.CustomID: // Custom/Local (1337)
if useMnemonic {
ux.Print(lc.log, "%s", log.Green.Wrap("Loading custom genesis FROM MNEMONIC (funds allocated to derived address)"))
cfg, err = local.NewLocalConfigFromMnemonic(lc.options.execPath, lc.options.numNodes)
} else {
// Use canonical genesis for custom network
ux.Print(lc.log, "%s", log.Green.Wrap("Loading CANONICAL custom genesis (deterministic bytes)"))
cfg, err = local.NewCanonicalCustomConfig(lc.options.execPath, lc.options.numNodes)
}
default:
// Fallback for unknown network IDs
ux.Print(lc.log, "%s", log.Orange.Wrap(fmt.Sprintf("Unknown network ID %d, using default config", networkID)))
cfg, err = local.NewDefaultConfigNNodes(lc.options.execPath, lc.options.numNodes)
}
if err != nil {
return err
}
}
@@ -699,9 +700,27 @@ func (lc *localNetwork) updateChainInfo(ctx context.Context) error {
if pChainClient == nil {
return fmt.Errorf("P-Chain client is nil")
}
// Retry GetBlockchains with exponential backoff
// The P-Chain may still be initializing even after health check passes
maxRetries := 120
retryInterval := 500 * time.Millisecond
blockchains, err := (*pChainClient).GetBlockchains(ctx)
for i := 1; err != nil && i < maxRetries; i++ {
lc.log.Debug("P-Chain GetBlockchains failed, retrying...",
log.Int("attempt", i+1),
log.Int("maxRetries", maxRetries),
log.Err(err))
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled while waiting for P-Chain: %w", ctx.Err())
case <-time.After(retryInterval):
// Continue to next retry
}
blockchains, err = (*pChainClient).GetBlockchains(ctx)
}
if err != nil {
return err
return fmt.Errorf("P-Chain GetBlockchains failed after %d retries: %w", maxRetries, err)
}
for _, blockchain := range blockchains {
@@ -720,9 +739,23 @@ func (lc *localNetwork) updateChainInfo(ctx context.Context) error {
}
}
// Retry GetNets with same logic
chains, err := (*pChainClient).GetNets(ctx, nil)
for i := 1; err != nil && i < maxRetries; i++ {
lc.log.Debug("P-Chain GetNets failed, retrying...",
log.Int("attempt", i+1),
log.Int("maxRetries", maxRetries),
log.Err(err))
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled while waiting for P-Chain GetNets: %w", ctx.Err())
case <-time.After(retryInterval):
// Continue to next retry
}
chains, err = (*pChainClient).GetNets(ctx, nil)
}
if err != nil {
return err
return fmt.Errorf("P-Chain GetNets failed after %d retries: %w", maxRetries, err)
}
chainIDList := []string{}
@@ -823,7 +856,9 @@ func (lc *localNetwork) awaitHealthyAndUpdateNetworkInfo(ctx context.Context) er
}
if err := lc.updateChainInfo(ctx); err != nil {
return err
// For fresh mainnet/testnet starts, P-Chain API may not be ready immediately
// This is non-fatal since updateChainInfo only populates custom chain info
lc.log.Warn("updateChainInfo failed (non-fatal for initial start)", log.Err(err))
}
nodeNames := slices.Collect(maps.Keys(lc.nodeInfos))
+4 -2
View File
@@ -52,10 +52,12 @@ const (
stopTimeout = 5 * time.Second
defaultStartTimeout = 5 * time.Minute
// waitForHealthyTimeout - 60s for local 5-node network bootstrap
// waitForHealthyTimeout - 7 minutes for mainnet network bootstrap
// Node's monitorBootstrap has a 5-minute timeout, so we need to exceed that
// First startup takes longer as nodes need to establish consensus
// Subsequent restarts are faster (<10s) but initial bootstrap needs time
waitForHealthyTimeout = 60 * time.Second
// P-Chain API may return 503 for up to 60s after health check passes
waitForHealthyTimeout = 7 * time.Minute
// chainDeployTimeout - time for chain deploy operations including node restarts
// Chain creation, node restart, and P-chain sync can take 60-90s
chainDeployTimeout = 120 * time.Second
+2 -2
View File
@@ -22,9 +22,9 @@ import (
luxd_ "github.com/luxfi/constants"
"github.com/luxfi/p2p/message"
"github.com/luxfi/sdk/api/admin"
"github.com/luxfi/sdk/admin"
"github.com/luxfi/sdk/api/platformvm"
"github.com/luxfi/sdk/platformvm"
"github.com/luxfi/ids"
log "github.com/luxfi/log"