mirror of
https://github.com/luxfi/netrunner.git
synced 2026-07-27 00:04:23 +00:00
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:
+9
-9
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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"`
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user