From cd0dd2fccc3bbf0d842d2987e697b614bd6c11a8 Mon Sep 17 00:00:00 2001 From: Zach Kelling Date: Mon, 29 Dec 2025 10:07:43 -0800 Subject: [PATCH] fix: correct network ID usage and remove HID conflicts - Remove ledger-lux-go transitive dependency - Fix Network ID vs Chain ID confusion in genesis_config.go - Use constants.MainnetID/TestnetID instead of configs.ChainID - Fix port base calculation for multi-network mode - Add snapshot server functionality - Update protobuf definitions with new RPC methods - Add mnemonic test coverage --- buf.yaml | 5 +- client/client.go | 6 + cmd/start.go | 88 +- go.mod | 59 +- go.sum | 124 +-- local/blockchain.go | 43 +- local/blockchain.go.bak | 1501 ------------------------------ local/default/cchain_config.json | 6 +- local/default/flags.json | 3 +- local/genesis_config.go | 122 ++- local/helpers.go | 65 +- local/mnemonic_test.go | 72 ++ local/network.go | 12 +- local/node.go | 5 +- local/snapshot.go | 117 +++ network/config.go | 2 + network/network.go | 4 + network/node/node.go | 2 + rpcpb/rpc.pb.go | 67 +- rpcpb/rpc.pb.gw.go | 66 ++ rpcpb/rpc.proto | 9 + rpcpb/rpc_grpc.pb.go | 42 + server/network.go | 101 +- server/server.go | 26 + 24 files changed, 862 insertions(+), 1685 deletions(-) delete mode 100644 local/blockchain.go.bak create mode 100644 local/mnemonic_test.go diff --git a/buf.yaml b/buf.yaml index 8d87c1a5..99940e47 100644 --- a/buf.yaml +++ b/buf.yaml @@ -6,6 +6,9 @@ breaking: - FILE lint: use: - - DEFAULT + - STANDARD except: - PACKAGE_VERSION_SUFFIX + - RPC_REQUEST_RESPONSE_UNIQUE + - RPC_REQUEST_STANDARD_NAME + - RPC_RESPONSE_STANDARD_NAME diff --git a/client/client.go b/client/client.go index 9db16593..216a6d02 100644 --- a/client/client.go +++ b/client/client.go @@ -52,6 +52,7 @@ type Client interface { SendOutboundMessage(ctx context.Context, nodeName string, peerID string, op uint32, msgBody []byte) (*rpcpb.SendOutboundMessageResponse, error) Close() error SaveSnapshot(ctx context.Context, snapshotName string) (*rpcpb.SaveSnapshotResponse, error) + SaveHotSnapshot(ctx context.Context, snapshotName string) (*rpcpb.SaveSnapshotResponse, error) LoadSnapshot(ctx context.Context, snapshotName string, opts ...OpOption) (*rpcpb.LoadSnapshotResponse, error) RemoveSnapshot(ctx context.Context, snapshotName string) (*rpcpb.RemoveSnapshotResponse, error) GetSnapshotNames(ctx context.Context) ([]string, error) @@ -343,6 +344,11 @@ func (c *client) SaveSnapshot(ctx context.Context, snapshotName string) (*rpcpb. return c.controlc.SaveSnapshot(ctx, &rpcpb.SaveSnapshotRequest{SnapshotName: snapshotName}) } +func (c *client) SaveHotSnapshot(ctx context.Context, snapshotName string) (*rpcpb.SaveSnapshotResponse, error) { + c.logger.Info("save hot snapshot", log.String("snapshot-name", snapshotName)) + return c.controlc.SaveHotSnapshot(ctx, &rpcpb.SaveSnapshotRequest{SnapshotName: snapshotName}) +} + func (c *client) LoadSnapshot(ctx context.Context, snapshotName string, opts ...OpOption) (*rpcpb.LoadSnapshotResponse, error) { c.logger.Info("load snapshot", log.String("snapshot-name", snapshotName)) ret := &Op{} diff --git a/cmd/start.go b/cmd/start.go index fe5e10d1..4e366a4a 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -54,19 +54,7 @@ var startCmd = &cobra.Command{ } func runMultiNetwork(cmd *cobra.Command, args []string) error { - fmt.Println("šŸš€ Starting networks in parallel mode...") - - dbPath := "/tmp/netrunner-shared-db" - if sharedDB { - fmt.Println("šŸ“¦ Using shared BadgerDB for cross-chain transactions") - } - - // Create multi-network manager - manager, err := multinet.NewMultiNetworkManager(startLogger, dbPath) - if err != nil { - return fmt.Errorf("failed to create multi-network manager: %w", err) - } - defer manager.Shutdown() + fmt.Println("šŸš€ Starting multiple networks...") // Determine which networks to start networksToStart := networks @@ -74,25 +62,71 @@ func runMultiNetwork(cmd *cobra.Command, args []string) error { networksToStart = []string{"mainnet", "testnet"} } - // Configure networks - for _, network := range networksToStart { - config := getNetworkConfig(network) - if err := manager.AddNetwork(config); err != nil { - return fmt.Errorf("failed to add network %s: %w", network, err) + // Load validator keys once + ks := keys.NewKeyStore(keysDir) + validatorKeys, err := ks.LoadAll() + if err != nil { + return fmt.Errorf("failed to load validator keys: %w", err) + } + if len(validatorKeys) == 0 { + return fmt.Errorf("no validator keys found in %s", keysDir) + } + + fmt.Printf("šŸ“‹ Loaded %d validator keys\n", len(validatorKeys)) + + // Track running networks for cleanup + var runningNetworks []network.Network + defer func() { + for _, ln := range runningNetworks { + _ = ln.Stop(cmd.Context()) + } + }() + + // Start each network + for _, networkName := range networksToStart { + fmt.Printf("\nšŸ”§ Starting %s...\n", networkName) + + var netConfig network.Config + switch networkName { + case "mainnet": + netConfig, err = local.NewMainnetConfigWithKeys(binaryPath, keysDir) + case "testnet": + netConfig, err = local.NewTestnetConfigWithKeys(binaryPath, keysDir) + default: + return fmt.Errorf("unknown network: %s", networkName) + } + if err != nil { + return fmt.Errorf("failed to create %s config: %w", networkName, err) + } + + ln, err := local.NewNetwork( + startLogger, + netConfig, + "/tmp/netrunner/"+networkName, + "", // snapshot dir + true, // reassign ports if busy + ) + if err != nil { + return fmt.Errorf("failed to start %s: %w", networkName, err) + } + runningNetworks = append(runningNetworks, ln) + + nodes, _ := ln.GetAllNodes() + for name, n := range nodes { + fmt.Printf(" %s: http://localhost:%d\n", name, n.GetAPIPort()) } } - // Start all networks - if err := manager.StartAll(); err != nil { - return fmt.Errorf("failed to start networks: %w", err) - } - - fmt.Println("\nāœ… Networks started successfully!") + fmt.Println("\nāœ… All networks started!") printNetworkEndpoints(networksToStart) - fmt.Println("\nPress Ctrl+C to stop...") - select {} + // Wait for interrupt + fmt.Println("\nā³ Press Ctrl+C to stop...") + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh + fmt.Println("\nšŸ›‘ Shutting down all networks...") return nil } @@ -214,7 +248,7 @@ func printNetworkEndpoints(networks []string) { case "mainnet": fmt.Println(" Lux Mainnet: http://localhost:9630") case "testnet": - fmt.Println(" Lux Testnet: http://localhost:9620") + fmt.Println(" Lux Testnet: http://localhost:9640") } } } diff --git a/go.mod b/go.mod index 89f10dee..11fbc74e 100644 --- a/go.mod +++ b/go.mod @@ -6,19 +6,28 @@ exclude google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 exclude github.com/luxfi/geth v1.16.1 +replace github.com/luxfi/genesis => /Users/z/work/lux/genesis + +replace github.com/luxfi/sdk => /Users/z/work/lux/sdk + +replace github.com/luxfi/node => /Users/z/work/lux/node + require ( + github.com/btcsuite/btcd v0.24.2 + github.com/btcsuite/btcd/btcutil v1.1.6 github.com/dgraph-io/badger/v4 v4.8.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 github.com/luxfi/config v1.0.0 - github.com/luxfi/consensus v1.22.41 - github.com/luxfi/constants v1.2.8 + github.com/luxfi/consensus v1.22.46 + github.com/luxfi/constants v1.3.0 github.com/luxfi/crypto v1.17.26 - github.com/luxfi/genesis v1.5.11 - github.com/luxfi/geth v1.16.60 + github.com/luxfi/genesis v1.5.16 + github.com/luxfi/geth v1.16.64 + github.com/luxfi/go-bip39 v1.1.2 github.com/luxfi/ids v1.2.5 github.com/luxfi/keys v1.0.3 github.com/luxfi/log v1.1.26 - github.com/luxfi/math v1.1.1 + github.com/luxfi/math v1.2.0 github.com/luxfi/metric v1.4.8 github.com/luxfi/node v1.22.61 github.com/luxfi/units v1.0.0 @@ -29,11 +38,10 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.11.1 - github.com/tyler-smith/go-bip32 v1.0.0 - github.com/tyler-smith/go-bip39 v1.1.0 go.uber.org/multierr v1.11.0 + golang.org/x/crypto v0.46.0 golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b - golang.org/x/mod v0.30.0 + golang.org/x/mod v0.31.0 golang.org/x/sync v0.19.0 google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a google.golang.org/grpc v1.75.1 @@ -43,14 +51,13 @@ require ( require ( github.com/DataDog/zstd v1.5.7 // indirect - github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect - github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251211224604-2e727cd2e6fe // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251221085550-b8e13ca38217 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect - github.com/btcsuite/btcd/btcutil v1.1.6 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.5 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.2-0.20251204010831-23491bd573cf // indirect @@ -95,17 +102,19 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/luxfi/address v1.0.0 // indirect github.com/luxfi/cache v1.1.0 // indirect - github.com/luxfi/database v1.2.14 // indirect + github.com/luxfi/database v1.2.17 // indirect github.com/luxfi/go-bip32 v1.0.2 // indirect - github.com/luxfi/go-bip39 v1.1.2 // indirect - github.com/luxfi/ledger-lux-go v1.0.2 // indirect + github.com/luxfi/keychain v1.0.0 // indirect github.com/luxfi/mock v0.1.0 // indirect - github.com/luxfi/p2p v1.5.0 // indirect + github.com/luxfi/p2p v1.18.2 // indirect + github.com/luxfi/sampler v1.0.0 // indirect + github.com/luxfi/staking v1.0.0 // indirect github.com/luxfi/trace v0.1.4 // indirect github.com/luxfi/utils v1.1.0 // indirect github.com/luxfi/vm v1.0.1 // indirect - github.com/luxfi/warp v1.18.0 // indirect + github.com/luxfi/warp v1.18.2 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/otiai10/mint v1.6.3 // indirect @@ -118,13 +127,13 @@ require ( github.com/prometheus/procfs v0.19.2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.11.1 // indirect - github.com/sagikazarmark/locafero v0.10.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/spf13/afero v1.14.0 // indirect + github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.7 // indirect - github.com/spf13/viper v1.20.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.16 // indirect @@ -143,14 +152,14 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/mock v0.6.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/net v0.47.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.40.0 // indirect gonum.org/v1/gonum v0.16.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect diff --git a/go.sum b/go.sum index 20318d34..0a5f05f5 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,11 @@ github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25YnZWly5Gq1ekg6jcmWaGj/vG/MhF4aisoc= -github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:kGUqhHd//musdITWjFvNTHn90WG9bMLBEPQZ17Cmlpw= -github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec h1:1Qb69mGp/UtRPn422BH4/Y4Q3SLUrD9KHuDkm8iodFc= -github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec/go.mod h1:CD8UlnlLDiqb36L110uqiP2iSflVjx9g/3U9hCI4q2U= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251211224604-2e727cd2e6fe h1:Z93WiwkZABbBBb0hGVFSF9nofjiYRvdF7PUxB75oeyE= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251211224604-2e727cd2e6fe/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251221085550-b8e13ca38217 h1:RNTau6sw5B3ltE+2GlzEom63eBMzoLUc+dhjmdIwNpI= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251221085550-b8e13ca38217/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -20,9 +16,12 @@ github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6 github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= +github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= +github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= @@ -30,6 +29,7 @@ github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/ github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= @@ -51,8 +51,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cloudflare/circl v1.6.2-0.20251204010831-23491bd573cf h1:+0huF756d72ZdHxachRP5HYmvRkkOk8CLyKUmJM138c= github.com/cloudflare/circl v1.6.2-0.20251204010831-23491bd573cf/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw= -github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= @@ -233,44 +231,48 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/luxfi/address v1.0.0 h1:9BPx5VyUfzIET3xqrSnvhmi3jY9ZIjcThO1dDdHnqlU= +github.com/luxfi/address v1.0.0/go.mod h1:K6EISvpZ4Ovgna4fsqVRv2xCHw7L1J12LxzqD2jAvv0= github.com/luxfi/cache v1.1.0 h1:6LUyGGZ+rrMAJBbAU6+UwkcamXj3zsboRUodIof2Ong= github.com/luxfi/cache v1.1.0/go.mod h1:9GvlEEE9rFPaaWxvVpSPwW8ZMo2+8VMNNcuPa4AwzPg= github.com/luxfi/config v1.0.0 h1:EFIFkdus3YdDklhlwHKJNYLpSVuvWqHm+fs/TGckv5U= github.com/luxfi/config v1.0.0/go.mod h1:71q4A4m7vj+o1qnL1dNk/tVII+Ugiwvi9+jVMN2xxTU= -github.com/luxfi/consensus v1.22.41 h1:YU5/QI3TADwVpWwPpsxWII8T7Ioohp84nCqadUdHR7w= -github.com/luxfi/consensus v1.22.41/go.mod h1:M7+JXcLOeyCMeNZzH2+UEKwEcfnooON385zTu14decM= -github.com/luxfi/constants v1.2.8 h1:VmtbkwiCz5IEIfjiuLb54L4C7ayn7MqshcWzd+JpIag= -github.com/luxfi/constants v1.2.8/go.mod h1:YyHBi205pBpzgYCmD44QPb6dCOzKTK0jwpGrfB4kcRA= +github.com/luxfi/consensus v1.22.46 h1:w97OZjtW/anWvgM0N/q8Y70LEJlqlFG6yeS10rKcnx4= +github.com/luxfi/consensus v1.22.46/go.mod h1:cbvtxHH5pab/XJIlHekg66tYaIChTwTt9SNMMgz8+as= +github.com/luxfi/constants v1.3.0 h1:nPWEhGyhYt+fJS00TUgV1FsyDlSVvsXf0fXq7UBI1RY= +github.com/luxfi/constants v1.3.0/go.mod h1:xHHeq8ALnhuDrc63B5dIhaOtSetBnA/LO17JbZFd0SU= github.com/luxfi/crypto v1.17.26 h1:B4hebyl2whu9Og+mb7QoQavD4LM1cUpwWQEnwi3H6fU= github.com/luxfi/crypto v1.17.26/go.mod h1:v6eaW5ejuzW2gecChWKQqx4CtVhoM1b+e40ro/6WVPg= -github.com/luxfi/database v1.2.14 h1:0lxsLiCUOb/42tffrRFgIQN/imD0N9rZdAb9r97pbJE= -github.com/luxfi/database v1.2.14/go.mod h1:5xrFyVRGw8z50vkwt9fF1GkvkFvvvEqxrdeSZqGar0U= -github.com/luxfi/genesis v1.5.11 h1:Lv8VGecA1fsF0pEH/SF5u4SSK9RXUH3c7XIAQuEOQMw= -github.com/luxfi/genesis v1.5.11/go.mod h1:a8NdOhQxUrl88cvI6dQBIC8PiTbKBe4xH5vbzu4NXRo= -github.com/luxfi/geth v1.16.60 h1:0fm8U815I4U6JhjCDWaVO0cF9jjksggcxSc/TuK3T9M= -github.com/luxfi/geth v1.16.60/go.mod h1:vRIzu5NRkevNIx50kksP+M+xBeLj0jTWbfn7KvMBqSo= +github.com/luxfi/database v1.2.17 h1:1PE0FlAarorfAMl+vAvO6EDbkS+a4ktL7kvtRk58DWY= +github.com/luxfi/database v1.2.17/go.mod h1:IzvNHGFZWdRs5CL5vswqz8/uu8t796cMGlbqPWs032s= +github.com/luxfi/geth v1.16.64 h1:kfuVA6FkgHOsOKiz75e+h6UG4eHLOInqXmJEhx9NKTs= +github.com/luxfi/geth v1.16.64/go.mod h1:7lQ25do4pQizMtrVKdepji2Pl/pL+8DUo5Q0SbSuWpE= github.com/luxfi/go-bip32 v1.0.2 h1:7vFbb+Wr4Z499q2tuCLdd7wWjtn8sH+HWBlx76mhH9Y= github.com/luxfi/go-bip32 v1.0.2/go.mod h1:bc7/LXDKAJQZ/F0Xjf5yXaTZxY9/ssLb4FC+Hxn/cDk= 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.5 h1:w4YSj3S6LQiUXhiChUmcbrMCRMfkCaBFIGn0Eo4ZSxk= github.com/luxfi/ids v1.2.5/go.mod h1:vZBjJWSmqMIZuodgnoszvTDvXIwsbnhM2pkktg6FGrg= +github.com/luxfi/keychain v1.0.0 h1:FzRW9ZlfsFQZ8RsMGL5X9Jhcg8G1DEmK+WAeGTA2tfM= +github.com/luxfi/keychain v1.0.0/go.mod h1:X7HLk5QRZ1VHJxovAyBL6z7f/nlosoXlveEtvUGhoaw= github.com/luxfi/keys v1.0.3 h1:O1f2h42G1hI6EIoRPl6zSYwrRF7gtUlskHMHbguRHlE= github.com/luxfi/keys v1.0.3/go.mod h1:YrmQQEJWaSdDJym1Wzs0ngNzvaBR5oJNRat0sHj6R1o= -github.com/luxfi/ledger-lux-go v1.0.2 h1:+B8wcF+bcmJ2Kr/dszMmbj20q8wUV5jLY60fkDkWqWo= -github.com/luxfi/ledger-lux-go v1.0.2/go.mod h1:BM5rJ3UPxhmElPRqjP+AHbB+JzwylUGmiGA/Gh4SKUU= github.com/luxfi/log v1.1.26 h1:ECnJ4wV7TF6WZ5gC6CD6ddZ4OhF+zhQij4bgVUhumDg= github.com/luxfi/log v1.1.26/go.mod h1:ACN1FtMlJ/NRuWROYH2TtiFmJuUbSG+OxINlRzJM/38= -github.com/luxfi/math v1.1.1 h1:lR8iS/WQpbNxZsLFVagVee/t90xs0qohL5eC3kK0JR0= -github.com/luxfi/math v1.1.1/go.mod h1:02NrpmajRYR8V1e6o669DWF12gztdtaF6RpiweVuA/k= +github.com/luxfi/math v1.2.0 h1:cZVQQj1PrIWh3Oy6xfbBL44xay4QFuAXZBg9+pWPBmE= +github.com/luxfi/math v1.2.0/go.mod h1:gcmMy6raVf6xrQW/4UmFXEEXC/UC2tZeGkxdiDzeBSg= github.com/luxfi/metric v1.4.8 h1:mMbcJW+E1z6qQxn34+BE109HlvT6ciiDXuc10qEBJRs= github.com/luxfi/metric v1.4.8/go.mod h1:AUQ7NSNz9WndAcr/SKnOkP7XSFFnBXOa+ihtJYfDaQY= 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/node v1.22.61 h1:BU+hJeyPqxr2nH8hSLzbCJr8tVW1DwroktufzK90XFY= -github.com/luxfi/node v1.22.61/go.mod h1:zTMpqUkfVeer0D6s0ZZShI1W8u+1ZIGotGlkC0h8NsU= -github.com/luxfi/p2p v1.5.0 h1:g03aCreOBRwMXuxFAT2ca0jDbW3RF32vBaM2/rfDmF8= -github.com/luxfi/p2p v1.5.0/go.mod h1:yVPJQbCm2uxnFfDwOiBWh0gfG0GschIroLUsLn5Fhm8= +github.com/luxfi/p2p v1.18.2 h1:elpyXr58wht1KP64D4BcKXft1AEjJ3IRACj/W8vdLI8= +github.com/luxfi/p2p v1.18.2/go.mod h1:eM6XpmBFbDjaI7fFuGYBPab77TS1esxHxAOluBLAC0Y= +github.com/luxfi/precompiles v0.1.8 h1:GWigA4sE+jKbO43Oum4apDcLHbhATv6rRivPsnUc5nY= +github.com/luxfi/precompiles v0.1.8/go.mod h1:bzX6yzT1FNhNwCSBu1XB7X/mQ8WO/vKORtJ4ejUbEHM= +github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0= +github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk= +github.com/luxfi/staking v1.0.0 h1:pYI7XMGGb/zK2ATfTIkush/7HPWE7IS2yfmJqj4nzes= +github.com/luxfi/staking v1.0.0/go.mod h1:E+fm3upfQTOu0fLCr3Uo4xYgAm7r/vEzgcUpNAhJuSE= 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/units v1.0.0 h1:2aNVB+WsP1XeDob71IsO0w3jJqP3FtZdYnFsmORkJZg= @@ -279,8 +281,8 @@ github.com/luxfi/utils v1.1.0 h1:ti7HvjNwJd4ILDMERJtOAWE9mF8l+zqDVkgWnF7Agic= github.com/luxfi/utils v1.1.0/go.mod h1:ABqhBdGNig0CaDcnNYldv1byS0BTNi5IKPbJSPF1p98= github.com/luxfi/vm v1.0.1 h1:eZqrp6mEFPkhiv5kf5ZXmYAbXXjpS9axpUQG3cd3yts= github.com/luxfi/vm v1.0.1/go.mod h1:PJinuF4McLaHN57iKf8p7h89wIQhQT//LD/fac+/Dk8= -github.com/luxfi/warp v1.18.0 h1:4FRpB+mnrQeOhPvcKUWOdQbNODV1bY67ll4FKi1mYKc= -github.com/luxfi/warp v1.18.0/go.mod h1:8kjDhuE5UKdd0IUGvoD2rnNphLuATCBOddyriz91pTM= +github.com/luxfi/warp v1.18.2 h1:TWmXjKXI7nxaDOnXIrw8BQTI/nCEniA17bs06bwNB4Q= +github.com/luxfi/warp v1.18.2/go.mod h1:qu3TZv98repNhIDf04I6pzXePSgHjzP+JoSfWmF4PBA= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -328,18 +330,18 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= -github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= -github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 h1:+FZIDR/D97YOPik4N4lPDaUcLDF/EQPogxtlHB2ZZRM= +github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= +github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= -github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= -github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= 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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -367,8 +369,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc= -github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= @@ -377,23 +379,22 @@ github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrel github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= -github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +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/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +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= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.1.5-0.20170601210322-f6abca593680/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -415,12 +416,10 @@ github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8O github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= -github.com/tyler-smith/go-bip32 v1.0.0 h1:sDR9juArbUgX+bO/iblgZnMPeWY1KZMUC2AFUJdv5KE= -github.com/tyler-smith/go-bip32 v1.0.0/go.mod h1:onot+eHknzV4BVPwrzqY5OoVpyCvnwD7lMawL5aQupE= -github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= -github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= 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= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -448,6 +447,8 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -456,11 +457,12 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -golang.org/x/crypto v0.0.0-20170613210332-850760c427c5/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -472,8 +474,8 @@ golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1Rac golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -486,8 +488,8 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -535,8 +537,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -577,5 +579,3 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54= -launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= diff --git a/local/blockchain.go b/local/blockchain.go index 9ef485e4..10072f28 100644 --- a/local/blockchain.go +++ b/local/blockchain.go @@ -17,8 +17,9 @@ import ( "strings" "time" - "github.com/luxfi/genesis/pkg/genesis" "github.com/luxfi/keys" + + "github.com/luxfi/genesis/pkg/genesis" "github.com/luxfi/node/vms/platformvm/reward" "github.com/luxfi/node/vms/components/lux" @@ -1277,24 +1278,27 @@ type wallet struct { // getDefaultKey loads the first key from ~/.lux/keys for wallet operations. // Priority: LUX_MNEMONIC > LUX_PRIVATE_KEY > disk keys func getDefaultKey() (*secp256k1.PrivateKey, error) { - // If LUX_MNEMONIC is set, derive key from mnemonic (index 0) + // If LUX_MNEMONIC is set, derive key using BIP44 path m/44'/9000'/0'/0/0 (LUX native) + // CRITICAL: This MUST match the derivation path used in genesis allocations + // (keys.DeriveValidatorFromMnemonic uses m/44'/9000'/0'/0/{index}) if mnemonic := os.Getenv("LUX_MNEMONIC"); mnemonic != "" { fmt.Printf("šŸ”‘ getDefaultKey: Using LUX_MNEMONIC (len=%d)\n", len(mnemonic)) + + // Use the SAME derivation function as genesis allocations + // This ensures wallet operations use keys that have funds allocated in genesis vk, err := keys.DeriveValidatorFromMnemonic(mnemonic, 0) if err != nil { return nil, fmt.Errorf("failed to derive key from mnemonic: %w", err) } - fmt.Printf("šŸ”‘ VK.PChainAddr (from mnemonic derivation): %s\n", vk.PChainAddr.String()) - privKey, err := secp256k1.ToPrivateKey(vk.ECPrivateKey) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert key: %w", err) } + pubKey := privKey.PublicKey() walletAddr := ids.ShortID(pubKey.Address()) - fmt.Printf("šŸ”‘ Wallet address (from secp256k1 key): %s\n", walletAddr.String()) - fmt.Printf("šŸ”‘ Addresses match: %v\n", vk.PChainAddr == walletAddr) - + fmt.Printf("šŸ”‘ Wallet address (from mnemonic m/44'/9000'/0'/0/0): %s\n", walletAddr.String()) + return privKey, nil } @@ -2440,6 +2444,29 @@ func (ln *localNetwork) setBlockchainConfigFiles( participants = nodeNames } chainAlias := blockchainTxs[i].ID().String() + + // For EVM chains, write genesis.json to chainConfigs directory + // This is required for the EVM to load the chain configuration + if len(chainSpec.Genesis) > 0 { + for _, nodeName := range participants { + _, b := ln.nodes[nodeName] + if !b { + return nil, fmt.Errorf("participant node %s is not in network nodes", nodeName) + } + // Initialize GenesisConfigFiles map if nil + if ln.nodes[nodeName].config.GenesisConfigFiles == nil { + ln.nodes[nodeName].config.GenesisConfigFiles = make(map[string]string) + } + ln.nodes[nodeName].config.GenesisConfigFiles[chainAlias] = string(chainSpec.Genesis) + nodesToRestart.Add(nodeName) + } + log.Info("set genesis config for chain", + "chainAlias", chainAlias, + "participants", len(participants), + "genesisLength", len(chainSpec.Genesis), + ) + } + // update config info. set defaults and node specifics if chainSpec.ChainConfig != nil || len(chainSpec.PerNodeChainConfig) != 0 { for _, nodeName := range participants { diff --git a/local/blockchain.go.bak b/local/blockchain.go.bak deleted file mode 100644 index ab4ddf85..00000000 --- a/local/blockchain.go.bak +++ /dev/null @@ -1,1501 +0,0 @@ -// Copyright (C) 2021-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package local - -import ( - "context" - "encoding/base64" - "errors" - "fmt" - "os" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/luxfi/node/vms/platformvm/reward" - - "github.com/luxfi/node/vms/components/lux" - "github.com/luxfi/node/vms/components/verify" - "github.com/luxfi/node/vms/xvm" - "github.com/luxfi/node/wallet/chain/x" - - "github.com/luxfi/netrunner/network" - "github.com/luxfi/netrunner/network/node" - "github.com/luxfi/netrunner/utils" - "github.com/luxfi/node/api/admin" - "github.com/luxfi/node/config" - "github.com/luxfi/node/genesis" - "github.com/luxfi/ids" - "github.com/luxfi/node/utils/constants" - "github.com/luxfi/crypto/bls" - "github.com/luxfi/log" - luxlog "github.com/luxfi/log" - "github.com/luxfi/node/utils/set" - "github.com/luxfi/node/vms/platformvm" - "github.com/luxfi/node/vms/platformvm/signer" - "github.com/luxfi/node/vms/platformvm/txs" - "github.com/luxfi/node/vms/secp256k1fx" - pwallet "github.com/luxfi/node/wallet/chain/p" - pbuilder "github.com/luxfi/node/wallet/chain/p/builder" - psigner "github.com/luxfi/node/wallet/chain/p/signer" - xbuilder "github.com/luxfi/node/wallet/chain/x/builder" - xsigner "github.com/luxfi/node/wallet/chain/x/signer" - primary "github.com/luxfi/node/wallet/net/primary" - common "github.com/luxfi/node/wallet/net/primary/common" - "go.uber.org/zap" - "golang.org/x/exp/maps" -) - -const ( - // offset of validation start from current time - validationStartOffset = 20 * time.Second - permissionlessValidationStartOffset = 30 * time.Second - // duration for primary network validators - validationDuration = 365 * 24 * time.Hour - // weight assigned to subnet validators - subnetValidatorsWeight = 1000 - // check period for blockchain logs while waiting for custom chains to be ready - blockchainLogPullFrequency = time.Second - // check period while waiting for all validators to be ready - waitForValidatorsPullFrequency = time.Second - defaultTimeout = time.Minute - stakingMinimumLeadTime = 25 * time.Second - minStakeDuration = 24 * 14 * time.Hour -) - -var ( - errAborted = errors.New("aborted") - defaultPoll = common.WithPollFrequency(100 * time.Millisecond) -) - -type blockchainInfo struct { - chainName string - vmID ids.ID - subnetID ids.ID - blockchainID ids.ID -} - -// get node with minimum port number -func (ln *localNetwork) getNode() node.Node { - var node node.Node - minAPIPortNumber := uint16(MaxPort) - for _, n := range ln.nodes { - if n.paused { - continue - } - if n.GetAPIPort() < minAPIPortNumber { - minAPIPortNumber = n.GetAPIPort() - node = n - } - } - return node -} - -// get node client URI for an arbitrary node in the network -func (ln *localNetwork) getClientURI() (string, error) { //nolint - node := ln.getNode() - clientURI := fmt.Sprintf("http://%s:%d", node.GetURL(), node.GetAPIPort()) - ln.log.Info("getClientURI", - zap.String("nodeName", node.GetName()), - zap.String("uri", clientURI)) - return clientURI, nil -} - -func (ln *localNetwork) CreateBlockchains( - ctx context.Context, - chainSpecs []network.BlockchainSpec, // VM name + genesis bytes -) ([]ids.ID, error) { - ln.lock.Lock() - defer ln.lock.Unlock() - - chainInfos, err := ln.installCustomChains(ctx, chainSpecs) - if err != nil { - return nil, err - } - - if err := ln.waitForCustomChainsReady(ctx, chainInfos); err != nil { - return nil, err - } - - if err := ln.RegisterBlockchainAliases(ctx, chainInfos, chainSpecs); err != nil { - return nil, err - } - - chainIDs := []ids.ID{} - for _, chainInfo := range chainInfos { - chainIDs = append(chainIDs, chainInfo.blockchainID) - } - - return chainIDs, nil -} - -// if alias is defined in blockchain-specs, registers an alias for the previously created blockchain -func (ln *localNetwork) RegisterBlockchainAliases( - ctx context.Context, - chainInfos []blockchainInfo, - chainSpecs []network.BlockchainSpec, -) error { - fmt.Println() - ln.log.Info(luxlog.Blue.Wrap(luxlog.Bold.Wrap("registering blockchain aliases"))) - for i, chainSpec := range chainSpecs { - if chainSpec.BlockchainAlias == "" { - continue - } - blockchainAlias := chainSpec.BlockchainAlias - chainID := chainInfos[i].blockchainID.String() - ln.log.Info("registering blockchain alias", - zap.String("alias", blockchainAlias), - zap.String("chain-id", chainID)) - for nodeName, node := range ln.nodes { - if node.paused { - continue - } - adminClient := node.client.AdminAPI() - if adminClient == nil { - return fmt.Errorf("admin client is nil for node %v", nodeName) - } - if err := (*adminClient).AliasChain(ctx, chainID, blockchainAlias); err != nil { - return fmt.Errorf("failure to register blockchain alias %v on node %v: %w", blockchainAlias, nodeName, err) - } - } - } - return nil -} - -func (ln *localNetwork) RemoveSubnetValidators( - ctx context.Context, - removeSubnetSpecs []network.RemoveSubnetValidatorSpec, -) error { - ln.lock.Lock() - defer ln.lock.Unlock() - - return ln.removeSubnetValidators(ctx, removeSubnetSpecs) -} - -func (ln *localNetwork) AddPermissionlessValidators( - ctx context.Context, - validatorSpec []network.PermissionlessValidatorSpec, -) error { - ln.lock.Lock() - defer ln.lock.Unlock() - - return ln.addPermissionlessValidators(ctx, validatorSpec) -} - -func (ln *localNetwork) TransformSubnet( - ctx context.Context, - elasticSubnetConfig []network.ElasticSubnetSpec, -) ([]ids.ID, []ids.ID, error) { - ln.lock.Lock() - defer ln.lock.Unlock() - - return ln.transformToElasticSubnets(ctx, elasticSubnetConfig) -} - -func (ln *localNetwork) CreateSubnets( - ctx context.Context, - subnetSpecs []network.SubnetSpec, -) ([]ids.ID, error) { - ln.lock.Lock() - defer ln.lock.Unlock() - - return ln.installSubnets(ctx, subnetSpecs) -} - -// provisions local cluster and install custom chains if applicable -// assumes the local cluster is already set up and healthy -func (ln *localNetwork) installCustomChains( - ctx context.Context, - chainSpecs []network.BlockchainSpec, -) ([]blockchainInfo, error) { - fmt.Println() - ln.log.Info(luxlog.Blue.Wrap(luxlog.Bold.Wrap("create and install custom chains"))) - - clientURI, err := ln.getClientURI() - if err != nil { - return nil, err - } - platformCli := platformvm.NewClient(clientURI) - - // wallet needs txs for all previously created subnets - var preloadTXs []ids.ID - for _, chainSpec := range chainSpecs { - // if subnet id for the blockchain is specified, we need to add the subnet id - // tx info to the wallet so blockchain creation does not fail - // if subnet id is not specified, a new subnet will later be created by using the wallet, - // and the wallet will obtain the tx info at that moment - if chainSpec.SubnetID != nil { - subnetID, err := ids.FromString(*chainSpec.SubnetID) - if err != nil { - return nil, err - } - preloadTXs = append(preloadTXs, subnetID) - } - } - - w, err := newWallet(ctx, clientURI, preloadTXs) - if err != nil { - return nil, err - } - - // get subnet specs for all new subnets to create - // for the list of requested blockchains, we take those that have undefined subnet id - // and use the provided subnet spec. if not given, use an empty default subnet spec - // that subnets will be created and later on assigned to the blockchain requests - subnetSpecs := []network.SubnetSpec{} - for _, chainSpec := range chainSpecs { - if chainSpec.SubnetID == nil { - if chainSpec.SubnetSpec == nil { - subnetSpecs = append(subnetSpecs, network.SubnetSpec{}) - } else { - subnetSpecs = append(subnetSpecs, *chainSpec.SubnetSpec) - } - } - } - - // if no participants are given for a new subnet, assume all nodes should be participants - allNodeNames := maps.Keys(ln.nodes) - sort.Strings(allNodeNames) - for i := range subnetSpecs { - if len(subnetSpecs[i].Participants) == 0 { - subnetSpecs[i].Participants = allNodeNames - } - } - - // create new nodes - for _, subnetSpec := range subnetSpecs { - for _, nodeName := range subnetSpec.Participants { - _, ok := ln.nodes[nodeName] - if !ok { - ln.log.Info(luxlog.Green.Wrap(fmt.Sprintf("adding new participant %s", nodeName))) - if _, err := ln.addNode(node.Config{Name: nodeName}); err != nil { - return nil, err - } - } - } - } - if err := ln.healthy(ctx); err != nil { - return nil, err - } - - // just ensure all nodes are primary validators (so can be subnet validators) - if err := ln.addPrimaryValidators(ctx, platformCli, w); err != nil { - return nil, err - } - - // create missing subnets - subnetIDs, err := createSubnets(ctx, uint32(len(subnetSpecs)), w, ln.log) - if err != nil { - return nil, err - } - - if err := ln.setSubnetConfigFiles(subnetIDs, subnetSpecs); err != nil { - return nil, err - } - - // assign created subnets to blockchain requests with undefined subnet id - j := 0 - for i := range chainSpecs { - if chainSpecs[i].SubnetID == nil { - subnetIDStr := subnetIDs[j].String() - chainSpecs[i].SubnetID = &subnetIDStr - j++ - } - } - - // wait for nodes to be primary validators before trying to add them as subnet ones - if err = ln.waitPrimaryValidators(ctx, platformCli); err != nil { - return nil, err - } - - if err = ln.addSubnetValidators(ctx, platformCli, w, subnetIDs, subnetSpecs); err != nil { - return nil, err - } - - blockchainTxs, err := createBlockchainTxs(ctx, chainSpecs, w, ln.log) - if err != nil { - return nil, err - } - - nodesToRestartForBlockchainConfigUpdate, err := ln.setBlockchainConfigFiles(ctx, chainSpecs, blockchainTxs, subnetIDs, subnetSpecs, ln.log) - if err != nil { - return nil, err - } - - if len(subnetSpecs) > 0 || len(nodesToRestartForBlockchainConfigUpdate) > 0 { - // we need to restart if there are new subnets or if there are new network config files - // add missing subnets, restarting network and waiting for subnet validation to start - if err := ln.restartNodes(ctx, subnetIDs, subnetSpecs, nil, nil, nodesToRestartForBlockchainConfigUpdate); err != nil { - return nil, err - } - clientURI, err = ln.getClientURI() - if err != nil { - return nil, err - } - w.reload(clientURI) - } - - // refresh vm list - if err := ln.reloadVMPlugins(ctx); err != nil { - return nil, err - } - - if err = ln.waitSubnetValidators(ctx, platformCli, subnetIDs, subnetSpecs); err != nil { - return nil, err - } - - // create blockchain from txs before spending more utxos - if err := ln.createBlockchains(ctx, chainSpecs, blockchainTxs, w, ln.log); err != nil { - return nil, err - } - - chainInfos := make([]blockchainInfo, len(chainSpecs)) - for i, chainSpec := range chainSpecs { - vmID, err := utils.VMID(chainSpec.VMName) - if err != nil { - return nil, err - } - subnetID, err := ids.FromString(*chainSpec.SubnetID) - if err != nil { - return nil, err - } - chainInfos[i] = blockchainInfo{ - // we keep a record of VM name in blockchain name field, - // as there is no way to recover VM name from VM ID - chainName: chainSpec.VMName, - vmID: vmID, - subnetID: subnetID, - blockchainID: blockchainTxs[i].ID(), - } - } - - return chainInfos, nil -} - -func (ln *localNetwork) installSubnets( - ctx context.Context, - subnetSpecs []network.SubnetSpec, -) ([]ids.ID, error) { - fmt.Println() - ln.log.Info(luxlog.Blue.Wrap(luxlog.Bold.Wrap("create subnets"))) - - clientURI, err := ln.getClientURI() - if err != nil { - return nil, err - } - platformCli := platformvm.NewClient(clientURI) - - w, err := newWallet(ctx, clientURI, []ids.ID{}) - if err != nil { - return nil, err - } - - // if no participants are given, assume all nodes should be participants - allNodeNames := maps.Keys(ln.nodes) - sort.Strings(allNodeNames) - for i := range subnetSpecs { - if len(subnetSpecs[i].Participants) == 0 { - subnetSpecs[i].Participants = allNodeNames - } - } - - // create new nodes - for _, subnetSpec := range subnetSpecs { - for _, nodeName := range subnetSpec.Participants { - _, ok := ln.nodes[nodeName] - if !ok { - ln.log.Info(luxlog.Green.Wrap(fmt.Sprintf("adding new participant %s", nodeName))) - if _, err := ln.addNode(node.Config{Name: nodeName}); err != nil { - return nil, err - } - } - } - } - if err := ln.healthy(ctx); err != nil { - return nil, err - } - - // just ensure all nodes are primary validators (so can be subnet validators) - if err := ln.addPrimaryValidators(ctx, platformCli, w); err != nil { - return nil, err - } - - subnetIDs, err := createSubnets(ctx, uint32(len(subnetSpecs)), w, ln.log) - if err != nil { - return nil, err - } - - if err := ln.setSubnetConfigFiles(subnetIDs, subnetSpecs); err != nil { - return nil, err - } - - // wait for nodes to be primary validators before trying to add them as subnet ones - if err = ln.waitPrimaryValidators(ctx, platformCli); err != nil { - return nil, err - } - - if err = ln.addSubnetValidators(ctx, platformCli, w, subnetIDs, subnetSpecs); err != nil { - return nil, err - } - - if err := ln.restartNodes(ctx, subnetIDs, subnetSpecs, nil, nil, nil); err != nil { - return nil, err - } - - if err = ln.waitSubnetValidators(ctx, platformCli, subnetIDs, subnetSpecs); err != nil { - return nil, err - } - - return subnetIDs, nil -} - -func (ln *localNetwork) getSubnetValidatorsNodenames( - ctx context.Context, - subnetID ids.ID, -) ([]string, error) { - clientURI, err := ln.getClientURI() - if err != nil { - return nil, err - } - platformCli := platformvm.NewClient(clientURI) - - ctx, cancel := createDefaultCtx(ctx) - vs, err := platformCli.GetCurrentValidators(ctx, subnetID, nil) - cancel() - if err != nil { - return nil, err - } - nodeNames := []string{} - for _, v := range vs { - for nodeName, node := range ln.nodes { - if v.NodeID == node.GetNodeID() { - nodeNames = append(nodeNames, nodeName) - } - } - } - if len(nodeNames) != len(vs) { - return nil, fmt.Errorf("not all validators for subnet %s are present in network", subnetID.String()) - } - return nodeNames, nil -} - -func (ln *localNetwork) waitForCustomChainsReady( - ctx context.Context, - chainInfos []blockchainInfo, -) error { - fmt.Println() - ln.log.Info(luxlog.Blue.Wrap(luxlog.Bold.Wrap("waiting for custom chains to report healthy..."))) - - if err := ln.healthy(ctx); err != nil { - return err - } - - for _, chainInfo := range chainInfos { - nodeNames, err := ln.getSubnetValidatorsNodenames(ctx, chainInfo.subnetID) - if err != nil { - return err - } - - for _, nodeName := range nodeNames { - node := ln.nodes[nodeName] - if node.paused { - continue - } - ln.log.Info("inspecting node log directory for custom chain logs", zap.String("log-dir", node.GetLogsDir()), zap.String("node-name", nodeName)) - p := filepath.Join(node.GetLogsDir(), chainInfo.blockchainID.String()+".log") - ln.log.Info("checking log", - zap.String("vm-ID", chainInfo.vmID.String()), - zap.String("subnet-ID", chainInfo.subnetID.String()), - zap.String("blockchain-ID", chainInfo.blockchainID.String()), - zap.String("path", p), - ) - for { - if _, err := os.Stat(p); err == nil { - ln.log.Info("found the log", zap.String("path", p)) - break - } - ln.log.Info("log not found yet, retrying...", - zap.String("vm-ID", chainInfo.vmID.String()), - zap.String("subnet-ID", chainInfo.subnetID.String()), - zap.String("blockchain-ID", chainInfo.blockchainID.String()), - ) - select { - case <-ln.onStopCh: - return errAborted - case <-ctx.Done(): - return ctx.Err() - case <-time.After(blockchainLogPullFrequency): - } - } - } - } - - fmt.Println() - ln.log.Info(luxlog.Green.Wrap("all custom chains are running!!!")) - - fmt.Println() - ln.log.Info(luxlog.Green.Wrap(luxlog.Bold.Wrap("all custom chains are ready on RPC server-side -- network-runner RPC client can poll and query the cluster status"))) - - return nil -} - -func (ln *localNetwork) restartNodes( - ctx context.Context, - subnetIDs []ids.ID, - subnetSpecs []network.SubnetSpec, - validatorSpecs []network.PermissionlessValidatorSpec, - removeValidatorSpecs []network.RemoveSubnetValidatorSpec, - nodesToRestartForBlockchainConfigUpdate set.Set[string], -) (err error) { - if (subnetSpecs != nil && validatorSpecs != nil) || (subnetSpecs != nil && removeValidatorSpecs != nil) || - (validatorSpecs != nil && removeValidatorSpecs != nil) { - return errors.New("only one type of spec between subnet specs, add permissionless validator specs and " + - "remove validator specs can be supplied at one time") - } - fmt.Println() - ln.log.Info(luxlog.Blue.Wrap(luxlog.Bold.Wrap("restarting network"))) - - nodeNames := maps.Keys(ln.nodes) - sort.Strings(nodeNames) - - for _, nodeName := range nodeNames { - node := ln.nodes[nodeName] - - // delete node specific flag so as to use default one - nodeConfig := node.GetConfig() - - previousTrackedSubnets := "" - previousTrackedSubnetsIntf, ok := nodeConfig.Flags[config.TrackSubnetsKey] - if ok { - previousTrackedSubnets, ok = previousTrackedSubnetsIntf.(string) - if !ok { - return fmt.Errorf("expected node config %s to have type string obtained %T", config.TrackSubnetsKey, previousTrackedSubnetsIntf) - } - } - - trackSubnetIDsSet := set.Set[string]{} - if previousTrackedSubnets != "" { - for _, s := range strings.Split(previousTrackedSubnets, ",") { - trackSubnetIDsSet.Add(s) - } - } - needsRestart := false - for _, validatorSpec := range validatorSpecs { - if validatorSpec.NodeName == node.name { - trackSubnetIDsSet.Add(validatorSpec.SubnetID) - needsRestart = true - } - } - - for _, removeValidatorSpec := range removeValidatorSpecs { - for _, toRemoveNode := range removeValidatorSpec.NodeNames { - if toRemoveNode == node.name { - trackSubnetIDsSet.Remove(removeValidatorSpec.SubnetID) - needsRestart = true - } - } - } - - for i, subnetID := range subnetIDs { - for _, participant := range subnetSpecs[i].Participants { - if participant == nodeName { - trackSubnetIDsSet.Add(subnetID.String()) - needsRestart = true - } - } - } - - trackSubnetIDs := trackSubnetIDsSet.List() - sort.Strings(trackSubnetIDs) - - tracked := strings.Join(trackSubnetIDs, ",") - nodeConfig.Flags[config.TrackSubnetsKey] = tracked - - if subnetSpecs != nil { - if nodesToRestartForBlockchainConfigUpdate.Contains(nodeName) { - needsRestart = true - } - } - - if !needsRestart { - continue - } - - if node.paused { - continue - } - - if removeValidatorSpecs != nil { - ln.log.Info(luxlog.Green.Wrap(fmt.Sprintf("restarting node %s to stop tracking subnets %s", nodeName, tracked))) - } else { - ln.log.Info(luxlog.Green.Wrap(fmt.Sprintf("restarting node %s to track subnets %s", nodeName, tracked))) - } - - if err := ln.restartNode(ctx, nodeName, "", "", "", nil, nil, nil); err != nil { - return err - } - } - if err := ln.healthy(ctx); err != nil { - return err - } - return nil -} - -type wallet struct { - addr ids.ShortID - pWallet pwallet.Wallet - pBackend pwallet.Backend - pBuilder pbuilder.Builder - pSigner psigner.Signer - xWallet x.Wallet - xChainID ids.ID - luxAssetID ids.ID -} - -func newWallet( - ctx context.Context, - uri string, - preloadTXs []ids.ID, -) (*wallet, error) { - kc := secp256k1fx.NewKeychain(genesis.EWOQKey) - luxState, err := primary.FetchState(ctx, uri, kc.Addrs) - if err != nil { - return nil, err - } - pClient := platformvm.NewClient(uri) - pTXs := make(map[ids.ID]*txs.Tx) - for _, id := range preloadTXs { - txBytes, err := pClient.GetTx(ctx, id) - if err != nil { - return nil, err - } - tx, err := txs.Parse(txs.Codec, txBytes) - if err != nil { - return nil, err - } - pTXs[id] = tx - } - pUTXOs := common.NewChainUTXOs(constants.PlatformChainID, luxState.UTXOs) - xChainID := luxState.XCTX.BlockchainID - xUTXOs := common.NewChainUTXOs(xChainID, luxState.UTXOs) - var w wallet - w.addr = genesis.EWOQKey.PublicKey().Address() - // TODO: Create owners map instead of pTXs - w.pBackend = pwallet.NewBackend(luxState.PCTX, pUTXOs, pTXs) - w.pBuilder = pbuilder.New(kc.Addrs, luxState.PCTX, w.pBackend) - w.pSigner = psigner.New(kc, w.pBackend) - w.pWallet = pwallet.NewWallet(w.pBuilder, w.pSigner, pClient, w.pBackend) - - xBackend := x.NewBackend(luxState.XCTX, xUTXOs) - xBuilder := xbuilder.New(kc.Addrs, luxState.XCTX, xBackend) - xSigner := xsigner.New(kc, xBackend) - xClient := xvm.NewClient(uri, "X") - w.xWallet = x.NewWallet(xBuilder, xSigner, xClient, xBackend) - w.xChainID = xChainID - w.luxAssetID = luxState.PCTX.XAssetID - return &w, nil -} - -func (w *wallet) reload(uri string) { - pClient := platformvm.NewClient(uri) - w.pWallet = pwallet.NewWallet(w.pBuilder, w.pSigner, pClient, w.pBackend) -} - -// add all nodes as validators of the primary network, in case they are not -// the validation starts as soon as possible and its duration is as long as possible, that is, -// it is set to max accepted duration by node -func (ln *localNetwork) addPrimaryValidators( - ctx context.Context, - platformCli platformvm.Client, - w *wallet, -) error { - ln.log.Info(luxlog.Green.Wrap("adding the nodes as primary network validators")) - // ref. https://docs.lux.network/build/node-apis/p-chain/#platformgetcurrentvalidators - ctx, cancel := createDefaultCtx(ctx) - vdrs, err := platformCli.GetCurrentValidators(ctx, constants.PrimaryNetworkID, nil) - cancel() - if err != nil { - return err - } - curValidators := set.Set[ids.NodeID]{} - for _, v := range vdrs { - curValidators.Add(v.NodeID) - } - for nodeName, node := range ln.nodes { - nodeID := node.GetNodeID() - - if curValidators.Contains(nodeID) { - continue - } - - // Prepare node BLS PoP - // It is important to note that this will ONLY register BLS signers for - // nodes registered AFTER genesis. - blsKeyBytes, err := base64.StdEncoding.DecodeString(node.GetConfig().StakingSigningKey) - if err != nil { - return err - } - blsSecretKey, err := bls.SecretKeyFromBytes(blsKeyBytes) - if err != nil { - return err - } - proofOfPossession, err := signer.NewProofOfPossession(blsSecretKey) - if err != nil { - return err - } - ctx, cancel = createDefaultCtx(ctx) - tx, err := w.pWallet.IssueAddPermissionlessValidatorTx( - &txs.NetValidator{ - Validator: txs.Validator{ - NodeID: nodeID, - Start: uint64(time.Now().Add(validationStartOffset).Unix()), - End: uint64(time.Now().Add(validationDuration).Unix()), - Wght: genesis.LocalParams.MinValidatorStake, - }, - Net: ids.Empty, - }, - proofOfPossession, - w.luxAssetID, - &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{w.addr}, - }, - &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{w.addr}, - }, - 10*10000, // 10% fee percent, times 10000 to make it as shares - common.WithContext(ctx), - ) - cancel() - if err != nil { - return fmt.Errorf("P-Wallet Tx Error %s %w, node ID %s", "IssueAddPermissionlessValidatorTx", err, nodeID.String()) - } - ln.log.Info("added node as primary subnet validator", zap.String("node-name", nodeName), zap.String("node-ID", nodeID.String()), zap.String("tx-ID", tx.ID().String())) - } - return nil -} - -func getXChainAssetID(ctx context.Context, w *wallet, tokenName string, tokenSymbol string, maxSupply uint64) (ids.ID, error) { - owner := &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - w.addr, - }, - } - ctx, cancel := createDefaultCtx(ctx) - defer cancel() - tx, err := w.xWallet.IssueCreateAssetTx( - tokenName, - tokenSymbol, - 9, // denomination for UI purposes only in explorer - map[uint32][]verify.State{ - 0: { - &secp256k1fx.TransferOutput{ - Amt: maxSupply, - OutputOwners: *owner, - }, - }, - }, - common.WithContext(ctx), - ) - if err != nil { - return ids.Empty, err - } - return tx.ID(), nil -} - -func exportXChainToPChain(ctx context.Context, w *wallet, owner *secp256k1fx.OutputOwners, subnetAssetID ids.ID, assetAmount uint64) error { - ctx, cancel := createDefaultCtx(ctx) - defer cancel() - _, err := w.xWallet.IssueExportTx( - ids.Empty, - []*lux.TransferableOutput{ - { - Asset: lux.Asset{ - ID: subnetAssetID, - }, - Out: &secp256k1fx.TransferOutput{ - Amt: assetAmount, - OutputOwners: *owner, - }, - }, - }, - defaultPoll, - ) - return err -} - -func importPChainFromXChain(ctx context.Context, w *wallet, owner *secp256k1fx.OutputOwners, xChainID ids.ID) error { - pWallet := w.pWallet - ctx, cancel := createDefaultCtx(ctx) - defer cancel() - _, err := pWallet.IssueImportTx( - xChainID, - owner, - defaultPoll, - ) - return err -} - -func (ln *localNetwork) removeSubnetValidators( - ctx context.Context, - removeSubnetSpecs []network.RemoveSubnetValidatorSpec, -) error { - ln.log.Info("removing subnet validator tx") - removeSubnetSpecIDs := make([]ids.ID, len(removeSubnetSpecs)) - clientURI, err := ln.getClientURI() - if err != nil { - return err - } - platformCli := platformvm.NewClient(clientURI) - // wallet needs txs for all previously created subnets - preloadTXs := make([]ids.ID, len(removeSubnetSpecs)) - for i, removeSubnetSpec := range removeSubnetSpecs { - subnetID, err := ids.FromString(removeSubnetSpec.SubnetID) - if err != nil { - return err - } - preloadTXs[i] = subnetID - } - w, err := newWallet(ctx, clientURI, preloadTXs) - if err != nil { - return err - } - ln.log.Info(luxlog.Green.Wrap("removing the nodes as subnet validators")) - for i, subnetSpec := range removeSubnetSpecs { - subnetID, err := ids.FromString(subnetSpec.SubnetID) - if err != nil { - return err - } - ctx, cancel := createDefaultCtx(ctx) - vs, err := platformCli.GetCurrentValidators(ctx, subnetID, nil) - cancel() - if err != nil { - return err - } - subnetValidators := set.Set[ids.NodeID]{} - for _, v := range vs { - subnetValidators.Add(v.NodeID) - } - toRemoveNodes := subnetSpec.NodeNames - for _, nodeName := range toRemoveNodes { - node, b := ln.nodes[nodeName] - if !b { - return fmt.Errorf("node %s is not in network nodes", nodeName) - } - nodeID := node.GetNodeID() - if isValidator := subnetValidators.Contains(nodeID); !isValidator { - return fmt.Errorf("node %s is currently not a subnet validator of subnet %s", nodeName, subnetID.String()) - } - _, cancel := createDefaultCtx(ctx) - tx, err := w.pWallet.IssueRemoveNetValidatorTx( - nodeID, - subnetID, - defaultPoll, - ) - cancel() - if err != nil { - return fmt.Errorf("P-Wallet Tx Error %s %w, node ID %s, subnetID %s", "IssueRemoveSubnetValidatorTx", err, nodeID.String(), subnetID.String()) - } - ln.log.Info("removed node as subnet validator", - zap.String("node-name", nodeName), - zap.String("node-ID", nodeID.String()), - zap.String("subnet-ID", subnetID.String()), - zap.String("tx-ID", tx.ID().String()), - ) - removeSubnetSpecIDs[i] = tx.ID() - } - } - return ln.restartNodes(ctx, nil, nil, nil, removeSubnetSpecs, nil) -} - -func (ln *localNetwork) addPermissionlessValidators( - ctx context.Context, - validatorSpecs []network.PermissionlessValidatorSpec, -) error { - ln.log.Info("adding permissionless validator tx") - clientURI, err := ln.getClientURI() - if err != nil { - return err - } - platformCli := platformvm.NewClient(clientURI) - // wallet needs txs for all previously created subnets - preloadTXs := make([]ids.ID, len(validatorSpecs)) - for i, validatorSpec := range validatorSpecs { - subnetID, err := ids.FromString(validatorSpec.SubnetID) - if err != nil { - return err - } - preloadTXs[i] = subnetID - } - w, err := newWallet(ctx, clientURI, preloadTXs) - if err != nil { - return err - } - owner := &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - w.addr, - }, - } - // create new nodes - for _, validatorSpec := range validatorSpecs { - _, ok := ln.nodes[validatorSpec.NodeName] - if !ok { - ln.log.Info(log.Green.Wrap(fmt.Sprintf("adding new participant %s", validatorSpec.NodeName))) - if _, err := ln.addNode(node.Config{Name: validatorSpec.NodeName}); err != nil { - return err - } - } - } - if err := ln.healthy(ctx); err != nil { - return err - } - - // just ensure all nodes are primary validators (so can be subnet validators) - if err := ln.addPrimaryValidators(ctx, platformCli, w); err != nil { - return err - } - - // wait for nodes to be primary validators before trying to add them as subnet ones - if err = ln.waitPrimaryValidators(ctx, platformCli); err != nil { - return err - } - - ctx, cancel := createDefaultCtx(ctx) - vs, err := platformCli.GetCurrentValidators(ctx, constants.PrimaryNetworkID, nil) - cancel() - if err != nil { - return err - } - primaryValidatorsEndtime := make(map[ids.NodeID]time.Time) - for _, v := range vs { - primaryValidatorsEndtime[v.NodeID] = time.Unix(int64(v.EndTime), 0) - } - - for _, validatorSpec := range validatorSpecs { - ln.log.Info(luxlog.Green.Wrap("adding permissionless validator"), zap.String("node ", validatorSpec.NodeName)) - _, cancel := createDefaultCtx(ctx) - validatorNodeID := ln.nodes[validatorSpec.NodeName].nodeID - subnetID, err := ids.FromString(validatorSpec.SubnetID) - if err != nil { - return err - } - assetID, err := ids.FromString(validatorSpec.AssetID) - if err != nil { - return err - } - var startTime uint64 - var endTime uint64 - if validatorSpec.StartTime.IsZero() { - startTime = uint64(time.Now().Add(permissionlessValidationStartOffset).Unix()) - } else { - startTime = uint64(validatorSpec.StartTime.Unix()) - } - - if validatorSpec.StakeDuration == 0 { - endTime = uint64(primaryValidatorsEndtime[validatorNodeID].Unix()) - } else { - endTime = uint64(validatorSpec.StartTime.Add(validatorSpec.StakeDuration).Unix()) - } - tx, err := w.pWallet.IssueAddPermissionlessValidatorTx( - &txs.NetValidator{ - Validator: txs.Validator{ - NodeID: validatorNodeID, - Start: startTime, - End: endTime, - Wght: validatorSpec.StakedAmount, - }, - Net: subnetID, - }, - &signer.Empty{}, - assetID, - owner, - &secp256k1fx.OutputOwners{}, - reward.PercentDenominator, - defaultPoll, - ) - cancel() - if err != nil { - return err - } - ln.log.Info("Validator successfully added as permissionless validator", zap.String("TX ID", tx.ID().String())) - } - return ln.restartNodes(ctx, nil, nil, validatorSpecs, nil, nil) -} - -func (ln *localNetwork) transformToElasticSubnets( - ctx context.Context, - elasticSubnetSpecs []network.ElasticSubnetSpec, -) ([]ids.ID, []ids.ID, error) { - ln.log.Info("transforming elastic subnet tx") - elasticSubnetIDs := make([]ids.ID, len(elasticSubnetSpecs)) - assetIDs := make([]ids.ID, len(elasticSubnetSpecs)) - clientURI, err := ln.getClientURI() - if err != nil { - return nil, nil, err - } - // wallet needs txs for all previously created subnets - var preloadTXs []ids.ID - for _, elasticSubnetSpec := range elasticSubnetSpecs { - if elasticSubnetSpec.SubnetID == nil { - return nil, nil, errors.New("elastic subnet spec has no subnet ID") - } else { - subnetID, err := ids.FromString(*elasticSubnetSpec.SubnetID) - if err != nil { - return nil, nil, err - } - preloadTXs = append(preloadTXs, subnetID) - } - } - w, err := newWallet(ctx, clientURI, preloadTXs) - if err != nil { - return nil, nil, err - } - - for i, elasticSubnetSpec := range elasticSubnetSpecs { - ln.log.Info(luxlog.Green.Wrap("transforming elastic subnet"), zap.String("subnet ID", *elasticSubnetSpec.SubnetID)) - - subnetAssetID, err := getXChainAssetID(ctx, w, elasticSubnetSpec.AssetName, elasticSubnetSpec.AssetSymbol, elasticSubnetSpec.MaxSupply) - if err != nil { - return nil, nil, err - } - assetIDs[i] = subnetAssetID - ln.log.Info("created asset ID", zap.String("asset-ID", subnetAssetID.String())) - owner := &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - w.addr, - }, - } - err = exportXChainToPChain(ctx, w, owner, subnetAssetID, elasticSubnetSpec.MaxSupply) - if err != nil { - return nil, nil, err - } - ln.log.Info("exported asset to P-Chain") - err = importPChainFromXChain(ctx, w, owner, w.xChainID) - if err != nil { - return nil, nil, err - } - ln.log.Info("imported asset from X-Chain") - subnetID, err := ids.FromString(*elasticSubnetSpec.SubnetID) - if err != nil { - return nil, nil, err - } - _, cancel := createDefaultCtx(ctx) - transformSubnetTx, err := w.pWallet.IssueTransformNetTx(subnetID, subnetAssetID, - elasticSubnetSpec.InitialSupply, elasticSubnetSpec.MaxSupply, elasticSubnetSpec.MinConsumptionRate, - elasticSubnetSpec.MaxConsumptionRate, elasticSubnetSpec.MinValidatorStake, elasticSubnetSpec.MaxValidatorStake, - elasticSubnetSpec.MinStakeDuration, elasticSubnetSpec.MaxStakeDuration, elasticSubnetSpec.MinDelegationFee, - elasticSubnetSpec.MinDelegatorStake, elasticSubnetSpec.MaxValidatorWeightFactor, elasticSubnetSpec.UptimeRequirement, - defaultPoll, - ) - cancel() - if err != nil { - return nil, nil, err - } - ln.log.Info("Subnet transformed into elastic subnet", zap.String("TX ID", transformSubnetTx.ID().String())) - elasticSubnetIDs[i] = transformSubnetTx.ID() - ln.subnetID2ElasticSubnetID[subnetID] = transformSubnetTx.ID() - } - return elasticSubnetIDs, assetIDs, nil -} - -func (ln *localNetwork) GetElasticSubnetID(_ context.Context, subnetID ids.ID) (ids.ID, error) { - elasticSubnetID, ok := ln.subnetID2ElasticSubnetID[subnetID] - if !ok { - return ids.Empty, fmt.Errorf("subnetID not found on map: %s", subnetID) - } - return elasticSubnetID, nil -} - -func createSubnets( - ctx context.Context, - numSubnets uint32, - w *wallet, - log luxlog.Logger, -) ([]ids.ID, error) { - fmt.Println() - log.Info(luxlog.Green.Wrap("creating subnets"), zap.Uint32("num-subnets", numSubnets)) - subnetIDs := make([]ids.ID, numSubnets) - for i := uint32(0); i < numSubnets; i++ { - log.Info("creating subnet tx") - _, cancel := createDefaultCtx(ctx) - tx, err := w.pWallet.IssueCreateNetTx( - &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{w.addr}, - }, - defaultPoll, - ) - cancel() - if err != nil { - return nil, fmt.Errorf("P-Wallet Tx Error %s %w", "IssueCreateSubnetTx", err) - } - // Get the subnet ID from the transaction - subnetID := tx.ID() - log.Info("created subnet tx", zap.String("subnet-ID", subnetID.String())) - subnetIDs[i] = subnetID - } - return subnetIDs, nil -} - -// add the nodes in subnet participant as validators of the given subnets, in case they are not -// the validation starts as soon as possible and its duration is as long as possible, that is, -// it ends at the time the primary network validation ends for the node -func (ln *localNetwork) addSubnetValidators( - ctx context.Context, - platformCli platformvm.Client, - w *wallet, - subnetIDs []ids.ID, - subnetSpecs []network.SubnetSpec, -) error { - ln.log.Info(luxlog.Green.Wrap("adding the nodes as subnet validators")) - for i, subnetID := range subnetIDs { - ctx, cancel := createDefaultCtx(ctx) - vs, err := platformCli.GetCurrentValidators(ctx, constants.PrimaryNetworkID, nil) - cancel() - if err != nil { - return err - } - primaryValidatorsEndtime := make(map[ids.NodeID]time.Time) - for _, v := range vs { - primaryValidatorsEndtime[v.NodeID] = time.Unix(int64(v.EndTime), 0) - } - ctx, cancel = createDefaultCtx(ctx) - vs, err = platformCli.GetCurrentValidators(ctx, subnetID, nil) - cancel() - if err != nil { - return err - } - subnetValidators := set.Set[ids.NodeID]{} - for _, v := range vs { - subnetValidators.Add(v.NodeID) - } - participants := subnetSpecs[i].Participants - for _, nodeName := range participants { - node, b := ln.nodes[nodeName] - if !b { - return fmt.Errorf("participant node %s is not in network nodes", nodeName) - } - nodeID := node.GetNodeID() - if isValidator := subnetValidators.Contains(nodeID); isValidator { - continue - } - _, cancel := createDefaultCtx(ctx) - tx, err := w.pWallet.IssueAddNetValidatorTx( - &txs.NetValidator{ - Validator: txs.Validator{ - NodeID: nodeID, - // reasonable delay in most/slow test environments - Start: uint64(time.Now().Add(validationStartOffset).Unix()), - End: uint64(primaryValidatorsEndtime[nodeID].Unix()), - Wght: subnetValidatorsWeight, - }, - Net: subnetID, - }, - defaultPoll, - ) - cancel() - if err != nil { - return fmt.Errorf("P-Wallet Tx Error %s %w, node ID %s, subnetID %s", "IssueAddSubnetValidatorTx", err, nodeID.String(), subnetID.String()) - } - ln.log.Info("added node as a subnet validator to subnet", - zap.String("node-name", nodeName), - zap.String("node-ID", nodeID.String()), - zap.String("subnet-ID", subnetID.String()), - zap.String("tx-ID", tx.ID().String()), - ) - } - } - return nil -} - -// waits until all nodes start validating the primary network -func (ln *localNetwork) waitPrimaryValidators( - ctx context.Context, - platformCli platformvm.Client, -) error { - ln.log.Info(luxlog.Green.Wrap("waiting for the nodes to become primary validators")) - for { - ready := true - ctx, cancel := createDefaultCtx(ctx) - vs, err := platformCli.GetCurrentValidators(ctx, constants.PrimaryNetworkID, nil) - cancel() - if err != nil { - return err - } - primaryValidators := set.Set[ids.NodeID]{} - for _, v := range vs { - primaryValidators.Add(v.NodeID) - } - for _, node := range ln.nodes { - nodeID := node.GetNodeID() - if isValidator := primaryValidators.Contains(nodeID); !isValidator { - ready = false - } - } - if ready { - return nil - } - select { - case <-ln.onStopCh: - return errAborted - case <-ctx.Done(): - return ctx.Err() - case <-time.After(waitForValidatorsPullFrequency): - } - } -} - -// waits until all subnet participants start validating the subnetID, for all given subnets -func (ln *localNetwork) waitSubnetValidators( - ctx context.Context, - platformCli platformvm.Client, - subnetIDs []ids.ID, - subnetSpecs []network.SubnetSpec, -) error { - ln.log.Info(luxlog.Green.Wrap("waiting for the nodes to become subnet validators")) - for { - ready := true - for i, subnetID := range subnetIDs { - ctx, cancel := createDefaultCtx(ctx) - vs, err := platformCli.GetCurrentValidators(ctx, subnetID, nil) - cancel() - if err != nil { - return err - } - subnetValidators := set.Set[ids.NodeID]{} - for _, v := range vs { - subnetValidators.Add(v.NodeID) - } - participants := subnetSpecs[i].Participants - for _, nodeName := range participants { - node, b := ln.nodes[nodeName] - if !b { - return fmt.Errorf("participant node %s is not in network nodes", nodeName) - } - nodeID := node.GetNodeID() - if isValidator := subnetValidators.Contains(nodeID); !isValidator { - ready = false - } - } - if !ready { - break - } - } - if ready { - return nil - } - select { - case <-ln.onStopCh: - return errAborted - case <-ctx.Done(): - return ctx.Err() - case <-time.After(waitForValidatorsPullFrequency): - } - } -} - -// reload VM plugins on all nodes -func (ln *localNetwork) reloadVMPlugins(ctx context.Context) error { - ln.log.Info(luxlog.Green.Wrap("reloading plugin binaries")) - for _, node := range ln.nodes { - if node.paused { - continue - } - uri := fmt.Sprintf("http://%s:%d", node.GetURL(), node.GetAPIPort()) - adminCli := admin.NewClient(uri) - ctx, cancel := createDefaultCtx(ctx) - _, failedVMs, err := adminCli.LoadVMs(ctx) - cancel() - if err != nil { - return err - } - if len(failedVMs) > 0 { - return fmt.Errorf("%d VMs failed to load: %v", len(failedVMs), failedVMs) - } - } - return nil -} - -func createBlockchainTxs( - ctx context.Context, - chainSpecs []network.BlockchainSpec, - w *wallet, - log luxlog.Logger, -) ([]*txs.Tx, error) { - fmt.Println() - log.Info(luxlog.Green.Wrap("creating tx for each custom chain")) - blockchainTxs := make([]*txs.Tx, len(chainSpecs)) - for i, chainSpec := range chainSpecs { - vmName := chainSpec.VMName - vmID, err := utils.VMID(vmName) - if err != nil { - return nil, err - } - genesisBytes := chainSpec.Genesis - - log.Info("creating blockchain tx", - zap.String("vm-name", vmName), - zap.String("vm-ID", vmID.String()), - zap.Int("bytes length of genesis", len(genesisBytes)), - ) - ctx, cancel := createDefaultCtx(ctx) - defer cancel() - subnetID, err := ids.FromString(*chainSpec.SubnetID) - if err != nil { - return nil, err - } - tx, err := w.pWallet.IssueCreateChainTx( - subnetID, - genesisBytes, - vmID, - nil, - vmName, - common.WithContext(ctx), - ) - if err != nil { - return nil, fmt.Errorf("failure creating blockchain tx: %w", err) - } - - blockchainTxs[i] = tx - } - - return blockchainTxs, nil -} - -func (ln *localNetwork) setBlockchainConfigFiles( - ctx context.Context, - chainSpecs []network.BlockchainSpec, - blockchainTxs []*txs.Tx, - subnetIDs []ids.ID, - subnetSpecs []network.SubnetSpec, - log luxlog.Logger, -) (set.Set[string], error) { - fmt.Println() - log.Info(luxlog.Green.Wrap("creating config files for each custom chain")) - nodesToRestart := set.Set[string]{} - for i, chainSpec := range chainSpecs { - // get subnet participants - participants := []string{} - chainSubnetID, err := ids.FromString(*chainSpec.SubnetID) - if err != nil { - return nil, err - } - for j, newSubnetID := range subnetIDs { - if chainSubnetID == newSubnetID { - // subnet is new, use participants from spec - participants = subnetSpecs[j].Participants - } - } - if len(participants) == 0 { - // get participants from network - nodeNames, err := ln.getSubnetValidatorsNodenames(ctx, chainSubnetID) - if err != nil { - return nil, err - } - participants = nodeNames - } - chainAlias := blockchainTxs[i].ID().String() - // update config info. set defaults and node specifics - if chainSpec.ChainConfig != nil || len(chainSpec.PerNodeChainConfig) != 0 { - for _, nodeName := range participants { - _, b := ln.nodes[nodeName] - if !b { - return nil, fmt.Errorf("participant node %s is not in network nodes", nodeName) - } - chainConfig := chainSpec.ChainConfig - if cfg, ok := chainSpec.PerNodeChainConfig[nodeName]; ok { - chainConfig = cfg - } - ln.nodes[nodeName].config.ChainConfigFiles[chainAlias] = string(chainConfig) - nodesToRestart.Add(nodeName) - } - } - if chainSpec.NetworkUpgrade != nil { - for _, nodeName := range participants { - _, b := ln.nodes[nodeName] - if !b { - return nil, fmt.Errorf("participant node %s is not in network nodes", nodeName) - } - ln.nodes[nodeName].config.UpgradeConfigFiles[chainAlias] = string(chainSpec.NetworkUpgrade) - nodesToRestart.Add(nodeName) - } - } - } - return nodesToRestart, nil -} - -func (ln *localNetwork) setSubnetConfigFiles( - subnetIDs []ids.ID, - subnetSpecs []network.SubnetSpec, -) error { - for i, subnetID := range subnetIDs { - participants := subnetSpecs[i].Participants - subnetConfig := subnetSpecs[i].SubnetConfig - if subnetConfig != nil { - for _, nodeName := range participants { - _, b := ln.nodes[nodeName] - if !b { - return fmt.Errorf("participant node %s is not in network nodes", nodeName) - } - ln.nodes[nodeName].config.SubnetConfigFiles[subnetID.String()] = string(subnetConfig) - } - } - } - return nil -} - -func (*localNetwork) createBlockchains( - ctx context.Context, - chainSpecs []network.BlockchainSpec, - blockchainTxs []*txs.Tx, - w *wallet, - log luxlog.Logger, -) error { - fmt.Println() - log.Info(luxlog.Green.Wrap("creating each custom chain")) - for i, chainSpec := range chainSpecs { - vmName := chainSpec.VMName - vmID, err := utils.VMID(vmName) - if err != nil { - return err - } - log.Info("creating blockchain", - zap.String("vm-name", vmName), - zap.String("vm-ID", vmID.String()), - ) - - ctx, cancel := createDefaultCtx(ctx) - defer cancel() - - err = w.pWallet.IssueTx( - blockchainTxs[i], - common.WithContext(ctx), - defaultPoll, - ) - if err != nil { - return fmt.Errorf("P-Wallet Tx Error %s %w, blockchainID %s", "IssueCreateBlockchainTx", err, blockchainTxs[i].ID().String()) - } - blockchainID := blockchainTxs[i].ID() - - log.Info("created a new blockchain", - zap.String("vm-name", vmName), - zap.String("vm-ID", vmID.String()), - zap.String("blockchain-ID", blockchainID.String()), - ) - } - - return nil -} - -func createDefaultCtx(ctx context.Context) (context.Context, context.CancelFunc) { - if ctx == nil { - ctx = context.Background() - } - return context.WithTimeout(ctx, defaultTimeout) -} diff --git a/local/default/cchain_config.json b/local/default/cchain_config.json index cadd428f..e0422f54 100644 --- a/local/default/cchain_config.json +++ b/local/default/cchain_config.json @@ -2,14 +2,16 @@ "consensus-api-enabled": false, "geth-admin-api-enabled": false, "geth-admin-api-dir": "", - "eth-apis": ["eth", "eth-filter", "net", "web3", "internal-eth", "internal-blockchain", "internal-transaction", "internal-account"], + "eth-apis": ["eth", "eth-filter", "net", "web3", "internal-eth", "internal-blockchain", "internal-transaction", "internal-account", "admin"], + "admin-api-enabled": true, "continuous-profiler-dir": "", "continuous-profiler-frequency": 900000000000, "continuous-profiler-max-files": 5, "rpc-gas-cap": 50000000, "rpc-tx-fee-cap": 100, "preimages-enabled": false, - "pruning-enabled": true, + "pruning-enabled": false, + "allow-missing-tries": true, "snapshot-async": true, "snapshot-verification-enabled": false, "metrics-enabled": false, diff --git a/local/default/flags.json b/local/default/flags.json index e6a0e06e..642f042f 100644 --- a/local/default/flags.json +++ b/local/default/flags.json @@ -7,8 +7,7 @@ "index-enabled":true, "log-display-level":"ERROR", "log-level": "INFO", - "sybil-protection-enabled": false, - "sybil-protection-disabled-weight": 100, + "sybil-protection-enabled": true, "network-health-min-conn-peers": 0, "track-all-chains": true } diff --git a/local/genesis_config.go b/local/genesis_config.go index 6989a25d..a66d9c27 100644 --- a/local/genesis_config.go +++ b/local/genesis_config.go @@ -10,6 +10,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" "github.com/luxfi/constants" @@ -335,6 +336,85 @@ func NewLocalConfig(binaryPath string, numNodes uint32) (network.Config, error) return NewConfigForNetwork(binaryPath, numNodes, configs.CustomID) } +// NewCanonicalMainnetConfig creates a mainnet network config using the CANONICAL genesis bytes. +// This function loads the pre-serialized genesis.json file directly, ensuring byte-for-byte +// deterministic output. Use this to avoid "db contains invalid genesis hash" errors on restart. +// +// CRITICAL: For mainnet/testnet, always use this function or NewCanonicalTestnetConfig +// rather than functions that regenerate genesis (which causes non-deterministic bytes). +func NewCanonicalMainnetConfig(binaryPath string, numNodes uint32) (network.Config, error) { + return newCanonicalConfig(binaryPath, numNodes, configs.MainnetID, 9630) +} + +// NewCanonicalTestnetConfig creates a testnet network config using the CANONICAL genesis bytes. +// See NewCanonicalMainnetConfig for details on why this is critical. +func NewCanonicalTestnetConfig(binaryPath string, numNodes uint32) (network.Config, error) { + return newCanonicalConfig(binaryPath, numNodes, configs.TestnetID, 9640) +} + +// NewCanonicalDevnetConfig creates a devnet network config using the CANONICAL genesis bytes. +// See NewCanonicalMainnetConfig for details on why this is critical. +func NewCanonicalDevnetConfig(binaryPath string, numNodes uint32) (network.Config, error) { + return newCanonicalConfig(binaryPath, numNodes, configs.DevnetID, 9650) +} + +// NewCanonicalCustomConfig creates a custom (local) network config using the CANONICAL genesis bytes. +// See NewCanonicalMainnetConfig for details on why this is critical. +func NewCanonicalCustomConfig(binaryPath string, numNodes uint32) (network.Config, error) { + return newCanonicalConfig(binaryPath, numNodes, configs.CustomID, 9660) +} + +// newCanonicalConfig creates a network config with canonical (pre-serialized) genesis bytes +// and loads validator keys from ~/.lux/keys/node{0..n-1}/ +func newCanonicalConfig(binaryPath string, numNodes uint32, networkID uint32, portBase int) (network.Config, error) { + // Load CANONICAL genesis bytes - no parsing/re-serialization + genesisBytes, err := configs.GetCanonicalGenesisBytes(networkID) + if err != nil { + return network.Config{}, fmt.Errorf("failed to load canonical genesis: %w", err) + } + + // Start with default config + netConfig := NewDefaultConfig(binaryPath) + netConfig.Genesis = string(genesisBytes) + + // Load validator keys from ~/.lux/keys/ + keysDir := os.Getenv("LUX_KEYS_DIR") + if keysDir == "" { + keysDir = validatorKeysDir() + } + ks := keys.NewKeyStore(keysDir) + + // Load keys for each node + nodeConfigs := make([]node.Config, numNodes) + for i := uint32(0); i < numNodes; i++ { + name := fmt.Sprintf("node%d", i) + vk, err := ks.Load(name) + if err != nil { + return network.Config{}, fmt.Errorf("failed to load validator key %s from %s: %w (run 'lux key generate' first)", name, keysDir, err) + } + + port := portBase + int(i)*2 + nodeConfigs[i] = node.Config{ + Flags: map[string]interface{}{ + config.HTTPPortKey: port, + config.StakingPortKey: port + 1, + }, + StakingKey: string(vk.StakerKey), + StakingCert: string(vk.StakerCert), + StakingSigningKey: base64.StdEncoding.EncodeToString(vk.BLSSecretKey), + IsBeacon: true, + ChainConfigFiles: map[string]string{}, + UpgradeConfigFiles: map[string]string{}, + PChainConfigFiles: map[string]string{}, + } + fmt.Printf(" Loaded validator %d: %s\n", i, vk.NodeID.String()) + } + netConfig.NodeConfigs = nodeConfigs + + fmt.Printf("āœ… Loaded canonical genesis for network %d with %d validators\n", networkID, numNodes) + return netConfig, nil +} + // NewConfigForNetworkWithCustomGenesis creates a network config with a custom genesis string. // Use this for networks not defined in the configs package or for testing. func NewConfigForNetworkWithCustomGenesis(binaryPath string, numNodes uint32, genesisJSON string) (network.Config, error) { @@ -394,11 +474,15 @@ func NewConfigForNetworkWithCustomGenesis(binaryPath string, numNodes uint32, ge // - ec/private.key for P-Chain addresses (optional) func NewConfigWithPreExistingKeys(binaryPath string, networkID uint32, keysDir string) (network.Config, error) { // Determine port base based on network ID - // Mainnet (96369): 9630 base - // Testnet (96368): 9640 base + // Mainnet (1): 9630 base + // Testnet (2): 9640 base + // Devnet (3): 9650 base portBase := 9630 - if networkID == configs.TestnetChainID { + switch networkID { + case constants.TestnetID: // 2 portBase = 9640 + case constants.DevnetID: // 3 + portBase = 9650 } // Get genesis for the specified network @@ -920,6 +1004,38 @@ func NewConfigFromMnemonic(binaryPath string, networkID uint32, numNodes uint32) genesis["initialStakers"] = initialStakers genesis["allocations"] = allocations + // CRITICAL: Also update xChainGenesis.allocations! + // The X-chain reads allocations from the embedded xChainGenesis JSON string, + // NOT from the top-level allocations array. We must update both. + if xChainGenesisStr, ok := genesis["xChainGenesis"].(string); ok { + var xChainGenesis map[string]interface{} + if err := json.Unmarshal([]byte(xChainGenesisStr), &xChainGenesis); err == nil { + // Build X-chain specific allocations (only X-chain addresses, use avaxAddr format) + xAllocations := make([]map[string]interface{}, 0) + for _, alloc := range allocations { + allocMap := alloc.(map[string]interface{}) + luxAddr, _ := allocMap["luxAddr"].(string) + // Only include X-chain allocations (start with "X-") + if strings.HasPrefix(luxAddr, "X-") { + initialAmount, _ := allocMap["initialAmount"].(uint64) + if initialAmount > 0 { + xAllocations = append(xAllocations, map[string]interface{}{ + "avaxAddr": luxAddr, // X-chain uses avaxAddr field name + "ethAddr": allocMap["ethAddr"], + "initialAmount": initialAmount, + "unlockSchedule": []interface{}{}, + }) + } + } + } + xChainGenesis["allocations"] = xAllocations + if updatedXChain, err := json.Marshal(xChainGenesis); err == nil { + genesis["xChainGenesis"] = string(updatedXChain) + fmt.Printf("āœ… Updated xChainGenesis with %d allocations\n", len(xAllocations)) + } + } + } + // IMPORTANT: Set initialStakedFunds to EMPTY so that P-chain allocations // are NOT filtered by the builder. The builder filters allocations where // the underlying ShortID matches initialStakedFunds, which would incorrectly diff --git a/local/helpers.go b/local/helpers.go index ea746dfa..8df0b729 100644 --- a/local/helpers.go +++ b/local/helpers.go @@ -11,9 +11,11 @@ import ( "path/filepath" "time" - "github.com/luxfi/netrunner/network/node" - "github.com/luxfi/node/config" + "github.com/luxfi/constants" "github.com/luxfi/log" + "github.com/luxfi/netrunner/network/node" + "github.com/luxfi/netrunner/utils" + "github.com/luxfi/node/config" ) func init() { @@ -95,12 +97,50 @@ func writeFiles(networkID uint32, genesis []byte, nodeRootDir string, nodeConfig contents: decodedStakingSigningKey, }, } - // Always write genesis file if provided, even for LocalID - // This allows custom genesis (e.g., with mnemonic-derived validators) to override defaults - if len(genesis) > 0 { + // Check if this is a resume from existing state: + // If genesis.json already exists AND db directory has chain data, preserve existing genesis + // This prevents the "db contains invalid genesis hash" error when resuming from snapshots + // EXCEPTION: For canonical networks (mainnet/testnet), if the existing genesis has a different + // networkID, we must clean the db and write the correct canonical genesis. + existingGenesisPath := filepath.Join(nodeRootDir, genesisFileName) + dbDir := filepath.Join(nodeRootDir, "db") + shouldPreserveGenesis := false + + if _, err := os.Stat(existingGenesisPath); err == nil { + // Genesis exists, check if db has data (indicating we're resuming) + if entries, err := os.ReadDir(dbDir); err == nil && len(entries) > 0 { + shouldPreserveGenesis = true + + // For canonical networks, verify the existing genesis matches + if networkID == constants.MainnetID || networkID == constants.TestnetID { + existingGenesisBytes, readErr := os.ReadFile(existingGenesisPath) + if readErr == nil { + existingNetworkID, parseErr := utils.NetworkIDFromGenesis(existingGenesisBytes) + if parseErr == nil && existingNetworkID != networkID { + // NetworkID mismatch! Clean db and write correct canonical genesis + log.Info("Genesis networkID mismatch for canonical network, cleaning db", + log.Uint32("existing", existingNetworkID), + log.Uint32("target", networkID), + log.String("dbDir", dbDir)) + if err := os.RemoveAll(dbDir); err != nil { + return nil, fmt.Errorf("failed to clean db directory for canonical network: %w", err) + } + // Recreate empty db directory + if err := os.MkdirAll(dbDir, 0o750); err != nil { + return nil, fmt.Errorf("failed to recreate db directory: %w", err) + } + shouldPreserveGenesis = false + } + } + } + } + } + + if len(genesis) > 0 && !shouldPreserveGenesis { + // Write new genesis (fresh start or custom genesis with mnemonic-derived validators) files = append(files, file{ - flagValue: filepath.Join(nodeRootDir, genesisFileName), - path: filepath.Join(nodeRootDir, genesisFileName), + flagValue: existingGenesisPath, + path: existingGenesisPath, pathKey: config.GenesisFileKey, contents: genesis, }) @@ -120,6 +160,10 @@ func writeFiles(networkID uint32, genesis []byte, nodeRootDir string, nodeConfig return nil, fmt.Errorf("couldn't write file at %q: %w", f.path, err) } } + // If preserving existing genesis, add the flag without writing the file + if shouldPreserveGenesis { + flags[config.GenesisFileKey] = existingGenesisPath + } // chain configs dir chainConfigDir := filepath.Join(nodeRootDir, chainConfigSubDir) if err := os.MkdirAll(chainConfigDir, 0o750); err != nil { @@ -139,6 +183,13 @@ func writeFiles(networkID uint32, genesis []byte, nodeRootDir string, nodeConfig return nil, fmt.Errorf("couldn't write file at %q: %w", chainConfigPath, err) } } + // genesis files for EVM chains + for chainAlias, genesisFile := range nodeConfig.GenesisConfigFiles { + genesisPath := filepath.Join(chainConfigDir, chainAlias, genesisFileName) + if err := createFileAndWrite(genesisPath, []byte(genesisFile)); err != nil { + return nil, fmt.Errorf("couldn't write genesis file at %q: %w", genesisPath, err) + } + } // network upgrades for chainAlias, chainUpgradeFile := range nodeConfig.UpgradeConfigFiles { chainUpgradePath := filepath.Join(chainConfigDir, chainAlias, upgradeConfigFileName) diff --git a/local/mnemonic_test.go b/local/mnemonic_test.go new file mode 100644 index 00000000..d55f2021 --- /dev/null +++ b/local/mnemonic_test.go @@ -0,0 +1,72 @@ +package local + +import ( + "fmt" + "os" + "testing" + + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg" + "github.com/luxfi/go-bip39" + luxcrypto "github.com/luxfi/crypto/secp256k1" + "golang.org/x/crypto/sha3" +) + +func TestMnemonicDerivation(t *testing.T) { + mnemonic := os.Getenv("LUX_MNEMONIC") + if mnemonic == "" { + t.Skip("LUX_MNEMONIC not set") + } + + fmt.Printf("Mnemonic: %s...\n", mnemonic[:20]) + + if !bip39.IsMnemonicValid(mnemonic) { + t.Fatal("Invalid mnemonic") + } + + seed := bip39.NewSeed(mnemonic, "") + + // Derive using BIP44 m/44'/60'/0'/0/0 + masterKey, _ := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) + purpose, _ := masterKey.Derive(hdkeychain.HardenedKeyStart + 44) + coinType, _ := purpose.Derive(hdkeychain.HardenedKeyStart + 60) + account, _ := coinType.Derive(hdkeychain.HardenedKeyStart + 0) + change, _ := account.Derive(0) + addressKey, _ := change.Derive(0) + + ecPrivKey, _ := addressKey.ECPrivKey() + privKeyBytes := ecPrivKey.Serialize() + + privKey, err := luxcrypto.ToPrivateKey(privKeyBytes) + if err != nil { + t.Fatalf("Error: %v", err) + } + pubKey := privKey.PublicKey() + shortID := pubKey.Address() + + // Get Ethereum address + pubKeyBytes := pubKey.Bytes() + hash := sha3.NewLegacyKeccak256() + hash.Write(pubKeyBytes[1:]) // skip the 0x04 prefix + ethAddr := hash.Sum(nil)[12:] + + fmt.Printf("\n=== Key Derivation Results (m/44'/60'/0'/0/0) ===\n") + fmt.Printf("Lux Short ID: %s\n", shortID.String()) + fmt.Printf("Ethereum Address: 0x%x\n", ethAddr) + fmt.Printf("Expected Treasury: 0x9011E888251AB053B7bD1cdB598Db4f9DEd94714\n") + + // Also derive using coin type 9000 for comparison + coinType9000, _ := purpose.Derive(hdkeychain.HardenedKeyStart + 9000) + account9000, _ := coinType9000.Derive(hdkeychain.HardenedKeyStart + 0) + change9000, _ := account9000.Derive(0) + addressKey9000, _ := change9000.Derive(0) + ecPrivKey9000, _ := addressKey9000.ECPrivKey() + privKey9000, _ := luxcrypto.ToPrivateKey(ecPrivKey9000.Serialize()) + shortID9000 := privKey9000.PublicKey().Address() + + fmt.Printf("\n=== Comparison: Coin Type 9000 (m/44'/9000'/0'/0/0) ===\n") + fmt.Printf("Lux Short ID (9000): %s\n", shortID9000.String()) + + fmt.Printf("\n=== Genesis Treasury P-chain Address ===\n") + fmt.Printf("Expected: P-lux1jqg73zp9r2c98daarnd4nrd5l80dj3c5eha5fl\n") +} diff --git a/local/network.go b/local/network.go index c70d985a..e73b4aa6 100644 --- a/local/network.go +++ b/local/network.go @@ -55,7 +55,7 @@ const ( // healthCheckFreq reduced from 3s to 1s for faster health polling healthCheckFreq = 1 * time.Second DefaultNumNodes = 5 - snapshotPrefix = "anr-snapshot-" + snapshotPrefix = "lux-snapshot-" networkRootDirPrefix = "network" defaultDBSubdir = "db" defaultLogsSubdir = "logs" @@ -114,6 +114,8 @@ type localNetwork struct { binaryPath string // chain config files to use per default chainConfigFiles map[string]string + // genesis config files for EVM chains (per blockchain ID) + genesisConfigFiles map[string]string // upgrade config files to use per default upgradeConfigFiles map[string]string // P-chain config files to use per default @@ -546,14 +548,14 @@ func (ln *localNetwork) loadConfig(ctx context.Context, networkConfig network.Co if ln.chainConfigFiles == nil { ln.chainConfigFiles = map[string]string{} } + ln.genesisConfigFiles = networkConfig.GenesisConfigFiles + if ln.genesisConfigFiles == nil { + ln.genesisConfigFiles = map[string]string{} + } ln.upgradeConfigFiles = networkConfig.UpgradeConfigFiles if ln.upgradeConfigFiles == nil { ln.upgradeConfigFiles = map[string]string{} } - ln.chainConfigFiles = networkConfig.ChainConfigFiles - if ln.chainConfigFiles == nil { - ln.chainConfigFiles = map[string]string{} - } // Sort node configs so beacons start first var nodeConfigs []node.Config diff --git a/local/node.go b/local/node.go index fd21349d..419c4044 100644 --- a/local/node.go +++ b/local/node.go @@ -12,15 +12,16 @@ import ( "github.com/luxfi/netrunner/api" "github.com/luxfi/netrunner/network/node" "github.com/luxfi/netrunner/network/node/status" + "github.com/luxfi/constants" "github.com/luxfi/ids" "github.com/luxfi/node/message" "github.com/luxfi/node/network/peer" "github.com/luxfi/node/network/throttling" "github.com/luxfi/node/network/tracker" + "github.com/luxfi/node/utils/compression" "github.com/luxfi/consensus/validator" // package name is validators "github.com/luxfi/node/staking" "github.com/luxfi/node/utils" - "github.com/luxfi/constants" "github.com/luxfi/crypto/bls" "github.com/luxfi/log" "github.com/luxfi/math/set" @@ -99,7 +100,7 @@ func (node *localNode) AttachPeer(ctx context.Context, router peer.InboundHandle } mc, err := message.NewCreator( prometheus.NewRegistry(), - constants.DefaultNetworkCompressionType, + compression.TypeZstd, 10*time.Second, ) if err != nil { diff --git a/local/snapshot.go b/local/snapshot.go index 14b3121e..d8db0881 100644 --- a/local/snapshot.go +++ b/local/snapshot.go @@ -151,6 +151,7 @@ func (ln *localNetwork) SaveSnapshot(ctx context.Context, snapshotName string) ( NodeConfigs: []node.Config{}, BinaryPath: ln.binaryPath, ChainConfigFiles: ln.chainConfigFiles, + GenesisConfigFiles: ln.genesisConfigFiles, UpgradeConfigFiles: ln.upgradeConfigFiles, PChainConfigFiles: ln.pChainConfigFiles, } @@ -293,6 +294,122 @@ func (ln *localNetwork) loadSnapshot( return ln.loadConfig(ctx, networkConfig) } +// SaveHotSnapshot saves a snapshot without stopping the network. +// Uses copy-on-write on filesystems that support it (APFS on macOS). +// WARNING: Hot snapshots may have minor inconsistencies if writes occur during copy. +// For guaranteed consistency, use SaveSnapshot which stops the network. +func (ln *localNetwork) SaveHotSnapshot(ctx context.Context, snapshotName string) (string, error) { + ln.lock.RLock() // Read lock - doesn't block network operations + defer ln.lock.RUnlock() + + if ln.stopCalled() { + return "", network.ErrStopped + } + if len(snapshotName) == 0 { + return "", fmt.Errorf("invalid snapshotName %q", snapshotName) + } + // check if snapshot already exists + snapshotDir := filepath.Join(ln.snapshotsDir, snapshotPrefix+snapshotName) + if _, err := os.Stat(snapshotDir); err == nil { + return "", fmt.Errorf("snapshot %q already exists", snapshotName) + } + + // Collect node info (read-only, safe during operation) + nodesConfig := map[string]node.Config{} + nodesDBDir := map[string]string{} + for nodeName, node := range ln.nodes { + nodeConfig := node.config + nodeConfig.Flags = maps.Clone(nodeConfig.Flags) + nodesConfig[nodeName] = nodeConfig + nodesDBDir[nodeName] = node.GetDbDir() + } + + // 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 + networkConfigFlags := maps.Clone(ln.flags) + delete(networkConfigFlags, config.DataDirKey) + delete(networkConfigFlags, config.LogsDirKey) + for nodeName, nodeConfig := range nodesConfig { + if nodeConfig.ConfigFile != "" { + var err error + nodeConfig.ConfigFile, err = utils.SetJSONKey(nodeConfig.ConfigFile, config.LogsDirKey, "") + if err != nil { + return "", err + } + } + delete(nodeConfig.Flags, config.DataDirKey) + delete(nodeConfig.Flags, config.LogsDirKey) + nodesConfig[nodeName] = nodeConfig + } + + // Sync filesystem to flush pending writes + syncFilesystem() + + // Create main snapshot dirs + snapshotDBDir := filepath.Join(snapshotDir, defaultDBSubdir) + if err := os.MkdirAll(snapshotDBDir, os.ModePerm); err != nil { + return "", err + } + + // Copy db directories (hot - network still running) + // On APFS (macOS), dircopy uses clonefile which is CoW and instant + 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) + } + } + + // Save network config + networkConfig := network.Config{ + Genesis: string(ln.genesis), + Flags: networkConfigFlags, + NodeConfigs: []node.Config{}, + BinaryPath: ln.binaryPath, + ChainConfigFiles: ln.chainConfigFiles, + GenesisConfigFiles: ln.genesisConfigFiles, + UpgradeConfigFiles: ln.upgradeConfigFiles, + PChainConfigFiles: ln.pChainConfigFiles, + } + networkConfig.NodeConfigs = append(networkConfig.NodeConfigs, maps.Values(nodesConfig)...) + networkConfigJSON, err := json.MarshalIndent(networkConfig, "", " ") + if err != nil { + return "", err + } + if err := createFileAndWrite(filepath.Join(snapshotDir, "network.json"), networkConfigJSON); err != nil { + return "", err + } + + // Save dynamic network state + chainID2ElasticChainID := map[string]string{} + for chainID, elasticChainID := range ln.chainID2ElasticChainID { + chainID2ElasticChainID[chainID.String()] = elasticChainID.String() + } + networkState := NetworkState{ + ChainID2ElasticChainID: chainID2ElasticChainID, + } + networkStateJSON, err := json.MarshalIndent(networkState, "", " ") + if err != nil { + return "", err + } + if err := createFileAndWrite(filepath.Join(snapshotDir, "state.json"), networkStateJSON); err != nil { + return "", err + } + + ln.logger.Info("Hot snapshot saved", log.String("snapshot", snapshotName), log.String("path", snapshotDir)) + return snapshotDir, nil +} + // Remove network snapshot func (ln *localNetwork) RemoveSnapshot(snapshotName string) error { snapshotDir := filepath.Join(ln.snapshotsDir, snapshotPrefix+snapshotName) diff --git a/network/config.go b/network/config.go index 5906f3f1..658b7718 100644 --- a/network/config.go +++ b/network/config.go @@ -71,6 +71,8 @@ type Config struct { BinaryPath string `json:"binaryPath"` // Chain config files to use per default, if not specified in node config ChainConfigFiles map[string]string `json:"chainConfigFiles"` + // Genesis config files for EVM chains (per blockchain ID) + GenesisConfigFiles map[string]string `json:"genesisConfigFiles"` // Upgrade config files to use per default, if not specified in node config UpgradeConfigFiles map[string]string `json:"upgradeConfigFiles"` // P-Chain config files to use per default, if not specified in node config diff --git a/network/network.go b/network/network.go index 30953494..032e32b3 100644 --- a/network/network.go +++ b/network/network.go @@ -99,6 +99,10 @@ type Network interface { // Network is stopped in order to do a safe preservation // Returns the full local path to the snapshot dir SaveSnapshot(context.Context, string) (string, error) + // SaveHotSnapshot saves a snapshot without stopping the network + // Uses CoW on APFS (macOS) for instant snapshots + // WARNING: May have minor inconsistencies if writes occur during copy + SaveHotSnapshot(context.Context, string) (string, error) // Remove network snapshot RemoveSnapshot(string) error // Get name of available snapshots diff --git a/network/node/node.go b/network/node/node.go index 7b4b2087..e7f90bfd 100644 --- a/network/node/node.go +++ b/network/node/node.go @@ -79,6 +79,8 @@ type Config struct { ConfigFile string `json:"configFile"` // May be nil. ChainConfigFiles map[string]string `json:"chainConfigFiles"` + // May be nil. Genesis files for EVM chains (genesis.json) + GenesisConfigFiles map[string]string `json:"genesisConfigFiles"` // May be nil. UpgradeConfigFiles map[string]string `json:"upgradeConfigFiles"` // May be nil. P-Chain chain (formerly subnet) config files. diff --git a/rpcpb/rpc.pb.go b/rpcpb/rpc.pb.go index ff66c350..5aeacb0b 100644 --- a/rpcpb/rpc.pb.go +++ b/rpcpb/rpc.pb.go @@ -3796,7 +3796,7 @@ const file_rpcpb_rpc_proto_rawDesc = "" + "\x18GetSnapshotNamesResponse\x12%\n" + "\x0esnapshot_names\x18\x01 \x03(\tR\rsnapshotNames2S\n" + "\vPingService\x12D\n" + - "\x04Ping\x12\x12.rpcpb.PingRequest\x1a\x13.rpcpb.PingResponse\"\x13\x82\xd3\xe4\x93\x02\r:\x01*\"\b/v1/ping2\xe7\x14\n" + + "\x04Ping\x12\x12.rpcpb.PingRequest\x1a\x13.rpcpb.PingResponse\"\x13\x82\xd3\xe4\x93\x02\r:\x01*\"\b/v1/ping2\xdb\x15\n" + "\x0eControlService\x12d\n" + "\n" + "RPCVersion\x12\x18.rpcpb.RPCVersionRequest\x1a\x19.rpcpb.RPCVersionResponse\"!\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/v1/control/rpcversion\x12P\n" + @@ -3822,7 +3822,8 @@ const file_rpcpb_rpc_proto_rawDesc = "" + "\n" + "AttachPeer\x12\x18.rpcpb.AttachPeerRequest\x1a\x19.rpcpb.AttachPeerResponse\"!\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/v1/control/attachpeer\x12\x88\x01\n" + "\x13SendOutboundMessage\x12!.rpcpb.SendOutboundMessageRequest\x1a\".rpcpb.SendOutboundMessageResponse\"*\x82\xd3\xe4\x93\x02$:\x01*\"\x1f/v1/control/sendoutboundmessage\x12l\n" + - "\fSaveSnapshot\x12\x1a.rpcpb.SaveSnapshotRequest\x1a\x1b.rpcpb.SaveSnapshotResponse\"#\x82\xd3\xe4\x93\x02\x1d:\x01*\"\x18/v1/control/savesnapshot\x12l\n" + + "\fSaveSnapshot\x12\x1a.rpcpb.SaveSnapshotRequest\x1a\x1b.rpcpb.SaveSnapshotResponse\"#\x82\xd3\xe4\x93\x02\x1d:\x01*\"\x18/v1/control/savesnapshot\x12r\n" + + "\x0fSaveHotSnapshot\x12\x1a.rpcpb.SaveSnapshotRequest\x1a\x1b.rpcpb.SaveSnapshotResponse\"&\x82\xd3\xe4\x93\x02 :\x01*\"\x1b/v1/control/savehotsnapshot\x12l\n" + "\fLoadSnapshot\x12\x1a.rpcpb.LoadSnapshotRequest\x1a\x1b.rpcpb.LoadSnapshotResponse\"#\x82\xd3\xe4\x93\x02\x1d:\x01*\"\x18/v1/control/loadsnapshot\x12t\n" + "\x0eRemoveSnapshot\x12\x1c.rpcpb.RemoveSnapshotRequest\x1a\x1d.rpcpb.RemoveSnapshotResponse\"%\x82\xd3\xe4\x93\x02\x1f:\x01*\"\x1a/v1/control/removesnapshot\x12|\n" + "\x10GetSnapshotNames\x12\x1e.rpcpb.GetSnapshotNamesRequest\x1a\x1f.rpcpb.GetSnapshotNamesResponse\"'\x82\xd3\xe4\x93\x02!:\x01*\"\x1c/v1/control/getsnapshotnamesB\"Z github.com/luxfi/netrunner;rpcpbb\x06proto3" @@ -3993,36 +3994,38 @@ var file_rpcpb_rpc_proto_depIdxs = []int32{ 50, // 68: rpcpb.ControlService.AttachPeer:input_type -> rpcpb.AttachPeerRequest 52, // 69: rpcpb.ControlService.SendOutboundMessage:input_type -> rpcpb.SendOutboundMessageRequest 54, // 70: rpcpb.ControlService.SaveSnapshot:input_type -> rpcpb.SaveSnapshotRequest - 56, // 71: rpcpb.ControlService.LoadSnapshot:input_type -> rpcpb.LoadSnapshotRequest - 58, // 72: rpcpb.ControlService.RemoveSnapshot:input_type -> rpcpb.RemoveSnapshotRequest - 60, // 73: rpcpb.ControlService.GetSnapshotNames:input_type -> rpcpb.GetSnapshotNamesRequest - 1, // 74: rpcpb.PingService.Ping:output_type -> rpcpb.PingResponse - 11, // 75: rpcpb.ControlService.RPCVersion:output_type -> rpcpb.RPCVersionResponse - 12, // 76: rpcpb.ControlService.Start:output_type -> rpcpb.StartResponse - 25, // 77: rpcpb.ControlService.CreateBlockchains:output_type -> rpcpb.CreateBlockchainsResponse - 16, // 78: rpcpb.ControlService.TransformElasticChains:output_type -> rpcpb.TransformElasticChainsResponse - 19, // 79: rpcpb.ControlService.AddPermissionlessValidator:output_type -> rpcpb.AddPermissionlessValidatorResponse - 22, // 80: rpcpb.ControlService.RemoveChainValidator:output_type -> rpcpb.RemoveChainValidatorResponse - 27, // 81: rpcpb.ControlService.CreateChains:output_type -> rpcpb.CreateChainsResponse - 29, // 82: rpcpb.ControlService.Health:output_type -> rpcpb.HealthResponse - 31, // 83: rpcpb.ControlService.URIs:output_type -> rpcpb.URIsResponse - 33, // 84: rpcpb.ControlService.WaitForHealthy:output_type -> rpcpb.WaitForHealthyResponse - 35, // 85: rpcpb.ControlService.Status:output_type -> rpcpb.StatusResponse - 37, // 86: rpcpb.ControlService.StreamStatus:output_type -> rpcpb.StreamStatusResponse - 41, // 87: rpcpb.ControlService.RemoveNode:output_type -> rpcpb.RemoveNodeResponse - 47, // 88: rpcpb.ControlService.AddNode:output_type -> rpcpb.AddNodeResponse - 39, // 89: rpcpb.ControlService.RestartNode:output_type -> rpcpb.RestartNodeResponse - 43, // 90: rpcpb.ControlService.PauseNode:output_type -> rpcpb.PauseNodeResponse - 45, // 91: rpcpb.ControlService.ResumeNode:output_type -> rpcpb.ResumeNodeResponse - 49, // 92: rpcpb.ControlService.Stop:output_type -> rpcpb.StopResponse - 51, // 93: rpcpb.ControlService.AttachPeer:output_type -> rpcpb.AttachPeerResponse - 53, // 94: rpcpb.ControlService.SendOutboundMessage:output_type -> rpcpb.SendOutboundMessageResponse - 55, // 95: rpcpb.ControlService.SaveSnapshot:output_type -> rpcpb.SaveSnapshotResponse - 57, // 96: rpcpb.ControlService.LoadSnapshot:output_type -> rpcpb.LoadSnapshotResponse - 59, // 97: rpcpb.ControlService.RemoveSnapshot:output_type -> rpcpb.RemoveSnapshotResponse - 61, // 98: rpcpb.ControlService.GetSnapshotNames:output_type -> rpcpb.GetSnapshotNamesResponse - 74, // [74:99] is the sub-list for method output_type - 49, // [49:74] is the sub-list for method input_type + 54, // 71: rpcpb.ControlService.SaveHotSnapshot:input_type -> rpcpb.SaveSnapshotRequest + 56, // 72: rpcpb.ControlService.LoadSnapshot:input_type -> rpcpb.LoadSnapshotRequest + 58, // 73: rpcpb.ControlService.RemoveSnapshot:input_type -> rpcpb.RemoveSnapshotRequest + 60, // 74: rpcpb.ControlService.GetSnapshotNames:input_type -> rpcpb.GetSnapshotNamesRequest + 1, // 75: rpcpb.PingService.Ping:output_type -> rpcpb.PingResponse + 11, // 76: rpcpb.ControlService.RPCVersion:output_type -> rpcpb.RPCVersionResponse + 12, // 77: rpcpb.ControlService.Start:output_type -> rpcpb.StartResponse + 25, // 78: rpcpb.ControlService.CreateBlockchains:output_type -> rpcpb.CreateBlockchainsResponse + 16, // 79: rpcpb.ControlService.TransformElasticChains:output_type -> rpcpb.TransformElasticChainsResponse + 19, // 80: rpcpb.ControlService.AddPermissionlessValidator:output_type -> rpcpb.AddPermissionlessValidatorResponse + 22, // 81: rpcpb.ControlService.RemoveChainValidator:output_type -> rpcpb.RemoveChainValidatorResponse + 27, // 82: rpcpb.ControlService.CreateChains:output_type -> rpcpb.CreateChainsResponse + 29, // 83: rpcpb.ControlService.Health:output_type -> rpcpb.HealthResponse + 31, // 84: rpcpb.ControlService.URIs:output_type -> rpcpb.URIsResponse + 33, // 85: rpcpb.ControlService.WaitForHealthy:output_type -> rpcpb.WaitForHealthyResponse + 35, // 86: rpcpb.ControlService.Status:output_type -> rpcpb.StatusResponse + 37, // 87: rpcpb.ControlService.StreamStatus:output_type -> rpcpb.StreamStatusResponse + 41, // 88: rpcpb.ControlService.RemoveNode:output_type -> rpcpb.RemoveNodeResponse + 47, // 89: rpcpb.ControlService.AddNode:output_type -> rpcpb.AddNodeResponse + 39, // 90: rpcpb.ControlService.RestartNode:output_type -> rpcpb.RestartNodeResponse + 43, // 91: rpcpb.ControlService.PauseNode:output_type -> rpcpb.PauseNodeResponse + 45, // 92: rpcpb.ControlService.ResumeNode:output_type -> rpcpb.ResumeNodeResponse + 49, // 93: rpcpb.ControlService.Stop:output_type -> rpcpb.StopResponse + 51, // 94: rpcpb.ControlService.AttachPeer:output_type -> rpcpb.AttachPeerResponse + 53, // 95: rpcpb.ControlService.SendOutboundMessage:output_type -> rpcpb.SendOutboundMessageResponse + 55, // 96: rpcpb.ControlService.SaveSnapshot:output_type -> rpcpb.SaveSnapshotResponse + 55, // 97: rpcpb.ControlService.SaveHotSnapshot:output_type -> rpcpb.SaveSnapshotResponse + 57, // 98: rpcpb.ControlService.LoadSnapshot:output_type -> rpcpb.LoadSnapshotResponse + 59, // 99: rpcpb.ControlService.RemoveSnapshot:output_type -> rpcpb.RemoveSnapshotResponse + 61, // 100: rpcpb.ControlService.GetSnapshotNames:output_type -> rpcpb.GetSnapshotNamesResponse + 75, // [75:101] is the sub-list for method output_type + 49, // [49:75] is the sub-list for method input_type 49, // [49:49] is the sub-list for extension type_name 49, // [49:49] is the sub-list for extension extendee 0, // [0:49] is the sub-list for field type_name diff --git a/rpcpb/rpc.pb.gw.go b/rpcpb/rpc.pb.gw.go index 00a8e325..ccd65218 100644 --- a/rpcpb/rpc.pb.gw.go +++ b/rpcpb/rpc.pb.gw.go @@ -625,6 +625,33 @@ func local_request_ControlService_SaveSnapshot_0(ctx context.Context, marshaler return msg, metadata, err } +func request_ControlService_SaveHotSnapshot_0(ctx context.Context, marshaler runtime.Marshaler, client ControlServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SaveSnapshotRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.SaveHotSnapshot(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ControlService_SaveHotSnapshot_0(ctx context.Context, marshaler runtime.Marshaler, server ControlServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SaveSnapshotRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.SaveHotSnapshot(ctx, &protoReq) + return msg, metadata, err +} + func request_ControlService_LoadSnapshot_0(ctx context.Context, marshaler runtime.Marshaler, client ControlServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq LoadSnapshotRequest @@ -1149,6 +1176,26 @@ func RegisterControlServiceHandlerServer(ctx context.Context, mux *runtime.Serve } forward_ControlService_SaveSnapshot_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_ControlService_SaveHotSnapshot_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/rpcpb.ControlService/SaveHotSnapshot", runtime.WithHTTPPathPattern("/v1/control/savehotsnapshot")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ControlService_SaveHotSnapshot_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ControlService_SaveHotSnapshot_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodPost, pattern_ControlService_LoadSnapshot_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -1670,6 +1717,23 @@ func RegisterControlServiceHandlerClient(ctx context.Context, mux *runtime.Serve } forward_ControlService_SaveSnapshot_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_ControlService_SaveHotSnapshot_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/rpcpb.ControlService/SaveHotSnapshot", runtime.WithHTTPPathPattern("/v1/control/savehotsnapshot")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ControlService_SaveHotSnapshot_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ControlService_SaveHotSnapshot_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodPost, pattern_ControlService_LoadSnapshot_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -1746,6 +1810,7 @@ var ( pattern_ControlService_AttachPeer_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "control", "attachpeer"}, "")) pattern_ControlService_SendOutboundMessage_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "control", "sendoutboundmessage"}, "")) pattern_ControlService_SaveSnapshot_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "control", "savesnapshot"}, "")) + pattern_ControlService_SaveHotSnapshot_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "control", "savehotsnapshot"}, "")) pattern_ControlService_LoadSnapshot_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "control", "loadsnapshot"}, "")) pattern_ControlService_RemoveSnapshot_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "control", "removesnapshot"}, "")) pattern_ControlService_GetSnapshotNames_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "control", "getsnapshotnames"}, "")) @@ -1773,6 +1838,7 @@ var ( forward_ControlService_AttachPeer_0 = runtime.ForwardResponseMessage forward_ControlService_SendOutboundMessage_0 = runtime.ForwardResponseMessage forward_ControlService_SaveSnapshot_0 = runtime.ForwardResponseMessage + forward_ControlService_SaveHotSnapshot_0 = runtime.ForwardResponseMessage forward_ControlService_LoadSnapshot_0 = runtime.ForwardResponseMessage forward_ControlService_RemoveSnapshot_0 = runtime.ForwardResponseMessage forward_ControlService_GetSnapshotNames_0 = runtime.ForwardResponseMessage diff --git a/rpcpb/rpc.proto b/rpcpb/rpc.proto index 4d80024a..068b9acc 100644 --- a/rpcpb/rpc.proto +++ b/rpcpb/rpc.proto @@ -169,6 +169,15 @@ service ControlService { }; } + // SaveHotSnapshot saves a snapshot without stopping the network + // Uses Copy-on-Write on APFS (macOS) for instant snapshots + rpc SaveHotSnapshot(SaveSnapshotRequest) returns (SaveSnapshotResponse) { + option (google.api.http) = { + post: "/v1/control/savehotsnapshot" + body: "*" + }; + } + rpc LoadSnapshot(LoadSnapshotRequest) returns (LoadSnapshotResponse) { option (google.api.http) = { post: "/v1/control/loadsnapshot" diff --git a/rpcpb/rpc_grpc.pb.go b/rpcpb/rpc_grpc.pb.go index 3e46a3d9..912ae9fb 100644 --- a/rpcpb/rpc_grpc.pb.go +++ b/rpcpb/rpc_grpc.pb.go @@ -142,6 +142,7 @@ const ( ControlService_AttachPeer_FullMethodName = "/rpcpb.ControlService/AttachPeer" ControlService_SendOutboundMessage_FullMethodName = "/rpcpb.ControlService/SendOutboundMessage" ControlService_SaveSnapshot_FullMethodName = "/rpcpb.ControlService/SaveSnapshot" + ControlService_SaveHotSnapshot_FullMethodName = "/rpcpb.ControlService/SaveHotSnapshot" ControlService_LoadSnapshot_FullMethodName = "/rpcpb.ControlService/LoadSnapshot" ControlService_RemoveSnapshot_FullMethodName = "/rpcpb.ControlService/RemoveSnapshot" ControlService_GetSnapshotNames_FullMethodName = "/rpcpb.ControlService/GetSnapshotNames" @@ -172,6 +173,9 @@ type ControlServiceClient interface { AttachPeer(ctx context.Context, in *AttachPeerRequest, opts ...grpc.CallOption) (*AttachPeerResponse, error) SendOutboundMessage(ctx context.Context, in *SendOutboundMessageRequest, opts ...grpc.CallOption) (*SendOutboundMessageResponse, error) SaveSnapshot(ctx context.Context, in *SaveSnapshotRequest, opts ...grpc.CallOption) (*SaveSnapshotResponse, error) + // SaveHotSnapshot saves a snapshot without stopping the network + // Uses Copy-on-Write on APFS (macOS) for instant snapshots + SaveHotSnapshot(ctx context.Context, in *SaveSnapshotRequest, opts ...grpc.CallOption) (*SaveSnapshotResponse, error) LoadSnapshot(ctx context.Context, in *LoadSnapshotRequest, opts ...grpc.CallOption) (*LoadSnapshotResponse, error) RemoveSnapshot(ctx context.Context, in *RemoveSnapshotRequest, opts ...grpc.CallOption) (*RemoveSnapshotResponse, error) GetSnapshotNames(ctx context.Context, in *GetSnapshotNamesRequest, opts ...grpc.CallOption) (*GetSnapshotNamesResponse, error) @@ -404,6 +408,16 @@ func (c *controlServiceClient) SaveSnapshot(ctx context.Context, in *SaveSnapsho return out, nil } +func (c *controlServiceClient) SaveHotSnapshot(ctx context.Context, in *SaveSnapshotRequest, opts ...grpc.CallOption) (*SaveSnapshotResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SaveSnapshotResponse) + err := c.cc.Invoke(ctx, ControlService_SaveHotSnapshot_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *controlServiceClient) LoadSnapshot(ctx context.Context, in *LoadSnapshotRequest, opts ...grpc.CallOption) (*LoadSnapshotResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(LoadSnapshotResponse) @@ -459,6 +473,9 @@ type ControlServiceServer interface { AttachPeer(context.Context, *AttachPeerRequest) (*AttachPeerResponse, error) SendOutboundMessage(context.Context, *SendOutboundMessageRequest) (*SendOutboundMessageResponse, error) SaveSnapshot(context.Context, *SaveSnapshotRequest) (*SaveSnapshotResponse, error) + // SaveHotSnapshot saves a snapshot without stopping the network + // Uses Copy-on-Write on APFS (macOS) for instant snapshots + SaveHotSnapshot(context.Context, *SaveSnapshotRequest) (*SaveSnapshotResponse, error) LoadSnapshot(context.Context, *LoadSnapshotRequest) (*LoadSnapshotResponse, error) RemoveSnapshot(context.Context, *RemoveSnapshotRequest) (*RemoveSnapshotResponse, error) GetSnapshotNames(context.Context, *GetSnapshotNamesRequest) (*GetSnapshotNamesResponse, error) @@ -535,6 +552,9 @@ func (UnimplementedControlServiceServer) SendOutboundMessage(context.Context, *S func (UnimplementedControlServiceServer) SaveSnapshot(context.Context, *SaveSnapshotRequest) (*SaveSnapshotResponse, error) { return nil, status.Error(codes.Unimplemented, "method SaveSnapshot not implemented") } +func (UnimplementedControlServiceServer) SaveHotSnapshot(context.Context, *SaveSnapshotRequest) (*SaveSnapshotResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SaveHotSnapshot not implemented") +} func (UnimplementedControlServiceServer) LoadSnapshot(context.Context, *LoadSnapshotRequest) (*LoadSnapshotResponse, error) { return nil, status.Error(codes.Unimplemented, "method LoadSnapshot not implemented") } @@ -936,6 +956,24 @@ func _ControlService_SaveSnapshot_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _ControlService_SaveHotSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SaveSnapshotRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ControlServiceServer).SaveHotSnapshot(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ControlService_SaveHotSnapshot_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ControlServiceServer).SaveHotSnapshot(ctx, req.(*SaveSnapshotRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ControlService_LoadSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(LoadSnapshotRequest) if err := dec(in); err != nil { @@ -1077,6 +1115,10 @@ var ControlService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SaveSnapshot", Handler: _ControlService_SaveSnapshot_Handler, }, + { + MethodName: "SaveHotSnapshot", + Handler: _ControlService_SaveHotSnapshot_Handler, + }, { MethodName: "LoadSnapshot", Handler: _ControlService_LoadSnapshot_Handler, diff --git a/server/network.go b/server/network.go index fb2714cd..9de885be 100644 --- a/server/network.go +++ b/server/network.go @@ -137,6 +137,7 @@ func newLocalNetwork(opts localNetworkOptions) (*localNetwork, error) { // TODO document. // Assumes [lc.lock] is held. func (lc *localNetwork) createConfig() error { + lc.log.Info("createConfig() ENTERED", "globalNodeConfig", lc.options.globalNodeConfig) var globalConfig map[string]interface{} if lc.options.globalNodeConfig != "" { @@ -164,36 +165,82 @@ func (lc *localNetwork) createConfig() error { } } + lc.log.Info("createConfig networkID parsed", "networkID", networkID, "MainnetID", luxd_constants.MainnetID, "TestnetID", luxd_constants.TestnetID) + + // 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. + existingGenesis, isResume := lc.checkForExistingGenesis() + 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) + } else { + fmt.Fprintf(os.Stderr, "DEBUG: Not resuming - checkForExistingGenesis returned false (rootDataDir=%s)\n", lc.options.rootDataDir) + } + // Use the appropriate genesis configuration based on network ID - // If LUX_MNEMONIC is set, derive validators from it (preferred for mainnet/testnet) + // 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 != "" - fmt.Printf("šŸ” DEBUG: LUX_MNEMONIC set: %v (len=%d)\n", useMnemonic, len(mnemonic)) switch networkID { case luxd_constants.MainnetID: // LUX Mainnet (1) if useMnemonic { - fmt.Printf("šŸ”‘ Using NewMainnetConfigFromMnemonic (mnemonic-derived keys)\n") + 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 { - fmt.Printf("šŸ“¦ Using NewMainnetConfig (embedded keys)\n") - cfg, err = local.NewMainnetConfig(lc.options.execPath, lc.options.numNodes) + // 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 luxd_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 { - // Use pre-existing keys from ~/.lux/keys if available - fmt.Printf("šŸ“¦ Using NewTestnetConfigWithKeys (pre-existing keys from ~/.lux/keys)\n") - cfg, err = local.NewTestnetConfigWithKeys(lc.options.execPath, "") + // 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 luxd_constants.DevnetID: // LUX Devnet (3) + // ALWAYS use canonical genesis for devnet - never regenerate + 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 luxd_constants.CustomID: // Custom/Local (1337) + // ALWAYS use canonical genesis for custom network - never regenerate + 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 == luxd_constants.MainnetID || networkID == luxd_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") + } 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 + } + } + } + // Ensure required staking flags are set for non-local networks // stake-minting-period must be >= max-stake-duration (1 year = 8760h) if _, ok := cfg.Flags["stake-minting-period"]; !ok { @@ -314,6 +361,44 @@ func (lc *localNetwork) Start(ctx context.Context) error { return nil } +// checkForExistingGenesis checks if we're resuming from existing state. +// Returns (genesisJSON, isResume) where isResume is true if: +// 1. node1/genesis.json exists, AND +// 2. node1/db directory has data (meaning blocks have been committed) +// +// This is critical because JSON serialization is non-deterministic in Go +// (map iteration order varies), so regenerating genesis from the same source +// data produces different bytes, causing "db contains invalid genesis hash" errors. +func (lc *localNetwork) checkForExistingGenesis() (string, bool) { + if lc.options.rootDataDir == "" { + return "", false + } + + // Check node1 for existing genesis and db data + node1Dir := filepath.Join(lc.options.rootDataDir, "node1") + genesisPath := filepath.Join(node1Dir, "genesis.json") + dbDir := filepath.Join(node1Dir, "db") + + // Check if genesis.json exists + genesisBytes, err := os.ReadFile(genesisPath) + if err != nil { + // No existing genesis file - this is a fresh start + return "", false + } + + // Check if db directory has data (indicating we're resuming) + entries, err := os.ReadDir(dbDir) + if err != nil || len(entries) == 0 { + // No db data - this is a fresh start even though genesis file exists + // (perhaps from a failed previous start) + return "", false + } + + // Both genesis.json exists AND db has data - we're resuming + // Note: We can't log here since lc.log may not be set yet during config creation + return string(genesisBytes), true +} + // Creates the blockchains specified in [chainSpecs]. // Assumes [lc.lock] isn't held. func (lc *localNetwork) CreateChains( diff --git a/server/server.go b/server/server.go index e667b321..03dd6d85 100644 --- a/server/server.go +++ b/server/server.go @@ -1493,6 +1493,32 @@ func (s *server) SaveSnapshot(ctx context.Context, req *rpcpb.SaveSnapshotReques return &rpcpb.SaveSnapshotResponse{SnapshotPath: snapshotPath}, nil } +// SaveHotSnapshot saves a snapshot without stopping the network +// Uses Copy-on-Write on APFS (macOS) for instant snapshots +func (s *server) SaveHotSnapshot(ctx context.Context, req *rpcpb.SaveSnapshotRequest) (*rpcpb.SaveSnapshotResponse, error) { + s.mu.RLock() // Read lock - doesn't block network operations + defer s.mu.RUnlock() + + s.logger.Info("SaveHotSnapshot", log.String("snapshot-name", req.SnapshotName)) + + if s.network == nil { + return nil, ErrNotBootstrapped + } + + snapshotPath, err := s.network.nw.SaveHotSnapshot(ctx, req.SnapshotName) + if err != nil { + s.logger.Warn("hot snapshot save failed to complete", log.Err(err)) + return nil, err + } + + // Note: We do NOT stop the network for hot snapshots + s.logger.Info("Hot snapshot saved successfully", + log.String("snapshot-name", req.SnapshotName), + log.String("path", snapshotPath)) + + return &rpcpb.SaveSnapshotResponse{SnapshotPath: snapshotPath}, nil +} + func (s *server) RemoveSnapshot(_ context.Context, req *rpcpb.RemoveSnapshotRequest) (*rpcpb.RemoveSnapshotResponse, error) { s.mu.Lock() defer s.mu.Unlock()