diff --git a/tests/antithesis/compose.go b/tests/antithesis/compose.go index 73433f305f7f..a01fee882f44 100644 --- a/tests/antithesis/compose.go +++ b/tests/antithesis/compose.go @@ -161,7 +161,7 @@ func newComposeProject(network *tmpnet.Network, nodeImageName string, workloadIm } // Apply configuration appropriate to a test network - for k, v := range tmpnet.DefaultTestFlags() { + for k, v := range tmpnet.DefaultTmpnetFlags() { env[k] = v } diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 65bf6d6c632c..0da72c202757 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -61,18 +61,22 @@ var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { require.NoError(tc, err) upgradeBase64 := base64.StdEncoding.EncodeToString(upgradeJSON) + + defaultFlags := tmpnet.FlagsMap{ + config.UpgradeFileContentKey: upgradeBase64, + // Ensure a min stake duration compatible with testing staking logic + config.MinStakeDurationKey: "1s", + } + defaultFlags.SetDefaults(tmpnet.DefaultE2EFlags()) + return e2e.NewTestEnvironment( tc, flagVars, &tmpnet.Network{ - Owner: flagVars.NetworkOwner(), - DefaultFlags: tmpnet.FlagsMap{ - config.UpgradeFileContentKey: upgradeBase64, - // Ensure a min stake duration compatible with testing staking logic - config.MinStakeDurationKey: tmpnet.DefaultMinStakeDuration, - }, - Nodes: nodes, - Subnets: subnets, + Owner: flagVars.NetworkOwner(), + DefaultFlags: defaultFlags, + Nodes: nodes, + Subnets: subnets, }, ).Marshal() }, func(envBytes []byte) { diff --git a/tests/fixture/tmpnet/defaults.go b/tests/fixture/tmpnet/defaults.go index bfaf53a872fc..8751ac438c61 100644 --- a/tests/fixture/tmpnet/defaults.go +++ b/tests/fixture/tmpnet/defaults.go @@ -35,8 +35,8 @@ const ( defaultConfigFilename = "config.json" ) -// Flags appropriate for networks used for all types of testing. -func DefaultTestFlags() FlagsMap { +// Flags suggested for temporary networks. Applied by default. +func DefaultTmpnetFlags() FlagsMap { return FlagsMap{ config.NetworkPeerListPullGossipFreqKey: "250ms", config.NetworkMaxReconnectDelayKey: "1s", @@ -46,26 +46,14 @@ func DefaultTestFlags() FlagsMap { } } -// Flags appropriate for tmpnet networks. -func DefaultTmpnetFlags() FlagsMap { - // Supply only non-default configuration to ensure that default values will be used. - flags := FlagsMap{ - // Default to dynamic port allocation - config.HTTPPortKey: "0", - config.StakingPortKey: "0", - // Specific to tmpnet deployment - config.PublicIPKey: "127.0.0.1", - config.HTTPHostKey: "127.0.0.1", - config.StakingHostKey: "127.0.0.1", - config.LogDisplayLevelKey: logging.Off.String(), // Display logging not needed since nodes run headless - config.LogLevelKey: logging.Debug.String(), - // Specific to e2e testing +// Flags suggested for e2e testing +func DefaultE2EFlags() FlagsMap { + return FlagsMap{ config.ProposerVMUseCurrentHeightKey: "true", // Reducing this from the 1s default speeds up tx acceptance config.ProposerVMMinBlockDelayKey: "0s", + config.LogLevelKey: logging.Debug.String(), } - flags.SetDefaults(DefaultTestFlags()) - return flags } // A set of chain configurations appropriate for testing. diff --git a/tests/fixture/tmpnet/network.go b/tests/fixture/tmpnet/network.go index 4bf234b63824..2cfaad9eafde 100644 --- a/tests/fixture/tmpnet/network.go +++ b/tests/fixture/tmpnet/network.go @@ -11,7 +11,6 @@ import ( "encoding/json" "errors" "fmt" - "maps" "net/netip" "os" "os/exec" @@ -65,7 +64,8 @@ var ( // TODO(marun) Remove when subnet-evm configures the genesis with this key. HardhatKey *secp256k1.PrivateKey - errInsufficientNodes = errors.New("at least one node is required") + errInsufficientNodes = errors.New("at least one node is required") + errMissingRuntimeConfig = errors.New("DefaultRuntimeConfig must not be empty") ) func init() { @@ -253,6 +253,11 @@ func (n *Network) EnsureDefaultConfig(log logging.Logger) error { } } + emptyRuntime := NodeRuntimeConfig{} + if n.DefaultRuntimeConfig == emptyRuntime { + return errMissingRuntimeConfig + } + return nil } @@ -471,10 +476,6 @@ func (n *Network) StartNode(ctx context.Context, node *Node) error { return err } - if err := n.writeNodeFlags(node); err != nil { - return fmt.Errorf("writing node flags: %w", err) - } - if err := node.Start(ctx); err != nil { // Attempt to stop an unhealthy node to provide some assurance to the caller // that an error condition will not result in a lingering process. @@ -762,10 +763,11 @@ func (n *Network) GetNodeURIs() []NodeURI { return GetNodeURIs(n.Nodes) } -// Retrieves bootstrap IPs and IDs for all nodes except the skipped one (this supports -// collecting the bootstrap details for restarting a node). +// Retrieves bootstrap IPs and IDs for all non-ephemeral nodes except the skipped one +// (this supports collecting the bootstrap details for restarting a node). +// // For consumption outside of avalanchego. Needs to be kept exported. -func (n *Network) GetBootstrapIPsAndIDs(skippedNode *Node) ([]string, []string, error) { +func (n *Network) GetBootstrapIPsAndIDs(skippedNode *Node) ([]string, []string) { bootstrapIPs := []string{} bootstrapIDs := []string{} for _, node := range n.Nodes { @@ -786,7 +788,7 @@ func (n *Network) GetBootstrapIPsAndIDs(skippedNode *Node) ([]string, []string, bootstrapIDs = append(bootstrapIDs, node.NodeID.String()) } - return bootstrapIPs, bootstrapIDs, nil + return bootstrapIPs, bootstrapIDs } // GetNetworkID returns the effective ID of the network. If the network @@ -879,77 +881,6 @@ func (n *Network) GetChainConfigContent() (string, error) { return base64.StdEncoding.EncodeToString(marshaledConfigs), nil } -// writeNodeFlags determines the set of flags that should be used to -// start the given node and writes them to a file in the node path. -func (n *Network) writeNodeFlags(node *Node) error { - flags := maps.Clone(node.Flags) - - // Convert the network id to a string to ensure consistency in JSON round-tripping. - flags.SetDefault(config.NetworkNameKey, strconv.FormatUint(uint64(n.GetNetworkID()), 10)) - - // Set the bootstrap configuration - bootstrapIPs, bootstrapIDs, err := n.GetBootstrapIPsAndIDs(node) - if err != nil { - return fmt.Errorf("failed to determine bootstrap configuration: %w", err) - } - flags.SetDefault(config.BootstrapIDsKey, strings.Join(bootstrapIDs, ",")) - flags.SetDefault(config.BootstrapIPsKey, strings.Join(bootstrapIPs, ",")) - - // TODO(marun) Maybe avoid computing content flags for each node start? - - if n.Genesis != nil { - genesisFileContent, err := n.GetGenesisFileContent() - if err != nil { - return fmt.Errorf("failed to get genesis file content: %w", err) - } - flags.SetDefault(config.GenesisFileContentKey, genesisFileContent) - - isSingleNodeNetwork := (len(n.Nodes) == 1 && len(n.Genesis.InitialStakers) == 1) - if isSingleNodeNetwork { - n.log.Info("defaulting to sybil protection disabled to enable a single-node network to start") - flags.SetDefault(config.SybilProtectionEnabledKey, "false") - } - } - - subnetConfigContent, err := n.GetSubnetConfigContent() - if err != nil { - return fmt.Errorf("failed to get subnet config content: %w", err) - } - if len(subnetConfigContent) > 0 { - flags.SetDefault(config.SubnetConfigContentKey, subnetConfigContent) - } - - chainConfigContent, err := n.GetChainConfigContent() - if err != nil { - return fmt.Errorf("failed to get chain config content: %w", err) - } - if len(chainConfigContent) > 0 { - flags.SetDefault(config.ChainConfigContentKey, chainConfigContent) - } - - // Only configure the plugin dir with a non-empty value to ensure the use of - // the default value (`[datadir]/plugins`) when no plugin dir is configured. - processConfig := node.getRuntimeConfig().Process - if processConfig != nil { - if len(processConfig.PluginDir) > 0 { - // Ensure the plugin directory exists or the node will fail to start - if err := os.MkdirAll(processConfig.PluginDir, perms.ReadWriteExecute); err != nil { - return fmt.Errorf("failed to create plugin dir: %w", err) - } - flags.SetDefault(config.PluginDirKey, processConfig.PluginDir) - } - - flags.SetDefault(config.DataDirKey, node.DataDir) - } - - // Set the network and tmpnet defaults last to ensure they can be overridden - flags.SetDefaults(n.DefaultFlags) - flags.SetDefaults(DefaultTmpnetFlags()) - - // Write the flags to disk - return node.writeFlags(flags) -} - // Waits until the provided nodes are healthy. func waitForHealthy(ctx context.Context, log logging.Logger, nodes []*Node) error { ticker := time.NewTicker(networkHealthCheckInterval) diff --git a/tests/fixture/tmpnet/network_test.go b/tests/fixture/tmpnet/network_test.go index 5390ff04cc84..fa789d7e9935 100644 --- a/tests/fixture/tmpnet/network_test.go +++ b/tests/fixture/tmpnet/network_test.go @@ -20,6 +20,8 @@ func TestNetworkSerialization(t *testing.T) { ctx := context.Background() network := NewDefaultNetwork("testnet") + // Runtime configuration is required + network.DefaultRuntimeConfig.Process = &ProcessRuntimeConfig{} // Validate round-tripping of primary subnet configuration network.PrimarySubnetConfig = ConfigMap{ "validatorOnly": true, diff --git a/tests/fixture/tmpnet/node.go b/tests/fixture/tmpnet/node.go index f48affdf56cc..629fc28e113c 100644 --- a/tests/fixture/tmpnet/node.go +++ b/tests/fixture/tmpnet/node.go @@ -9,9 +9,11 @@ import ( "errors" "fmt" "io" + "maps" "net" "net/http" "net/netip" + "strconv" "strings" "time" @@ -53,12 +55,6 @@ type NodeRuntimeConfig struct { Process *ProcessRuntimeConfig `json:"process,omitempty"` } -type ProcessRuntimeConfig struct { - AvalancheGoPath string `json:"avalancheGoPath,omitempty"` - PluginDir string `json:"pluginDir,omitempty"` - ReuseDynamicPorts bool `json:"reuseDynamicPorts,omitempty"` -} - // Node supports configuring and running a node participating in a temporary network. type Node struct { // Set by EnsureNodeID which is also called when the node is read. @@ -323,6 +319,59 @@ func (n *Node) GetUniqueID() string { return n.network.UUID + "-" + strings.ToLower(nodeIDString[startIndex:endIndex]) } +// composeFlags determines the set of flags that should be used to +// start the node. +func (n *Node) composeFlags() (FlagsMap, error) { + flags := maps.Clone(n.Flags) + + // Apply the network defaults first so that they are not overridden + flags.SetDefaults(n.network.DefaultFlags) + + flags.SetDefaults(DefaultTmpnetFlags()) + + // Convert the network id to a string to ensure consistency in JSON round-tripping. + flags.SetDefault(config.NetworkNameKey, strconv.FormatUint(uint64(n.network.GetNetworkID()), 10)) + + // Set the bootstrap configuration + bootstrapIPs, bootstrapIDs := n.network.GetBootstrapIPsAndIDs(n) + flags.SetDefault(config.BootstrapIDsKey, strings.Join(bootstrapIDs, ",")) + flags.SetDefault(config.BootstrapIPsKey, strings.Join(bootstrapIPs, ",")) + + // TODO(marun) Maybe avoid computing content flags for each node start? + + if n.network.Genesis != nil { + genesisFileContent, err := n.network.GetGenesisFileContent() + if err != nil { + return nil, fmt.Errorf("failed to get genesis file content: %w", err) + } + flags.SetDefault(config.GenesisFileContentKey, genesisFileContent) + + isSingleNodeNetwork := len(n.network.Nodes) == 1 && len(n.network.Genesis.InitialStakers) == 1 + if isSingleNodeNetwork { + n.network.log.Info("defaulting to sybil protection disabled to enable a single-node network to start") + flags.SetDefault(config.SybilProtectionEnabledKey, "false") + } + } + + subnetConfigContent, err := n.network.GetSubnetConfigContent() + if err != nil { + return nil, fmt.Errorf("failed to get subnet config content: %w", err) + } + if len(subnetConfigContent) > 0 { + flags.SetDefault(config.SubnetConfigContentKey, subnetConfigContent) + } + + chainConfigContent, err := n.network.GetChainConfigContent() + if err != nil { + return nil, fmt.Errorf("failed to get chain config content: %w", err) + } + if len(chainConfigContent) > 0 { + flags.SetDefault(config.ChainConfigContentKey, chainConfigContent) + } + + return flags, nil +} + // Saves the currently allocated API port to the node's configuration // for use across restarts. func (n *Node) SaveAPIPort() error { diff --git a/tests/fixture/tmpnet/node_config.go b/tests/fixture/tmpnet/node_config.go index ba48dea1cd3e..66643338b8cc 100644 --- a/tests/fixture/tmpnet/node_config.go +++ b/tests/fixture/tmpnet/node_config.go @@ -23,17 +23,6 @@ func (n *Node) GetFlagsPath() string { return filepath.Join(n.DataDir, "flags.json") } -func (n *Node) writeFlags(flags FlagsMap) error { - bytes, err := DefaultJSONMarshal(flags) - if err != nil { - return fmt.Errorf("failed to marshal node flags: %w", err) - } - if err := os.WriteFile(n.GetFlagsPath(), bytes, perms.ReadWrite); err != nil { - return fmt.Errorf("failed to write node flags: %w", err) - } - return nil -} - func (n *Node) getConfigPath() string { return filepath.Join(n.DataDir, defaultConfigFilename) } diff --git a/tests/fixture/tmpnet/process_runtime.go b/tests/fixture/tmpnet/process_runtime.go index 4341223fcb8e..9bc103fbe80d 100644 --- a/tests/fixture/tmpnet/process_runtime.go +++ b/tests/fixture/tmpnet/process_runtime.go @@ -38,11 +38,16 @@ const ( var ( AvalancheGoPluginDirEnvName = config.EnvVarName(config.EnvPrefix, config.PluginDirKey) - errNodeAlreadyRunning = errors.New("failed to start node: node is already running") - errNotRunning = errors.New("node is not running") - errMissingRuntimeConfig = errors.New("node is missing runtime configuration") + errNodeAlreadyRunning = errors.New("failed to start node: node is already running") + errNotRunning = errors.New("node is not running") ) +type ProcessRuntimeConfig struct { + AvalancheGoPath string `json:"avalancheGoPath,omitempty"` + PluginDir string `json:"pluginDir,omitempty"` + ReuseDynamicPorts bool `json:"reuseDynamicPorts,omitempty"` +} + // Defines local-specific node configuration. Supports setting default // and node-specific values. type ProcessRuntime struct { @@ -52,6 +57,10 @@ type ProcessRuntime struct { pid int } +func (p *ProcessRuntime) getRuntimeConfig() *ProcessRuntimeConfig { + return p.node.getRuntimeConfig().Process +} + func (p *ProcessRuntime) setProcessContext(processContext node.ProcessContext) { p.pid = processContext.PID p.node.URI = processContext.URI @@ -93,10 +102,7 @@ func (p *ProcessRuntime) Start(ctx context.Context) error { return errNodeAlreadyRunning } - runtimeConfig := p.node.getRuntimeConfig().Process - if runtimeConfig == nil { - return errMissingRuntimeConfig - } + runtimeConfig := p.getRuntimeConfig() // Attempt to check for rpc version compatibility if err := checkVMBinaries(log, p.node.network.Subnets, runtimeConfig); err != nil { @@ -109,6 +115,10 @@ func (p *ProcessRuntime) Start(ctx context.Context) error { return fmt.Errorf("failed to remove stale process context file: %w", err) } + if err := p.writeFlags(); err != nil { + return fmt.Errorf("writing node flags: %w", err) + } + // All arguments are provided in the flags file cmd := exec.Command(runtimeConfig.AvalancheGoPath, "--config-file", p.node.GetFlagsPath()) // #nosec G204 // Ensure process is detached from the parent process so that an error in the parent will not affect the child @@ -144,6 +154,51 @@ func (p *ProcessRuntime) Start(ctx context.Context) error { return p.writeMonitoringConfig() } +func (p *ProcessRuntime) writeFlags() error { + flags, err := p.node.composeFlags() + if err != nil { + return fmt.Errorf("failed to compose node flags: %w", err) + } + + flags.SetDefaults(FlagsMap{ + config.DataDirKey: p.node.DataDir, + // Use dynamic port allocation + config.HTTPPortKey: "0", + config.StakingPortKey: "0", + // Binding to localhost on macos avoids having a permission + // dialog pop up for every node that tries to bind to + // non-localhost interfaces + config.PublicIPKey: "127.0.0.1", + config.HTTPHostKey: "127.0.0.1", + config.StakingHostKey: "127.0.0.1", + }) + + runtimeConfig := p.getRuntimeConfig() + + // Only configure the plugin dir with a non-empty value to ensure the use of the + // default value (`[datadir]/plugins`) when no plugin dir is configured. + if len(runtimeConfig.PluginDir) > 0 { + flags.SetDefault(config.PluginDirKey, runtimeConfig.PluginDir) + } + + // Ensure a non-empty plugin directory exists or the node will fail to start. + pluginDir := flags[config.PluginDirKey] + if len(pluginDir) > 0 { + if err := os.MkdirAll(pluginDir, perms.ReadWriteExecute); err != nil { + return fmt.Errorf("failed to create plugin dir: %w", err) + } + } + + bytes, err := DefaultJSONMarshal(flags) + if err != nil { + return fmt.Errorf("failed to marshal node flags: %w", err) + } + if err := os.WriteFile(p.node.GetFlagsPath(), bytes, perms.ReadWrite); err != nil { + return fmt.Errorf("failed to write node flags: %w", err) + } + return nil +} + // Signals the node process to stop. func (p *ProcessRuntime) InitiateStop(_ context.Context) error { proc, err := p.getProcess()