From c723a52ac0d4350e46d2a38806f749acc39f0a2a Mon Sep 17 00:00:00 2001 From: chatton Date: Wed, 18 Mar 2026 08:43:08 +0000 Subject: [PATCH 1/8] chore: updating home dirs to not use root --- framework/docker/evstack/evmsingle/config.go | 3 +-- framework/docker/evstack/evmsingle/node.go | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/framework/docker/evstack/evmsingle/config.go b/framework/docker/evstack/evmsingle/config.go index 7e8887c..ce5f161 100644 --- a/framework/docker/evstack/evmsingle/config.go +++ b/framework/docker/evstack/evmsingle/config.go @@ -25,8 +25,7 @@ type Config struct { // DefaultImage returns the default container image for ev-node-evm. func DefaultImage() container.Image { - // Default ev-node tag pinned for reproducibility - return container.Image{Repository: "ghcr.io/evstack/ev-node-evm", Version: "v1.0.0-rc.4"} + return container.Image{Repository: "ghcr.io/evstack/ev-node-evm", Version: "v1.0.0-rc.4", UIDGID: "10001:10001"} } // DefaultBinary returns the default binary name for ev-node-evm. diff --git a/framework/docker/evstack/evmsingle/node.go b/framework/docker/evstack/evmsingle/node.go index 5483e22..0579071 100644 --- a/framework/docker/evstack/evmsingle/node.go +++ b/framework/docker/evstack/evmsingle/node.go @@ -42,8 +42,7 @@ func newNode(ctx context.Context, cfg Config, testName string, index int, nodeCf log := cfg.Logger.With(zap.String("component", "evm-single"), zap.Int("i", index)) - // ev-node-evm default home - homeDir := "/root/.evm" + homeDir := "/home/ev-node/.evm" n := &Node{cfg: cfg, nodeCfg: nodeCfg, logger: log, internal: ports, chainName: chainName} n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, homeDir, index, NodeType, log) From 409c3ac79e254edd80d0a0825049403cb52158b4 Mon Sep 17 00:00:00 2001 From: chatton Date: Wed, 18 Mar 2026 11:01:03 +0000 Subject: [PATCH 2/8] chore: make homedir configurable for all components --- framework/docker/cosmos/chain_builder.go | 18 ++++++++++++++---- framework/docker/dataavailability/config.go | 5 +++++ .../docker/dataavailability/network_builder.go | 10 +++++++++- framework/docker/dataavailability/node.go | 7 +++++-- framework/docker/evstack/chain_builder.go | 10 +++++++++- framework/docker/evstack/config.go | 5 +++++ framework/docker/evstack/evmsingle/builder.go | 12 ++++++++++++ framework/docker/evstack/evmsingle/config.go | 7 +++++++ framework/docker/evstack/evmsingle/node.go | 4 +--- framework/docker/evstack/node.go | 8 +++++--- framework/docker/evstack/reth/builder.go | 12 ++++++++++++ framework/docker/evstack/reth/config.go | 7 +++++++ framework/docker/evstack/reth/node.go | 3 +-- framework/docker/evstack/spamoor/builder.go | 8 +++++--- framework/docker/evstack/spamoor/node.go | 7 +++++-- 15 files changed, 102 insertions(+), 21 deletions(-) diff --git a/framework/docker/cosmos/chain_builder.go b/framework/docker/cosmos/chain_builder.go index 0b3e1ca..2aa23f9 100644 --- a/framework/docker/cosmos/chain_builder.go +++ b/framework/docker/cosmos/chain_builder.go @@ -158,6 +158,8 @@ type ChainBuilder struct { // blockWaitTimeout is the timeout for waiting for blocks after starting the chain. // If zero, defaults to 120 seconds. blockWaitTimeout time.Duration + // homeDir overrides the default home directory inside the container + homeDir string } // NewChainBuilder initializes and returns a new ChainBuilder with default values for testing purposes. @@ -315,6 +317,12 @@ func (b *ChainBuilder) WithImage(image container.Image) *ChainBuilder { return b } +// WithHomeDir overrides the default home directory inside the container. +func (b *ChainBuilder) WithHomeDir(homeDir string) *ChainBuilder { + b.homeDir = homeDir + return b +} + // WithAdditionalStartArgs sets the default additional start arguments for all nodes in the chain func (b *ChainBuilder) WithAdditionalStartArgs(args ...string) *ChainBuilder { b.additionalStartArgs = args @@ -494,10 +502,12 @@ func (b *ChainBuilder) newChainNode( } func (b *ChainBuilder) newDockerChainNode(log *zap.Logger, nodeConfig ChainNodeConfig, index int) *ChainNode { - // use a default home directory if name is not set - homeDir := "/var/cosmos-chain" - if b.name != "" { - homeDir = path.Join("/var/cosmos-chain", b.name) + homeDir := b.homeDir + if homeDir == "" { + homeDir = "/var/cosmos-chain" + if b.name != "" { + homeDir = path.Join("/var/cosmos-chain", b.name) + } } chainParams := ChainNodeParams{ diff --git a/framework/docker/dataavailability/config.go b/framework/docker/dataavailability/config.go index d464b3f..6f0e509 100644 --- a/framework/docker/dataavailability/config.go +++ b/framework/docker/dataavailability/config.go @@ -25,3 +25,8 @@ type Config struct { // AdditionalStartArgs are additional arguments passed to all nodes when starting AdditionalStartArgs []string } + +// DefaultHomeDir returns the default home directory for DA node containers. +func DefaultHomeDir() string { + return "/home/celestia" +} diff --git a/framework/docker/dataavailability/network_builder.go b/framework/docker/dataavailability/network_builder.go index 4695dc3..cd2c7aa 100644 --- a/framework/docker/dataavailability/network_builder.go +++ b/framework/docker/dataavailability/network_builder.go @@ -35,6 +35,8 @@ type NetworkBuilder struct { chainID string // binaryName is the name of the Node binary executable (e.g., "celestia") binaryName string + // homeDir overrides the default home directory inside the container + homeDir string } // NewNetworkBuilder initializes and returns a new NetworkBuilder with default values for testing purposes @@ -108,6 +110,12 @@ func (b *NetworkBuilder) WithDockerNetworkID(networkID string) *NetworkBuilder { return b } +// WithHomeDir overrides the default home directory inside the container. +func (b *NetworkBuilder) WithHomeDir(homeDir string) *NetworkBuilder { + b.homeDir = homeDir + return b +} + // WithImage sets the default Docker image for all nodes in the network func (b *NetworkBuilder) WithImage(image container.Image) *NetworkBuilder { b.dockerImage = &image @@ -228,7 +236,7 @@ func (b *NetworkBuilder) newNode(ctx context.Context, nodeConfig NodeConfig, ind nodeConfig.Env = b.env } - node := NewNode(cfg, b.testName, imageToUse, index, nodeConfig) + node := NewNode(cfg, b.testName, imageToUse, b.homeDir, index, nodeConfig) // Create and setup volume using shared logic if err := node.CreateAndSetupVolume(ctx, node.Name()); err != nil { diff --git a/framework/docker/dataavailability/node.go b/framework/docker/dataavailability/node.go index fd85643..017247c 100644 --- a/framework/docker/dataavailability/node.go +++ b/framework/docker/dataavailability/node.go @@ -76,7 +76,10 @@ type Node struct { externalPorts types.Ports } -func NewNode(cfg Config, testName string, image container.Image, index int, nodeConfig NodeConfig) *Node { +func NewNode(cfg Config, testName string, image container.Image, homeDir string, index int, nodeConfig NodeConfig) *Node { + if homeDir == "" { + homeDir = DefaultHomeDir() + } logger := cfg.Logger.With( zap.String("node_type", nodeConfig.NodeType.String()), ) @@ -86,7 +89,7 @@ func NewNode(cfg Config, testName string, image container.Image, index int, node additionalStartArgs: nodeConfig.AdditionalStartArgs, configModifications: nodeConfig.ConfigModifications, internalPorts: initializeDANodePorts(nodeConfig.InternalPorts), - Node: container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, "/home/celestia", index, nodeConfig.NodeType, logger), + Node: container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, homeDir, index, nodeConfig.NodeType, logger), } node.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, node.Name())) diff --git a/framework/docker/evstack/chain_builder.go b/framework/docker/evstack/chain_builder.go index b231367..fa54fde 100644 --- a/framework/docker/evstack/chain_builder.go +++ b/framework/docker/evstack/chain_builder.go @@ -37,6 +37,8 @@ type ChainBuilder struct { binaryName string // aggregatorPassphrase is the passphrase used for aggregator nodes aggregatorPassphrase string + // homeDir overrides the default home directory inside the container + homeDir string } // NewChainBuilder initializes and returns a new ChainBuilder with default values for testing purposes @@ -101,6 +103,12 @@ func (b *ChainBuilder) WithDockerNetworkID(networkID string) *ChainBuilder { return b } +// WithHomeDir overrides the default home directory inside the container. +func (b *ChainBuilder) WithHomeDir(homeDir string) *ChainBuilder { + b.homeDir = homeDir + return b +} + // WithImage sets the default Docker image for all nodes in the chain func (b *ChainBuilder) WithImage(image container.Image) *ChainBuilder { b.dockerImage = &image @@ -187,7 +195,7 @@ func (b *ChainBuilder) newNode(ctx context.Context, nodeConfig NodeConfig, index Image: imageToUse, } - node := NewNode(cfg, b.testName, imageToUse, index, nodeConfig.IsAggregator, b.getAdditionalStartArgs(nodeConfig)) + node := NewNode(cfg, b.testName, imageToUse, b.homeDir, index, nodeConfig.IsAggregator, b.getAdditionalStartArgs(nodeConfig)) // Create and setup volume using shared logic if err := node.CreateAndSetupVolume(ctx, node.Name()); err != nil { diff --git a/framework/docker/evstack/config.go b/framework/docker/evstack/config.go index 495285b..eb75527 100644 --- a/framework/docker/evstack/config.go +++ b/framework/docker/evstack/config.go @@ -25,3 +25,8 @@ type Config struct { // Image specifies the Docker image used for the evstack nodes. Image container.Image } + +// DefaultHomeDir returns the default home directory for evstack containers. +func DefaultHomeDir() string { + return "/var/evstack" +} diff --git a/framework/docker/evstack/evmsingle/builder.go b/framework/docker/evstack/evmsingle/builder.go index 8207b71..312b2bb 100644 --- a/framework/docker/evstack/evmsingle/builder.go +++ b/framework/docker/evstack/evmsingle/builder.go @@ -25,6 +25,7 @@ type ChainBuilder struct { addlArgs []string nodes []NodeConfig name string + homeDir string } func NewChainBuilder(t *testing.T) *ChainBuilder { @@ -100,6 +101,11 @@ func (b *ChainBuilder) WithNodes(cfgs ...NodeConfig) *ChainBuilder { return b } +func (b *ChainBuilder) WithHomeDir(homeDir string) *ChainBuilder { + b.homeDir = homeDir + return b +} + func (b *ChainBuilder) WithName(name string) *ChainBuilder { if err := internal.ValidateDockerHostnamePart(name); err != nil { panic(fmt.Sprintf("invalid evmsingle chain name: %v", err)) @@ -110,12 +116,18 @@ func (b *ChainBuilder) WithName(name string) *ChainBuilder { // Build constructs a Chain with nodes created and volumes initialized (not isInitialized) func (b *ChainBuilder) Build(ctx context.Context) (*Chain, error) { + homeDir := b.homeDir + if homeDir == "" { + homeDir = DefaultHomeDir() + } + cfg := Config{ Logger: b.logger, DockerClient: b.dockerClient, DockerNetworkID: b.networkID, Image: b.image, Bin: b.bin, + HomeDir: homeDir, Env: b.env, AdditionalStartArgs: b.addlArgs, } diff --git a/framework/docker/evstack/evmsingle/config.go b/framework/docker/evstack/evmsingle/config.go index ce5f161..74f7bef 100644 --- a/framework/docker/evstack/evmsingle/config.go +++ b/framework/docker/evstack/evmsingle/config.go @@ -15,6 +15,8 @@ type Config struct { Image container.Image // Bin is the executable name (default: evm-single) Bin string + // HomeDir is the home directory inside the container. Defaults to DefaultHomeDir(). + HomeDir string // Env are default environment variables applied to all nodes Env []string // AdditionalStartArgs are default start arguments applied to all nodes @@ -23,6 +25,11 @@ type Config struct { AdditionalInitArgs []string } +// DefaultHomeDir returns the default home directory for ev-node-evm containers. +func DefaultHomeDir() string { + return "/home/ev-node/.evm" +} + // DefaultImage returns the default container image for ev-node-evm. func DefaultImage() container.Image { return container.Image{Repository: "ghcr.io/evstack/ev-node-evm", Version: "v1.0.0-rc.4", UIDGID: "10001:10001"} diff --git a/framework/docker/evstack/evmsingle/node.go b/framework/docker/evstack/evmsingle/node.go index 0579071..2e40895 100644 --- a/framework/docker/evstack/evmsingle/node.go +++ b/framework/docker/evstack/evmsingle/node.go @@ -42,10 +42,8 @@ func newNode(ctx context.Context, cfg Config, testName string, index int, nodeCf log := cfg.Logger.With(zap.String("component", "evm-single"), zap.Int("i", index)) - homeDir := "/home/ev-node/.evm" - n := &Node{cfg: cfg, nodeCfg: nodeCfg, logger: log, internal: ports, chainName: chainName} - n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, homeDir, index, NodeType, log) + n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, cfg.HomeDir, index, NodeType, log) n.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, n.Name())) if err := n.CreateAndSetupVolume(ctx, n.Name()); err != nil { return nil, err diff --git a/framework/docker/evstack/node.go b/framework/docker/evstack/node.go index facb14f..feb2e4b 100644 --- a/framework/docker/evstack/node.go +++ b/framework/docker/evstack/node.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "path" "path/filepath" "sync" "time" @@ -54,7 +53,10 @@ type Node struct { externalPorts types.Ports } -func NewNode(cfg Config, testName string, image container.Image, index int, isAggregator bool, additionalStartArgs []string) *Node { +func NewNode(cfg Config, testName string, image container.Image, homeDir string, index int, isAggregator bool, additionalStartArgs []string) *Node { + if homeDir == "" { + homeDir = DefaultHomeDir() + } logger := cfg.Logger.With( zap.Int("i", index), zap.Bool("aggregator", isAggregator), @@ -63,7 +65,7 @@ func NewNode(cfg Config, testName string, image container.Image, index int, isAg cfg: cfg, isAggregatorFlag: isAggregator, additionalStartArgs: additionalStartArgs, - Node: container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, path.Join("/var", "evstack"), index, EvstackType, logger), + Node: container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, homeDir, index, EvstackType, logger), } node.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, node.Name())) diff --git a/framework/docker/evstack/reth/builder.go b/framework/docker/evstack/reth/builder.go index a407a4a..51d1c3f 100644 --- a/framework/docker/evstack/reth/builder.go +++ b/framework/docker/evstack/reth/builder.go @@ -26,6 +26,7 @@ type NodeBuilder struct { genesis []byte jwtSecretHex string name string + homeDir string hyperlaneChainName string hyperlaneChainID uint64 hyperlaneDomainID uint32 @@ -104,6 +105,11 @@ func (b *NodeBuilder) WithName(name string) *NodeBuilder { return b } +func (b *NodeBuilder) WithHomeDir(homeDir string) *NodeBuilder { + b.homeDir = homeDir + return b +} + func (b *NodeBuilder) WithHyperlaneChainName(name string) *NodeBuilder { b.hyperlaneChainName = name return b @@ -121,12 +127,18 @@ func (b *NodeBuilder) WithHyperlaneDomainID(domainID uint32) *NodeBuilder { // Build constructs the Node and initializes its Docker volume but does not start the container. func (b *NodeBuilder) Build(ctx context.Context) (*Node, error) { + homeDir := b.homeDir + if homeDir == "" { + homeDir = DefaultHomeDir() + } + cfg := Config{ Logger: b.logger, DockerClient: b.dockerClient, DockerNetworkID: b.networkID, Image: b.image, Bin: b.bin, + HomeDir: homeDir, Env: b.env, AdditionalStartArgs: b.additionalStartArgs, JWTSecretHex: b.jwtSecretHex, diff --git a/framework/docker/evstack/reth/config.go b/framework/docker/evstack/reth/config.go index 766b2fb..8c4f4df 100644 --- a/framework/docker/evstack/reth/config.go +++ b/framework/docker/evstack/reth/config.go @@ -21,6 +21,8 @@ type Config struct { Image container.Image // Bin is the executable name (default: ev-reth) Bin string + // HomeDir is the home directory inside the container. Defaults to DefaultHomeDir(). + HomeDir string // Env are default environment variables applied to all nodes Env []string @@ -51,6 +53,11 @@ func (c Config) Validate() error { return nil } +// DefaultHomeDir returns the default home directory for Reth containers. +func DefaultHomeDir() string { + return "/home/ev-reth" +} + // DefaultImage returns the default container image for Reth nodes. func DefaultImage() container.Image { // Pin default to a known stable release for local/E2E reproducibility diff --git a/framework/docker/evstack/reth/node.go b/framework/docker/evstack/reth/node.go index 90271ea..93181d5 100644 --- a/framework/docker/evstack/reth/node.go +++ b/framework/docker/evstack/reth/node.go @@ -38,13 +38,12 @@ func newNode(ctx context.Context, cfg Config, testName string, index int, name s log := cfg.Logger.With(zap.String("component", "reth-node"), zap.Int("i", index)) - homeDir := "/home/ev-reth" n := &Node{ cfg: cfg, logger: log, name: name, } - n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, homeDir, index, NodeType, log) + n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, cfg.HomeDir, index, NodeType, log) n.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, n.Name())) if err := n.CreateAndSetupVolume(ctx, n.Name()); err != nil { diff --git a/framework/docker/evstack/spamoor/builder.go b/framework/docker/evstack/spamoor/builder.go index ce7753b..c1776eb 100644 --- a/framework/docker/evstack/spamoor/builder.go +++ b/framework/docker/evstack/spamoor/builder.go @@ -14,6 +14,7 @@ type Builder struct { dockerNetwork string logger *zap.Logger image container.Image + homeDir string rpcHosts []string privKey string @@ -30,8 +31,9 @@ func NewNodeBuilder(testName string) *Builder { func (b *Builder) WithDockerClient(c types.TastoraDockerClient) *Builder { b.dockerClient = c; return b } func (b *Builder) WithDockerNetworkID(id string) *Builder { b.dockerNetwork = id; return b } func (b *Builder) WithLogger(l *zap.Logger) *Builder { b.logger = l; return b } -func (b *Builder) WithImage(img container.Image) *Builder { b.image = img; return b } -func (b *Builder) WithRPCHosts(hosts ...string) *Builder { b.rpcHosts = hosts; return b } +func (b *Builder) WithImage(img container.Image) *Builder { b.image = img; return b } +func (b *Builder) WithHomeDir(homeDir string) *Builder { b.homeDir = homeDir; return b } +func (b *Builder) WithRPCHosts(hosts ...string) *Builder { b.rpcHosts = hosts; return b } func (b *Builder) WithPrivateKey(pk string) *Builder { b.privKey = pk; return b } func (b *Builder) WithNameSuffix(s string) *Builder { b.nameSuffix = s; return b } @@ -44,6 +46,6 @@ func (b *Builder) Build(ctx context.Context) (*Node, error) { RPCHosts: b.rpcHosts, PrivateKey: b.privKey, } - return newNode(ctx, cfg, b.testName, 0, b.nameSuffix) + return newNode(ctx, cfg, b.testName, 0, b.nameSuffix, b.homeDir) } diff --git a/framework/docker/evstack/spamoor/node.go b/framework/docker/evstack/spamoor/node.go index 1abcc3e..331494f 100644 --- a/framework/docker/evstack/spamoor/node.go +++ b/framework/docker/evstack/spamoor/node.go @@ -48,10 +48,13 @@ type Node struct { name string } -func newNode(ctx context.Context, cfg Config, testName string, index int, name string) (*Node, error) { +func newNode(ctx context.Context, cfg Config, testName string, index int, name string, homeDir string) (*Node, error) { + if homeDir == "" { + homeDir = "/home/spamoor" + } log := cfg.Logger.With(zap.String("component", "spamoor-daemon"), zap.Int("i", index)) n := &Node{cfg: cfg, logger: log, name: name} - n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, cfg.Image, "/home/spamoor", index, nodeType(0), log) + n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, cfg.Image, homeDir, index, nodeType(0), log) n.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, n.Name())) if err := n.CreateAndSetupVolume(ctx, n.Name()); err != nil { return nil, err From ad70f937892385342c36cc88d8d245b70e4be7a3 Mon Sep 17 00:00:00 2001 From: chatton Date: Wed, 13 May 2026 10:57:07 +0100 Subject: [PATCH 3/8] refactor: pass homeDir via Config struct and fix formatting Move homeDir from bare function parameter into Config structs for dataavailability and evstack packages, matching the pattern already used by evmsingle and reth. Add DefaultHomeDir() to spamoor for consistency. Fix spaces-to-tabs formatting across several files. --- framework/docker/container/job.go | 2 +- framework/docker/cosmos/chain_builder.go | 10 +- framework/docker/da_network_test.go | 138 +++++----- framework/docker/dataavailability/config.go | 2 + framework/docker/dataavailability/network.go | 56 ++-- .../dataavailability/network_builder.go | 3 +- framework/docker/dataavailability/node.go | 3 +- framework/docker/evstack/chain_builder.go | 3 +- framework/docker/evstack/config.go | 2 + framework/docker/evstack/evmsingle/config.go | 4 +- framework/docker/evstack/node.go | 3 +- framework/docker/evstack/reth/config.go | 4 +- framework/docker/evstack/spamoor/builder.go | 66 ++--- framework/docker/evstack/spamoor/node.go | 254 +++++++++--------- framework/docker/ibc/relayer/hermes_types.go | 2 +- framework/docker/internal/keyring.go | 2 +- framework/docker/reth_test.go | 14 +- 17 files changed, 291 insertions(+), 277 deletions(-) diff --git a/framework/docker/container/job.go b/framework/docker/container/job.go index d0c6645..15e35fe 100644 --- a/framework/docker/container/job.go +++ b/framework/docker/container/job.go @@ -14,10 +14,10 @@ import ( "time" "github.com/containerd/errdefs" + "github.com/moby/moby/api/pkg/stdcopy" "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/mount" "github.com/moby/moby/api/types/network" - "github.com/moby/moby/api/pkg/stdcopy" "github.com/moby/moby/client" "go.uber.org/zap" ) diff --git a/framework/docker/cosmos/chain_builder.go b/framework/docker/cosmos/chain_builder.go index 2aa23f9..f45fa7d 100644 --- a/framework/docker/cosmos/chain_builder.go +++ b/framework/docker/cosmos/chain_builder.go @@ -449,11 +449,11 @@ func (b *ChainBuilder) Build(ctx context.Context) (*Chain, error) { Env: b.env, GenesisFileBz: b.genesisBz, }, - t: b.t, - Validators: validators, - FullNodes: fullNodes, - cdc: cdc, - log: b.logger, + t: b.t, + Validators: validators, + FullNodes: fullNodes, + cdc: cdc, + log: b.logger, faucetWallet: b.faucetWallet, skipInit: b.skipInit, blockWaitTimeout: b.blockWaitTimeout, diff --git a/framework/docker/da_network_test.go b/framework/docker/da_network_test.go index 470ded8..b263e62 100644 --- a/framework/docker/da_network_test.go +++ b/framework/docker/da_network_test.go @@ -124,75 +124,75 @@ func TestDANetworkCreation(t *testing.T) { // TestDANetworkStopAndRestart ensures a DA network can be stopped and then restarted, // and nodes continue to respond to RPC after restart. func TestDANetworkStopAndRestart(t *testing.T) { - if testing.Short() { - t.Skip("skipping due to short mode") - } - t.Parallel() - configureBech32PrefixOnce() - - // Setup isolated docker environment for this test - testCfg := setupDockerTest(t) - - chain, err := testCfg.ChainBuilder.Build(testCfg.Ctx) - require.NoError(t, err) - - err = chain.Start(testCfg.Ctx) - require.NoError(t, err) - defer func() { _ = chain.Remove(testCfg.Ctx) }() - - // Default image for the DA network - defaultImage := container.Image{ - Repository: "ghcr.io/celestiaorg/celestia-node", - Version: "v0.26.4", - UIDGID: "10001:10001", - } - - // Create bridge node config - bridgeNodeConfig := da.NewNodeBuilder(). - WithNodeType(types.BridgeNode). - Build() - - // Create DA network with a single bridge node - daNetwork, err := testCfg.DANetworkBuilder. - WithChainID(chain.GetChainID()). - WithImage(defaultImage). - WithNodes(bridgeNodeConfig). - Build(testCfg.Ctx) - require.NoError(t, err) - - // Start the bridge node - bridgeNode := daNetwork.GetBridgeNodes()[0] - - chainNetworkInfo, err := chain.GetNodes()[0].GetNetworkInfo(testCfg.Ctx) - require.NoError(t, err, "failed to get network info") - hostname := chainNetworkInfo.Internal.Hostname - - chainID := chain.GetChainID() - genesisHash, err := getGenesisHash(testCfg.Ctx, chain) - require.NoError(t, err) - - require.NoError(t, bridgeNode.Start(testCfg.Ctx, - da.WithChainID(chainID), - da.WithAdditionalStartArguments("--p2p.network", chainID, "--core.ip", hostname, "--rpc.addr", "0.0.0.0"), - da.WithEnvironmentVariables(map[string]string{ - "CELESTIA_CUSTOM": types.BuildCelestiaCustomEnvVar(chainID, genesisHash, ""), - "P2P_NETWORK": chainID, - }), - )) - - // Verify it is responding - _, err = bridgeNode.GetP2PInfo(testCfg.Ctx) - require.NoError(t, err, "failed to get bridge node p2p info before stop") - - // Stop the entire DA network - require.NoError(t, daNetwork.Stop(testCfg.Ctx)) - - // Start the entire DA network again - require.NoError(t, daNetwork.Start(testCfg.Ctx)) - - // Verify RPC works after restart - _, err = bridgeNode.GetP2PInfo(testCfg.Ctx) - require.NoError(t, err, "failed to get bridge node p2p info after restart") + if testing.Short() { + t.Skip("skipping due to short mode") + } + t.Parallel() + configureBech32PrefixOnce() + + // Setup isolated docker environment for this test + testCfg := setupDockerTest(t) + + chain, err := testCfg.ChainBuilder.Build(testCfg.Ctx) + require.NoError(t, err) + + err = chain.Start(testCfg.Ctx) + require.NoError(t, err) + defer func() { _ = chain.Remove(testCfg.Ctx) }() + + // Default image for the DA network + defaultImage := container.Image{ + Repository: "ghcr.io/celestiaorg/celestia-node", + Version: "v0.26.4", + UIDGID: "10001:10001", + } + + // Create bridge node config + bridgeNodeConfig := da.NewNodeBuilder(). + WithNodeType(types.BridgeNode). + Build() + + // Create DA network with a single bridge node + daNetwork, err := testCfg.DANetworkBuilder. + WithChainID(chain.GetChainID()). + WithImage(defaultImage). + WithNodes(bridgeNodeConfig). + Build(testCfg.Ctx) + require.NoError(t, err) + + // Start the bridge node + bridgeNode := daNetwork.GetBridgeNodes()[0] + + chainNetworkInfo, err := chain.GetNodes()[0].GetNetworkInfo(testCfg.Ctx) + require.NoError(t, err, "failed to get network info") + hostname := chainNetworkInfo.Internal.Hostname + + chainID := chain.GetChainID() + genesisHash, err := getGenesisHash(testCfg.Ctx, chain) + require.NoError(t, err) + + require.NoError(t, bridgeNode.Start(testCfg.Ctx, + da.WithChainID(chainID), + da.WithAdditionalStartArguments("--p2p.network", chainID, "--core.ip", hostname, "--rpc.addr", "0.0.0.0"), + da.WithEnvironmentVariables(map[string]string{ + "CELESTIA_CUSTOM": types.BuildCelestiaCustomEnvVar(chainID, genesisHash, ""), + "P2P_NETWORK": chainID, + }), + )) + + // Verify it is responding + _, err = bridgeNode.GetP2PInfo(testCfg.Ctx) + require.NoError(t, err, "failed to get bridge node p2p info before stop") + + // Stop the entire DA network + require.NoError(t, daNetwork.Stop(testCfg.Ctx)) + + // Start the entire DA network again + require.NoError(t, daNetwork.Start(testCfg.Ctx)) + + // Verify RPC works after restart + _, err = bridgeNode.GetP2PInfo(testCfg.Ctx) + require.NoError(t, err, "failed to get bridge node p2p info after restart") } // TestModifyConfigFileDANetwork ensures modification of config files is possible by diff --git a/framework/docker/dataavailability/config.go b/framework/docker/dataavailability/config.go index 6f0e509..6f41941 100644 --- a/framework/docker/dataavailability/config.go +++ b/framework/docker/dataavailability/config.go @@ -24,6 +24,8 @@ type Config struct { Image container.Image // AdditionalStartArgs are additional arguments passed to all nodes when starting AdditionalStartArgs []string + // HomeDir is the home directory inside the container. Defaults to DefaultHomeDir(). + HomeDir string } // DefaultHomeDir returns the default home directory for DA node containers. diff --git a/framework/docker/dataavailability/network.go b/framework/docker/dataavailability/network.go index 00d3b54..c6f6fa2 100644 --- a/framework/docker/dataavailability/network.go +++ b/framework/docker/dataavailability/network.go @@ -67,7 +67,7 @@ func (n *Network) GetBridgeNodes() []*Node { // GetLightNodes returns only the light nodes in the network. func (n *Network) GetLightNodes() []*Node { - return n.GetNodesByType(types.LightNode) + return n.GetNodesByType(types.LightNode) } // AddNodes adds one or more nodes to the DA network with the given configurations. @@ -151,41 +151,41 @@ func (n *Network) RemoveNodes(ctx context.Context, nodeNames ...string) error { // Stop stops all nodes in the data availability network without removing them. func (n *Network) Stop(ctx context.Context) error { - nodes := n.GetNodes() - var eg errgroup.Group - for _, nd := range nodes { - nd := nd - eg.Go(func() error { - return nd.Stop(ctx) - }) - } - return eg.Wait() + nodes := n.GetNodes() + var eg errgroup.Group + for _, nd := range nodes { + nd := nd + eg.Go(func() error { + return nd.Stop(ctx) + }) + } + return eg.Wait() } // Remove stops and removes all nodes in the data availability network. // Matches the semantics of cosmos.Chain.Remove by operating on all components concurrently. func (n *Network) Remove(ctx context.Context, opts ...types.RemoveOption) error { - nodes := n.GetNodes() - var eg errgroup.Group - for _, nd := range nodes { - nd := nd - eg.Go(func() error { - return nd.Remove(ctx, opts...) - }) - } - return eg.Wait() + nodes := n.GetNodes() + var eg errgroup.Group + for _, nd := range nodes { + nd := nd + eg.Go(func() error { + return nd.Remove(ctx, opts...) + }) + } + return eg.Wait() } // Start starts all nodes in the data availability network. // If nodes were previously initialized and only stopped, this will only start their containers. func (n *Network) Start(ctx context.Context) error { - nodes := n.GetNodes() - var eg errgroup.Group - for _, nd := range nodes { - nd := nd - eg.Go(func() error { - return nd.Start(ctx) - }) - } - return eg.Wait() + nodes := n.GetNodes() + var eg errgroup.Group + for _, nd := range nodes { + nd := nd + eg.Go(func() error { + return nd.Start(ctx) + }) + } + return eg.Wait() } diff --git a/framework/docker/dataavailability/network_builder.go b/framework/docker/dataavailability/network_builder.go index cd2c7aa..a800446 100644 --- a/framework/docker/dataavailability/network_builder.go +++ b/framework/docker/dataavailability/network_builder.go @@ -222,6 +222,7 @@ func (b *NetworkBuilder) newNode(ctx context.Context, nodeConfig NodeConfig, ind ChainID: b.chainID, Bin: b.binaryName, Image: imageToUse, + HomeDir: b.homeDir, // Env and AdditionalStartArgs provide default set of values for all nodes, but can // be individually overridden by nodeConfig. Env: b.env, @@ -236,7 +237,7 @@ func (b *NetworkBuilder) newNode(ctx context.Context, nodeConfig NodeConfig, ind nodeConfig.Env = b.env } - node := NewNode(cfg, b.testName, imageToUse, b.homeDir, index, nodeConfig) + node := NewNode(cfg, b.testName, imageToUse, index, nodeConfig) // Create and setup volume using shared logic if err := node.CreateAndSetupVolume(ctx, node.Name()); err != nil { diff --git a/framework/docker/dataavailability/node.go b/framework/docker/dataavailability/node.go index 017247c..07fb651 100644 --- a/framework/docker/dataavailability/node.go +++ b/framework/docker/dataavailability/node.go @@ -76,7 +76,8 @@ type Node struct { externalPorts types.Ports } -func NewNode(cfg Config, testName string, image container.Image, homeDir string, index int, nodeConfig NodeConfig) *Node { +func NewNode(cfg Config, testName string, image container.Image, index int, nodeConfig NodeConfig) *Node { + homeDir := cfg.HomeDir if homeDir == "" { homeDir = DefaultHomeDir() } diff --git a/framework/docker/evstack/chain_builder.go b/framework/docker/evstack/chain_builder.go index fa54fde..900566a 100644 --- a/framework/docker/evstack/chain_builder.go +++ b/framework/docker/evstack/chain_builder.go @@ -193,9 +193,10 @@ func (b *ChainBuilder) newNode(ctx context.Context, nodeConfig NodeConfig, index Bin: b.binaryName, AggregatorPassphrase: b.aggregatorPassphrase, Image: imageToUse, + HomeDir: b.homeDir, } - node := NewNode(cfg, b.testName, imageToUse, b.homeDir, index, nodeConfig.IsAggregator, b.getAdditionalStartArgs(nodeConfig)) + node := NewNode(cfg, b.testName, imageToUse, index, nodeConfig.IsAggregator, b.getAdditionalStartArgs(nodeConfig)) // Create and setup volume using shared logic if err := node.CreateAndSetupVolume(ctx, node.Name()); err != nil { diff --git a/framework/docker/evstack/config.go b/framework/docker/evstack/config.go index eb75527..3d0085a 100644 --- a/framework/docker/evstack/config.go +++ b/framework/docker/evstack/config.go @@ -24,6 +24,8 @@ type Config struct { AggregatorPassphrase string // Image specifies the Docker image used for the evstack nodes. Image container.Image + // HomeDir is the home directory inside the container. Defaults to DefaultHomeDir(). + HomeDir string } // DefaultHomeDir returns the default home directory for evstack containers. diff --git a/framework/docker/evstack/evmsingle/config.go b/framework/docker/evstack/evmsingle/config.go index 74f7bef..8e24027 100644 --- a/framework/docker/evstack/evmsingle/config.go +++ b/framework/docker/evstack/evmsingle/config.go @@ -32,10 +32,10 @@ func DefaultHomeDir() string { // DefaultImage returns the default container image for ev-node-evm. func DefaultImage() container.Image { - return container.Image{Repository: "ghcr.io/evstack/ev-node-evm", Version: "v1.0.0-rc.4", UIDGID: "10001:10001"} + return container.Image{Repository: "ghcr.io/evstack/ev-node-evm", Version: "v1.0.0-rc.4", UIDGID: "10001:10001"} } // DefaultBinary returns the default binary name for ev-node-evm. func DefaultBinary() string { - return "evm" + return "evm" } diff --git a/framework/docker/evstack/node.go b/framework/docker/evstack/node.go index feb2e4b..cbae849 100644 --- a/framework/docker/evstack/node.go +++ b/framework/docker/evstack/node.go @@ -53,7 +53,8 @@ type Node struct { externalPorts types.Ports } -func NewNode(cfg Config, testName string, image container.Image, homeDir string, index int, isAggregator bool, additionalStartArgs []string) *Node { +func NewNode(cfg Config, testName string, image container.Image, index int, isAggregator bool, additionalStartArgs []string) *Node { + homeDir := cfg.HomeDir if homeDir == "" { homeDir = DefaultHomeDir() } diff --git a/framework/docker/evstack/reth/config.go b/framework/docker/evstack/reth/config.go index 8c4f4df..a23d676 100644 --- a/framework/docker/evstack/reth/config.go +++ b/framework/docker/evstack/reth/config.go @@ -60,6 +60,6 @@ func DefaultHomeDir() string { // DefaultImage returns the default container image for Reth nodes. func DefaultImage() container.Image { - // Pin default to a known stable release for local/E2E reproducibility - return container.Image{Repository: "ghcr.io/evstack/ev-reth", Version: "v0.2.2"} + // Pin default to a known stable release for local/E2E reproducibility + return container.Image{Repository: "ghcr.io/evstack/ev-reth", Version: "v0.2.2"} } diff --git a/framework/docker/evstack/spamoor/builder.go b/framework/docker/evstack/spamoor/builder.go index c1776eb..0f68ba5 100644 --- a/framework/docker/evstack/spamoor/builder.go +++ b/framework/docker/evstack/spamoor/builder.go @@ -1,51 +1,53 @@ package spamoor import ( - "context" + "context" - "github.com/celestiaorg/tastora/framework/docker/container" - "github.com/celestiaorg/tastora/framework/types" - "go.uber.org/zap" + "github.com/celestiaorg/tastora/framework/docker/container" + "github.com/celestiaorg/tastora/framework/types" + "go.uber.org/zap" ) type Builder struct { - testName string - dockerClient types.TastoraDockerClient - dockerNetwork string - logger *zap.Logger - image container.Image - homeDir string - - rpcHosts []string - privKey string - nameSuffix string + testName string + dockerClient types.TastoraDockerClient + dockerNetwork string + logger *zap.Logger + image container.Image + homeDir string + + rpcHosts []string + privKey string + nameSuffix string } func NewNodeBuilder(testName string) *Builder { - return &Builder{ - testName: testName, - image: container.NewImage("ethpandaops/spamoor", "latest", ""), - } + return &Builder{ + testName: testName, + image: container.NewImage("ethpandaops/spamoor", "latest", ""), + } } -func (b *Builder) WithDockerClient(c types.TastoraDockerClient) *Builder { b.dockerClient = c; return b } +func (b *Builder) WithDockerClient(c types.TastoraDockerClient) *Builder { + b.dockerClient = c + return b +} func (b *Builder) WithDockerNetworkID(id string) *Builder { b.dockerNetwork = id; return b } func (b *Builder) WithLogger(l *zap.Logger) *Builder { b.logger = l; return b } -func (b *Builder) WithImage(img container.Image) *Builder { b.image = img; return b } -func (b *Builder) WithHomeDir(homeDir string) *Builder { b.homeDir = homeDir; return b } -func (b *Builder) WithRPCHosts(hosts ...string) *Builder { b.rpcHosts = hosts; return b } +func (b *Builder) WithImage(img container.Image) *Builder { b.image = img; return b } +func (b *Builder) WithHomeDir(homeDir string) *Builder { b.homeDir = homeDir; return b } +func (b *Builder) WithRPCHosts(hosts ...string) *Builder { b.rpcHosts = hosts; return b } func (b *Builder) WithPrivateKey(pk string) *Builder { b.privKey = pk; return b } func (b *Builder) WithNameSuffix(s string) *Builder { b.nameSuffix = s; return b } func (b *Builder) Build(ctx context.Context) (*Node, error) { - cfg := Config{ - DockerClient: b.dockerClient, - DockerNetworkID: b.dockerNetwork, - Logger: b.logger, - Image: b.image, - RPCHosts: b.rpcHosts, - PrivateKey: b.privKey, - } - return newNode(ctx, cfg, b.testName, 0, b.nameSuffix, b.homeDir) + cfg := Config{ + DockerClient: b.dockerClient, + DockerNetworkID: b.dockerNetwork, + Logger: b.logger, + Image: b.image, + RPCHosts: b.rpcHosts, + PrivateKey: b.privKey, + } + return newNode(ctx, cfg, b.testName, 0, b.nameSuffix, b.homeDir) } - diff --git a/framework/docker/evstack/spamoor/node.go b/framework/docker/evstack/spamoor/node.go index 331494f..7ca3a76 100644 --- a/framework/docker/evstack/spamoor/node.go +++ b/framework/docker/evstack/spamoor/node.go @@ -1,20 +1,20 @@ package spamoor import ( - "context" - "fmt" - "net/http" - "strings" - "sync" - "time" - - "github.com/celestiaorg/tastora/framework/docker/container" - "github.com/celestiaorg/tastora/framework/docker/internal" - "github.com/celestiaorg/tastora/framework/types" - "net/netip" - - "github.com/moby/moby/api/types/network" - "go.uber.org/zap" + "context" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "github.com/celestiaorg/tastora/framework/docker/container" + "github.com/celestiaorg/tastora/framework/docker/internal" + "github.com/celestiaorg/tastora/framework/types" + "net/netip" + + "github.com/moby/moby/api/types/network" + "go.uber.org/zap" ) type nodeType int @@ -22,149 +22,153 @@ type nodeType int func (nodeType) String() string { return "spamoor" } type Ports struct { - Web string // web UI + /metrics + Web string // web UI + /metrics } func defaultInternalPorts() Ports { return Ports{Web: "8080"} } type Config struct { - DockerClient types.TastoraDockerClient - DockerNetworkID string - Logger *zap.Logger - Image container.Image + DockerClient types.TastoraDockerClient + DockerNetworkID string + Logger *zap.Logger + Image container.Image - RPCHosts []string - PrivateKey string + RPCHosts []string + PrivateKey string } type Node struct { - *container.Node - - cfg Config - logger *zap.Logger - started bool - mu sync.Mutex - external types.Ports // HTTP field stores web/metrics host port - name string + *container.Node + + cfg Config + logger *zap.Logger + started bool + mu sync.Mutex + external types.Ports // HTTP field stores web/metrics host port + name string +} + +// DefaultHomeDir returns the default home directory for spamoor containers. +func DefaultHomeDir() string { + return "/home/spamoor" } func newNode(ctx context.Context, cfg Config, testName string, index int, name string, homeDir string) (*Node, error) { - if homeDir == "" { - homeDir = "/home/spamoor" - } - log := cfg.Logger.With(zap.String("component", "spamoor-daemon"), zap.Int("i", index)) - n := &Node{cfg: cfg, logger: log, name: name} - n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, cfg.Image, homeDir, index, nodeType(0), log) - n.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, n.Name())) - if err := n.CreateAndSetupVolume(ctx, n.Name()); err != nil { - return nil, err - } - return n, nil + if homeDir == "" { + homeDir = DefaultHomeDir() + } + log := cfg.Logger.With(zap.String("component", "spamoor-daemon"), zap.Int("i", index)) + n := &Node{cfg: cfg, logger: log, name: name} + n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, cfg.Image, homeDir, index, nodeType(0), log) + n.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, n.Name())) + if err := n.CreateAndSetupVolume(ctx, n.Name()); err != nil { + return nil, err + } + return n, nil } func (n *Node) Name() string { - if n.name != "" { - return fmt.Sprintf("spamoor-%s-%d-%s", n.name, n.Index, internal.SanitizeDockerResourceName(n.TestName)) - } - return fmt.Sprintf("spamoor-%d-%s", n.Index, internal.SanitizeDockerResourceName(n.TestName)) + if n.name != "" { + return fmt.Sprintf("spamoor-%s-%d-%s", n.name, n.Index, internal.SanitizeDockerResourceName(n.TestName)) + } + return fmt.Sprintf("spamoor-%d-%s", n.Index, internal.SanitizeDockerResourceName(n.TestName)) } func (n *Node) HostName() string { return internal.CondenseHostName(n.Name()) } func (n *Node) GetNetworkInfo(ctx context.Context) (types.NetworkInfo, error) { - internalIP, err := internal.GetContainerInternalIP(ctx, n.DockerClient, n.ContainerLifecycle.ContainerID()) - if err != nil { - return types.NetworkInfo{}, err - } - return types.NetworkInfo{ - Internal: types.Network{Hostname: n.HostName(), IP: internalIP, Ports: types.Ports{HTTP: defaultInternalPorts().Web}}, - External: types.Network{Hostname: "0.0.0.0", Ports: n.external}, - }, nil + internalIP, err := internal.GetContainerInternalIP(ctx, n.DockerClient, n.ContainerLifecycle.ContainerID()) + if err != nil { + return types.NetworkInfo{}, err + } + return types.NetworkInfo{ + Internal: types.Network{Hostname: n.HostName(), IP: internalIP, Ports: types.Ports{HTTP: defaultInternalPorts().Web}}, + External: types.Network{Hostname: "0.0.0.0", Ports: n.external}, + }, nil } func (n *Node) Start(ctx context.Context) error { - n.mu.Lock() - defer n.mu.Unlock() - if n.started { - return n.StartContainer(ctx) - } - if err := n.createNodeContainer(ctx); err != nil { - return err - } - if err := n.ContainerLifecycle.StartContainer(ctx); err != nil { - return err - } - hostPorts, err := n.ContainerLifecycle.GetHostPorts(ctx, defaultInternalPorts().Web+"/tcp") - if err != nil { - return err - } - mapped := internal.MustExtractPort(hostPorts[0]) - n.external = types.Ports{HTTP: mapped} - n.started = true - // readiness wait for /metrics endpoint (best-effort) - waitHTTP(fmt.Sprintf("http://127.0.0.1:%s/metrics", n.external.HTTP), 20*time.Second) - return nil + n.mu.Lock() + defer n.mu.Unlock() + if n.started { + return n.StartContainer(ctx) + } + if err := n.createNodeContainer(ctx); err != nil { + return err + } + if err := n.ContainerLifecycle.StartContainer(ctx); err != nil { + return err + } + hostPorts, err := n.ContainerLifecycle.GetHostPorts(ctx, defaultInternalPorts().Web+"/tcp") + if err != nil { + return err + } + mapped := internal.MustExtractPort(hostPorts[0]) + n.external = types.Ports{HTTP: mapped} + n.started = true + // readiness wait for /metrics endpoint (best-effort) + waitHTTP(fmt.Sprintf("http://127.0.0.1:%s/metrics", n.external.HTTP), 20*time.Second) + return nil } // API returns a client bound to this node's exposed HTTP port. func (n *Node) API() *API { - base := fmt.Sprintf("http://127.0.0.1:%s", n.external.HTTP) - return NewAPI(base) + base := fmt.Sprintf("http://127.0.0.1:%s", n.external.HTTP) + return NewAPI(base) } func (n *Node) createNodeContainer(ctx context.Context) error { - p := defaultInternalPorts() - - // Daemon flags only; entrypoint will be spamoor-daemon - dbPath := fmt.Sprintf("%s/%s", n.HomeDir(), "spamoor.db") - binds := n.Bind() - cmd := []string{ - "--privkey", n.cfg.PrivateKey, - "--port", p.Web, - "--db", dbPath, - } - for _, h := range n.cfg.RPCHosts { - if s := strings.TrimSpace(h); s != "" { - cmd = append(cmd, "--rpchost", s) - } - } - - port := network.MustParsePort(p.Web + "/tcp") - ports := network.PortMap{ - port: []network.PortBinding{{HostIP: netip.MustParseAddr("0.0.0.0"), HostPort: ""}}, - } - - // IMPORTANT: override entrypoint to the daemon (absolute path inside image) - return n.CreateContainer( - ctx, - n.TestName, - n.NetworkID, - n.cfg.Image, - ports, - "", - binds, - nil, - n.HostName(), - cmd, - nil, - []string{"/app/spamoor-daemon"}, // entrypoint override - ) + p := defaultInternalPorts() + + // Daemon flags only; entrypoint will be spamoor-daemon + dbPath := fmt.Sprintf("%s/%s", n.HomeDir(), "spamoor.db") + binds := n.Bind() + cmd := []string{ + "--privkey", n.cfg.PrivateKey, + "--port", p.Web, + "--db", dbPath, + } + for _, h := range n.cfg.RPCHosts { + if s := strings.TrimSpace(h); s != "" { + cmd = append(cmd, "--rpchost", s) + } + } + + port := network.MustParsePort(p.Web + "/tcp") + ports := network.PortMap{ + port: []network.PortBinding{{HostIP: netip.MustParseAddr("0.0.0.0"), HostPort: ""}}, + } + + // IMPORTANT: override entrypoint to the daemon (absolute path inside image) + return n.CreateContainer( + ctx, + n.TestName, + n.NetworkID, + n.cfg.Image, + ports, + "", + binds, + nil, + n.HostName(), + cmd, + nil, + []string{"/app/spamoor-daemon"}, // entrypoint override + ) } // waitHTTP polls a URL until it succeeds or the timeout elapses. func waitHTTP(url string, timeout time.Duration) { - deadline := time.Now().Add(timeout) - client := &http.Client{Timeout: 500 * time.Millisecond} - for time.Now().Before(deadline) { - resp, err := client.Get(url) - if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 500 { - _ = resp.Body.Close() - return - } - if resp != nil && resp.Body != nil { - _ = resp.Body.Close() - } - time.Sleep(100 * time.Millisecond) - } + deadline := time.Now().Add(timeout) + client := &http.Client{Timeout: 500 * time.Millisecond} + for time.Now().Before(deadline) { + resp, err := client.Get(url) + if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 500 { + _ = resp.Body.Close() + return + } + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + time.Sleep(100 * time.Millisecond) + } } - diff --git a/framework/docker/ibc/relayer/hermes_types.go b/framework/docker/ibc/relayer/hermes_types.go index cecd8f1..fbb341f 100644 --- a/framework/docker/ibc/relayer/hermes_types.go +++ b/framework/docker/ibc/relayer/hermes_types.go @@ -33,4 +33,4 @@ type CreateConnectionResult struct { // ConnectionSide captures the connection ID for each side type ConnectionSide struct { ConnectionID string `json:"connection_id"` -} \ No newline at end of file +} diff --git a/framework/docker/internal/keyring.go b/framework/docker/internal/keyring.go index 4f8f57b..b14d9a9 100644 --- a/framework/docker/internal/keyring.go +++ b/framework/docker/internal/keyring.go @@ -11,10 +11,10 @@ import ( "path/filepath" "github.com/cosmos/cosmos-sdk/codec" - "github.com/moby/moby/client" codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/moby/moby/client" ) // NewLocalKeyringFromDockerContainer copies the contents of the given container directory into a specified local directory. diff --git a/framework/docker/reth_test.go b/framework/docker/reth_test.go index b61b787..6f4b251 100644 --- a/framework/docker/reth_test.go +++ b/framework/docker/reth_test.go @@ -1,12 +1,12 @@ package docker import ( - "math/big" - "strings" - "testing" - "time" + "math/big" + "strings" + "testing" + "time" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/require" ) // TestRethNode_LivenessAndGenesis verifies the first-class reth resource by @@ -20,8 +20,8 @@ func TestRethNode_LivenessAndGenesis(t *testing.T) { testCfg := setupDockerTest(t) - // Build a single Reth node from pre-configured builder - node, err := testCfg.RethBuilder.Build(testCfg.Ctx) + // Build a single Reth node from pre-configured builder + node, err := testCfg.RethBuilder.Build(testCfg.Ctx) require.NoError(t, err) t.Cleanup(func() { From bcd4a12127fa92bcb918e77626d313447aff6209 Mon Sep 17 00:00:00 2001 From: chatton Date: Wed, 13 May 2026 11:16:49 +0100 Subject: [PATCH 4/8] fix: set container User from image UIDGID in CreateContainer Volume ownership was set via UIDGID but the container runtime user was not, causing containers to run as root while volumes were owned by a different UID. Setting User in container.Config ensures the runtime user matches the volume owner. --- framework/docker/container/lifecycle.go | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/docker/container/lifecycle.go b/framework/docker/container/lifecycle.go index fb4f1c1..64c6303 100644 --- a/framework/docker/container/lifecycle.go +++ b/framework/docker/container/lifecycle.go @@ -102,6 +102,7 @@ func (c *Lifecycle) CreateContainer( Name: c.containerName, Config: &container.Config{ Image: imageRef, + User: image.UIDGID, Entrypoint: entrypoint, Cmd: cmd, From 0d373f0dc4c7fb97396f0d7061a1c213633423c2 Mon Sep 17 00:00:00 2001 From: chatton Date: Wed, 13 May 2026 12:40:19 +0100 Subject: [PATCH 5/8] fix: only set container User when UIDGID is explicitly specified Setting User to empty string overrides the image USER directive and forces the container to run as root. Only set it when UIDGID is non-empty to preserve the image default. --- framework/docker/container/lifecycle.go | 28 ++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/framework/docker/container/lifecycle.go b/framework/docker/container/lifecycle.go index 64c6303..1ecaa0f 100644 --- a/framework/docker/container/lifecycle.go +++ b/framework/docker/container/lifecycle.go @@ -96,21 +96,25 @@ func (c *Lifecycle) CreateContainer( } } + containerCfg := &container.Config{ + Image: imageRef, + + Entrypoint: entrypoint, + Cmd: cmd, + Env: env, + Hostname: hostName, + Labels: map[string]string{consts.CleanupLabel: c.client.CleanupLabel()}, + ExposedPorts: pS, + } + if image.UIDGID != "" { + containerCfg.User = image.UIDGID + } + cc, err := c.client.ContainerCreate( ctx, client.ContainerCreateOptions{ - Name: c.containerName, - Config: &container.Config{ - Image: imageRef, - User: image.UIDGID, - - Entrypoint: entrypoint, - Cmd: cmd, - Env: env, - Hostname: hostName, - Labels: map[string]string{consts.CleanupLabel: c.client.CleanupLabel()}, - ExposedPorts: pS, - }, + Name: c.containerName, + Config: containerCfg, HostConfig: &container.HostConfig{ Binds: volumeBinds, PortBindings: pb, From 9e646c92b710540df112e5f436c79720dbba64fb Mon Sep 17 00:00:00 2001 From: chatton Date: Wed, 13 May 2026 13:25:48 +0100 Subject: [PATCH 6/8] fix: propagate UIDGID to Job exec and remove non-root from simd image Pass image UIDGID as User in Node.Exec so init commands run as the same user as the main container, preventing file ownership mismatches. Remove UIDGID from ibc-go-simd test image since simd writes to $HOME/.simapp on startup which fails as non-root. --- framework/docker/container/node.go | 1 + framework/docker/ibc_test.go | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/docker/container/node.go b/framework/docker/container/node.go index e1ad6a9..6182005 100644 --- a/framework/docker/container/node.go +++ b/framework/docker/container/node.go @@ -69,6 +69,7 @@ func (n *Node) Exec(ctx context.Context, logger *zap.Logger, cmd []string, env [ opts := Options{ Env: env, Binds: n.Bind(), + User: n.Image.UIDGID, } res := job.Run(ctx, cmd, opts) if res.Err != nil { diff --git a/framework/docker/ibc_test.go b/framework/docker/ibc_test.go index 0a9d691..11530c2 100644 --- a/framework/docker/ibc_test.go +++ b/framework/docker/ibc_test.go @@ -56,7 +56,8 @@ func createSimappChain(t *testing.T, ctx context.Context, client types.TastoraDo // use the simapp from ibc-go as a simple app with basic wiring and no token filters. // TODO: this is a custom built simapp that has the bech32prefix as "celestia" as a workaround for the global // SDK config not being usable when 2 chains have a different beck32 preix (e.g. "celestia" and "cosmos" ) if it is sealed. - WithImage(container.NewImage("ghcr.io/chatton/ibc-go-simd", "v8.5.0", "1000:1000")). + // simd tries to mkdir $HOME/.simapp on startup which fails as non-root + WithImage(container.NewImage("ghcr.io/chatton/ibc-go-simd", "v8.5.0", "")). WithBinaryName("simd"). WithBech32Prefix("celestia"). WithDenom("stake"). From 5eedd2cb82dcd5c109dca7a48465600870488279 Mon Sep 17 00:00:00 2001 From: chatton Date: Wed, 13 May 2026 13:37:45 +0100 Subject: [PATCH 7/8] fix: use explicit root UIDGID for simd image instead of omitting it --- framework/docker/ibc_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/docker/ibc_test.go b/framework/docker/ibc_test.go index 11530c2..7a27e5b 100644 --- a/framework/docker/ibc_test.go +++ b/framework/docker/ibc_test.go @@ -56,8 +56,8 @@ func createSimappChain(t *testing.T, ctx context.Context, client types.TastoraDo // use the simapp from ibc-go as a simple app with basic wiring and no token filters. // TODO: this is a custom built simapp that has the bech32prefix as "celestia" as a workaround for the global // SDK config not being usable when 2 chains have a different beck32 preix (e.g. "celestia" and "cosmos" ) if it is sealed. - // simd tries to mkdir $HOME/.simapp on startup which fails as non-root - WithImage(container.NewImage("ghcr.io/chatton/ibc-go-simd", "v8.5.0", "")). + // simd writes to $HOME/.simapp on startup, requires root + WithImage(container.NewImage("ghcr.io/chatton/ibc-go-simd", "v8.5.0", "0:0")). WithBinaryName("simd"). WithBech32Prefix("celestia"). WithDenom("stake"). From f7a3d822a91735beedf53c8d86ef62de1ca0a6c4 Mon Sep 17 00:00:00 2001 From: chatton Date: Wed, 13 May 2026 14:00:19 +0100 Subject: [PATCH 8/8] fix: use root UIDGID for forwarding-relayer image Binary writes to /app/storage/ outside the volume mount, requires root. --- framework/docker/hyperlane/hyperlane_config.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/docker/hyperlane/hyperlane_config.go b/framework/docker/hyperlane/hyperlane_config.go index f0903ba..c69dfe3 100644 --- a/framework/docker/hyperlane/hyperlane_config.go +++ b/framework/docker/hyperlane/hyperlane_config.go @@ -24,7 +24,8 @@ func DefaultDeployerImage() container.Image { } func DefaultForwardRelayerImage() container.Image { - return container.NewImage("ghcr.io/celestiaorg/forwarding-relayer", "v0.1.0", "1000:1000") + // binary writes to /app/storage/ which is outside the volume, requires root + return container.NewImage("ghcr.io/celestiaorg/forwarding-relayer", "v0.1.0", "0:0") } // CosmosConfig contains the IDs of all deployed cosmos-native hyperlane components