From 70b73807816f9e995bbd8d5480c5428eddf17bbc Mon Sep 17 00:00:00 2001 From: chatton Date: Mon, 23 Feb 2026 11:09:21 +0000 Subject: [PATCH 01/11] wip: passing test, but hacky --- execution/evm/test/test_helpers.go | 71 +++++++++++++++----------- test/docker-e2e/go.mod | 2 +- test/docker-e2e/go.sum | 4 +- test/e2e/evm_spamoor_smoke_test.go | 82 ++++++++++++++++++++++++++---- test/e2e/evm_test_common.go | 8 +-- test/e2e/go.mod | 5 +- test/e2e/go.sum | 2 - 7 files changed, 123 insertions(+), 51 deletions(-) diff --git a/execution/evm/test/test_helpers.go b/execution/evm/test/test_helpers.go index 1aa4ec317..f8d090e70 100644 --- a/execution/evm/test/test_helpers.go +++ b/execution/evm/test/test_helpers.go @@ -24,11 +24,14 @@ import ( // Test-scoped Docker client/network mapping to avoid conflicts between tests var ( - dockerClients = make(map[string]types.TastoraDockerClient) - dockerNetworks = make(map[string]string) - dockerMutex sync.RWMutex + dockerClients = make(map[string]types.TastoraDockerClient) + dockerNetworks = make(map[string]string) + dockerMutex sync.RWMutex ) +// RethNodeOpt allows tests to customize the reth node builder before building. +type RethNodeOpt func(b *reth.NodeBuilder) + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" func randomString(n int) string { @@ -42,26 +45,31 @@ func randomString(n int) string { // getTestScopedDockerSetup returns a Docker client and network ID that are scoped to the specific test. func getTestScopedDockerSetup(t testing.TB) (types.TastoraDockerClient, string) { - t.Helper() - - testKey := t.Name() - dockerMutex.Lock() - defer dockerMutex.Unlock() - - dockerCli, exists := dockerClients[testKey] - if !exists { - cli, netID := docker.Setup(t) - dockerClients[testKey] = cli - dockerNetworks[testKey] = netID - dockerCli = cli - } - dockerNetID := dockerNetworks[testKey] - - return dockerCli, dockerNetID + t.Helper() + + testKey := t.Name() + dockerMutex.Lock() + defer dockerMutex.Unlock() + + dockerCli, exists := dockerClients[testKey] + if !exists { + cli, netID := docker.Setup(t) + dockerClients[testKey] = cli + dockerNetworks[testKey] = netID + dockerCli = cli + } + dockerNetID := dockerNetworks[testKey] + + return dockerCli, dockerNetID } +// SetExtraRethEnvForTest sets additional environment variables to be applied +// when building the test-scoped reth node for the given test name. +// Call this before SetupTestRethNode in the same test. +// (no global setters; prefer passing RethNodeOpt to SetupTestRethNode) + // SetupTestRethNode creates a single Reth node for testing purposes. -func SetupTestRethNode(t testing.TB) *reth.Node { +func SetupTestRethNode(t testing.TB, opts ...RethNodeOpt) *reth.Node { t.Helper() ctx := context.Background() @@ -72,15 +80,20 @@ func SetupTestRethNode(t testing.TB) *reth.Node { if testing.Verbose() { logger = zaptest.NewLogger(t) } - n, err := new(reth.NodeBuilder). - WithTestName(testName). - WithLogger(logger). - WithImage(reth.DefaultImage()). - WithBin("ev-reth"). - WithDockerClient(dockerCli). - WithDockerNetworkID(dockerNetID). - WithGenesis([]byte(reth.DefaultEvolveGenesisJSON())). - Build(ctx) + b := new(reth.NodeBuilder). + WithTestName(testName). + WithLogger(logger). + WithImage(reth.DefaultImage()). + WithBin("ev-reth"). + WithDockerClient(dockerCli). + WithDockerNetworkID(dockerNetID). + WithGenesis([]byte(reth.DefaultEvolveGenesisJSON())) + for _, opt := range opts { + if opt != nil { + opt(b) + } + } + n, err := b.Build(ctx) t.Cleanup(func() { _ = n.Remove(context.Background()) }) diff --git a/test/docker-e2e/go.mod b/test/docker-e2e/go.mod index 783810301..486ae9a0d 100644 --- a/test/docker-e2e/go.mod +++ b/test/docker-e2e/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( cosmossdk.io/math v1.5.3 - github.com/celestiaorg/tastora v0.12.0 + github.com/celestiaorg/tastora v0.15.0 github.com/ethereum/go-ethereum v1.16.8 github.com/evstack/ev-node/execution/evm v1.0.0-rc.3 github.com/libp2p/go-libp2p v0.47.0 diff --git a/test/docker-e2e/go.sum b/test/docker-e2e/go.sum index 8cd19ff08..c3928d7ba 100644 --- a/test/docker-e2e/go.sum +++ b/test/docker-e2e/go.sum @@ -147,8 +147,8 @@ github.com/celestiaorg/go-square/v3 v3.0.2 h1:eSQOgNII8inK9IhiBZ+6GADQeWbRq4HYY7 github.com/celestiaorg/go-square/v3 v3.0.2/go.mod h1:oFReMLsSDMRs82ICFEeFQFCqNvwdsbIM1BzCcb0f7dM= github.com/celestiaorg/nmt v0.24.2 h1:LlpJSPOd6/Lw1Ig6HUhZuqiINHLka/ZSRTBzlNJpchg= github.com/celestiaorg/nmt v0.24.2/go.mod h1:vgLBpWBi8F5KLxTdXSwb7AU4NhiIQ1AQRGa+PzdcLEA= -github.com/celestiaorg/tastora v0.12.0 h1:xs1a/d+/QFbebShoHZxnWj8q3Kr1i6PY5137oxR+h+4= -github.com/celestiaorg/tastora v0.12.0/go.mod h1:ObeKMraNab/xofYZyylnOEiveHvUAPdKP5HiNjnvQoM= +github.com/celestiaorg/tastora v0.15.0 h1:rpXX/y19BzZ6Qf3pCc2YxJid2uwIJbTFf+BgBruOD34= +github.com/celestiaorg/tastora v0.15.0/go.mod h1:C867PBm6Ne6e/1JlmsRqcLeJ6RHAuMoMRCvwJzV/q8g= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= diff --git a/test/e2e/evm_spamoor_smoke_test.go b/test/e2e/evm_spamoor_smoke_test.go index 0c65fc3bc..e84be6e69 100644 --- a/test/e2e/evm_spamoor_smoke_test.go +++ b/test/e2e/evm_spamoor_smoke_test.go @@ -7,12 +7,17 @@ import ( "fmt" "net/http" "path/filepath" + "strings" "testing" "time" + tastoradocker "github.com/celestiaorg/tastora/framework/docker" + reth "github.com/celestiaorg/tastora/framework/docker/evstack/reth" spamoor "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" + jaeger "github.com/celestiaorg/tastora/framework/docker/jaeger" dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" ) // TestSpamoorSmoke spins up reth + sequencer and a Spamoor node, starts a few @@ -21,20 +26,47 @@ func TestSpamoorSmoke(t *testing.T) { t.Parallel() sut := NewSystemUnderTest(t) + // Prepare a shared docker client and network for Jaeger and reth. + ctx := t.Context() + dcli, netID := tastoradocker.Setup(t) + jcfg := jaeger.Config{Logger: zaptest.NewLogger(t), DockerClient: dcli, DockerNetworkID: netID} + jg, err := jaeger.New(ctx, jcfg, t.Name(), 0) + require.NoError(t, err, "failed to create jaeger node") + t.Cleanup(func() { _ = jg.Remove(t.Context()) }) + require.NoError(t, jg.Start(ctx), "failed to start jaeger node") + // Bring up reth + local DA and start sequencer with default settings. - seqJWT, _, genesisHash, endpoints, rethNode := setupCommonEVMTest(t, sut, false) + seqJWT, _, genesisHash, endpoints, rethNode := setupCommonEVMTest(t, sut, false, + func(b *reth.NodeBuilder) { + b.WithDockerClient(dcli). + WithDockerNetworkID(netID). + WithEnv( + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="+jg.IngestHTTPEndpoint()+"/v1/traces", + "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http", + "RUST_LOG=info", + "OTEL_SDK_DISABLED=false", + ) + }, + ) sequencerHome := filepath.Join(t.TempDir(), "sequencer") - // In-process OTLP/HTTP collector to capture ev-node spans. - collector := newOTLPCollector(t) - t.Cleanup(func() { - _ = collector.close() - }) + // ev-node runs on the host, so use Jaeger's host-mapped OTLP/HTTP port (external address). + jinfo, err := jg.GetNetworkInfo(ctx) + require.NoError(t, err, "failed to get jaeger network info") + otlpHTTP := fmt.Sprintf("http://127.0.0.1:%s", jinfo.External.Ports.HTTP) - // Start sequencer with tracing to our collector. + // Configure ev-reth to export traces to Jaeger (Rust OTLP exporter expects explicit /v1/traces path). + //evmtest.SetExtraRethEnvForTest(t.Name(), + // "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="+jg.IngestHTTPEndpoint()+"/v1/traces", + // "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http", + // "RUST_LOG=info", + // "OTEL_SDK_DISABLED=false", + //) + + // Start sequencer with tracing to Jaeger collector. setupSequencerNode(t, sut, sequencerHome, seqJWT, genesisHash, endpoints, "--evnode.instrumentation.tracing=true", - "--evnode.instrumentation.tracing_endpoint", collector.endpoint(), + "--evnode.instrumentation.tracing_endpoint", otlpHTTP, "--evnode.instrumentation.tracing_sample_rate", "1.0", "--evnode.instrumentation.tracing_service_name", "ev-node-smoke", ) @@ -45,6 +77,11 @@ func TestSpamoorSmoke(t *testing.T) { require.NoError(t, err, "failed to get network info") internalRPC := "http://" + ni.Internal.RPCAddress() + // Preferred typed clients from tastora's reth node helpers + ethCli, err := rethNode.GetEthClient(ctx) + require.NoError(t, err, "failed to get ethclient") + rpcCli, err := rethNode.GetRPCClient(ctx) + require.NoError(t, err, "failed to get rpc client") spBuilder := spamoor.NewNodeBuilder(t.Name()). WithDockerClient(rethNode.DockerClient). @@ -53,7 +90,7 @@ func TestSpamoorSmoke(t *testing.T) { WithRPCHosts(internalRPC). WithPrivateKey(TestPrivateKey) - ctx := t.Context() + ctx = t.Context() spNode, err := spBuilder.Build(ctx) require.NoError(t, err, "failed to build sp node") @@ -120,8 +157,31 @@ func TestSpamoorSmoke(t *testing.T) { sent := sumCounter(metrics["spamoor_transactions_sent_total"]) fail := sumCounter(metrics["spamoor_transactions_failed_total"]) - time.Sleep(2 * time.Second) - printCollectedTraceReport(t, collector) + // Probe ev-reth via JSON-RPC as proxy metrics: head height should advance; peer count should be >= 0. + h1, err := ethCli.BlockNumber(ctx) + require.NoError(t, err, "failed to query initial block number") + time.Sleep(5 * time.Second) + h2, err := ethCli.BlockNumber(ctx) + require.NoError(t, err, "failed to query subsequent block number") + var peerCountHex string + require.NoError(t, rpcCli.CallContext(ctx, &peerCountHex, "net_peerCount")) + t.Logf("reth head: %d -> %d, net_peerCount=%s", h1, h2, strings.TrimSpace(peerCountHex)) + + // Verify Jaeger received traces from ev-node. + // Service name is set above via --evnode.instrumentation.tracing_service_name "ev-node-smoke". + traceCtx, cancel := context.WithTimeout(ctx, 3*time.Minute) + defer cancel() + ok, err := jg.External.WaitForTraces(traceCtx, "ev-node-smoke", 1, 2*time.Second) + require.NoError(t, err, "error while waiting for Jaeger traces; UI: %s", jg.QueryHostURL()) + require.True(t, ok, "expected at least one trace in Jaeger; UI: %s", jg.QueryHostURL()) + + // Also wait for traces from ev-reth and print a small sample. + ok, err = jg.External.WaitForTraces(traceCtx, "ev-reth", 1, 2*time.Second) + require.NoError(t, err, "error while waiting for ev-reth traces; UI: %s", jg.External.URL()) + require.True(t, ok, "expected at least one trace from ev-reth; UI: %s", jg.External.URL()) + if traces, err := jg.External.Traces(traceCtx, "ev-reth", 3); err == nil && len(traces) > 0 { + t.Logf("sample ev-reth traces: %v", traces[0]) + } require.Greater(t, sent, float64(0), "at least one transaction should have been sent") require.Zero(t, fail, "no transactions should have failed") diff --git a/test/e2e/evm_test_common.go b/test/e2e/evm_test_common.go index 80aa21721..c78f61990 100644 --- a/test/e2e/evm_test_common.go +++ b/test/e2e/evm_test_common.go @@ -513,7 +513,7 @@ func submitTransactionAndGetBlockNumber(t testing.TB, sequencerClients ...*ethcl // - daPort: optional DA port to use (if empty, uses default) // // Returns: jwtSecret, fullNodeJwtSecret (empty if needsFullNode=false), genesisHash -func setupCommonEVMTest(t testing.TB, sut *SystemUnderTest, needsFullNode bool) (string, string, string, *TestEndpoints, *reth.Node) { +func setupCommonEVMTest(t testing.TB, sut *SystemUnderTest, needsFullNode bool, rethOpts ...evmtest.RethNodeOpt) (string, string, string, *TestEndpoints, *reth.Node) { t.Helper() // Reset global nonce for each test to ensure clean state @@ -531,7 +531,7 @@ func setupCommonEVMTest(t testing.TB, sut *SystemUnderTest, needsFullNode bool) sut.ExecCmd(localDABinary, "-port", dynEndpoints.DAPort) t.Logf("Started local DA on port %s", dynEndpoints.DAPort) - rethNode := evmtest.SetupTestRethNode(t) + rethNode := evmtest.SetupTestRethNode(t, rethOpts...) networkInfo, err := rethNode.GetNetworkInfo(context.Background()) require.NoError(t, err, "failed to get reth network info") @@ -540,8 +540,8 @@ func setupCommonEVMTest(t testing.TB, sut *SystemUnderTest, needsFullNode bool) var fnJWT string var rethFn *reth.Node - if needsFullNode { - rethFn = evmtest.SetupTestRethNode(t) + if needsFullNode { + rethFn = evmtest.SetupTestRethNode(t, rethOpts...) fnJWT = rethFn.JWTSecretHex() } diff --git a/test/e2e/go.mod b/test/e2e/go.mod index 8d04cfcc0..cd119464c 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -5,7 +5,7 @@ go 1.25.6 require ( cosmossdk.io/math v1.5.3 github.com/celestiaorg/go-square/v3 v3.0.2 - github.com/celestiaorg/tastora v0.14.0 + github.com/celestiaorg/tastora v0.15.0 github.com/cosmos/cosmos-sdk v0.53.6 github.com/cosmos/ibc-go/v8 v8.8.0 github.com/ethereum/go-ethereum v1.17.0 @@ -17,10 +17,12 @@ require ( github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/proto/otlp v1.9.0 + go.uber.org/zap v1.27.1 google.golang.org/protobuf v1.36.11 ) replace ( + github.com/celestiaorg/tastora => ../../../../celestiaorg/tastora github.com/evstack/ev-node => ../../ github.com/evstack/ev-node/core => ../../core github.com/evstack/ev-node/execution/evm => ../../execution/evm @@ -294,7 +296,6 @@ require ( go.uber.org/fx v1.24.0 // indirect go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.17.0 // indirect diff --git a/test/e2e/go.sum b/test/e2e/go.sum index 464179e01..0d6619217 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -145,8 +145,6 @@ github.com/celestiaorg/go-square/v3 v3.0.2 h1:eSQOgNII8inK9IhiBZ+6GADQeWbRq4HYY7 github.com/celestiaorg/go-square/v3 v3.0.2/go.mod h1:oFReMLsSDMRs82ICFEeFQFCqNvwdsbIM1BzCcb0f7dM= github.com/celestiaorg/nmt v0.24.2 h1:LlpJSPOd6/Lw1Ig6HUhZuqiINHLka/ZSRTBzlNJpchg= github.com/celestiaorg/nmt v0.24.2/go.mod h1:vgLBpWBi8F5KLxTdXSwb7AU4NhiIQ1AQRGa+PzdcLEA= -github.com/celestiaorg/tastora v0.14.0 h1:kvcx1MSKTx4DjOW60g9lcndL/LFpLq3H+CbzglaNlA0= -github.com/celestiaorg/tastora v0.14.0/go.mod h1:ObeKMraNab/xofYZyylnOEiveHvUAPdKP5HiNjnvQoM= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= From 938e6d29662afff94e86eaedaaa24be882fe3ff0 Mon Sep 17 00:00:00 2001 From: chatton Date: Mon, 23 Feb 2026 13:27:31 +0000 Subject: [PATCH 02/11] chore: refactor test setup to allow for injection of docker client --- execution/evm/test/execution_test.go | 7 ++- execution/evm/test/test_helpers.go | 53 ++++++---------- test/e2e/evm_contract_e2e_test.go | 26 ++++---- test/e2e/evm_da_restart_e2e_test.go | 4 +- test/e2e/evm_force_inclusion_e2e_test.go | 69 +++++++++++---------- test/e2e/evm_full_node_e2e_test.go | 60 +++++++++--------- test/e2e/evm_sequencer_e2e_test.go | 4 +- test/e2e/evm_spamoor_smoke_test.go | 40 ++++++------ test/e2e/evm_test_common.go | 76 +++++++++++++++++------ test/e2e/failover_e2e_test.go | 78 +++++++++++++----------- 10 files changed, 230 insertions(+), 187 deletions(-) diff --git a/execution/evm/test/execution_test.go b/execution/evm/test/execution_test.go index 867b99b77..4b6c6986c 100644 --- a/execution/evm/test/execution_test.go +++ b/execution/evm/test/execution_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + tastoradocker "github.com/celestiaorg/tastora/framework/docker" "github.com/ethereum/go-ethereum/common" ethTypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" @@ -59,8 +60,10 @@ func TestEngineExecution(t *testing.T) { genesisStateRoot := common.HexToHash(GENESIS_STATEROOT) GenesisStateRoot := genesisStateRoot[:] + dockerClient, dockerNetworkID := tastoradocker.Setup(t) + t.Run("Build chain", func(tt *testing.T) { - rethNode := SetupTestRethNode(t) + rethNode := SetupTestRethNode(t, dockerClient, dockerNetworkID) ni, err := rethNode.GetNetworkInfo(context.TODO()) require.NoError(tt, err) @@ -158,7 +161,7 @@ func TestEngineExecution(t *testing.T) { // start new container and try to sync t.Run("Sync chain", func(tt *testing.T) { - rethNode := SetupTestRethNode(t) + rethNode := SetupTestRethNode(t, dockerClient, dockerNetworkID) ni, err := rethNode.GetNetworkInfo(context.TODO()) require.NoError(tt, err) diff --git a/execution/evm/test/test_helpers.go b/execution/evm/test/test_helpers.go index 1aa4ec317..eed7f3950 100644 --- a/execution/evm/test/test_helpers.go +++ b/execution/evm/test/test_helpers.go @@ -9,11 +9,9 @@ import ( mathrand "math/rand" "net/http" "strings" - "sync" "testing" "time" - "github.com/celestiaorg/tastora/framework/docker" "github.com/celestiaorg/tastora/framework/docker/evstack/reth" "github.com/celestiaorg/tastora/framework/types" "github.com/golang-jwt/jwt/v5" @@ -22,12 +20,10 @@ import ( "go.uber.org/zap/zaptest" ) -// Test-scoped Docker client/network mapping to avoid conflicts between tests -var ( - dockerClients = make(map[string]types.TastoraDockerClient) - dockerNetworks = make(map[string]string) - dockerMutex sync.RWMutex -) +// (Docker client/network are now provided by callers) + +// RethNodeOpt allows tests to customize the reth node builder before building. +type RethNodeOpt func(b *reth.NodeBuilder) const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" @@ -40,47 +36,32 @@ func randomString(n int) string { return string(b) } -// getTestScopedDockerSetup returns a Docker client and network ID that are scoped to the specific test. -func getTestScopedDockerSetup(t testing.TB) (types.TastoraDockerClient, string) { - t.Helper() - - testKey := t.Name() - dockerMutex.Lock() - defer dockerMutex.Unlock() - - dockerCli, exists := dockerClients[testKey] - if !exists { - cli, netID := docker.Setup(t) - dockerClients[testKey] = cli - dockerNetworks[testKey] = netID - dockerCli = cli - } - dockerNetID := dockerNetworks[testKey] - - return dockerCli, dockerNetID -} - // SetupTestRethNode creates a single Reth node for testing purposes. -func SetupTestRethNode(t testing.TB) *reth.Node { +func SetupTestRethNode(t testing.TB, client types.TastoraDockerClient, networkID string, opts ...RethNodeOpt) *reth.Node { t.Helper() ctx := context.Background() - dockerCli, dockerNetID := getTestScopedDockerSetup(t) - testName := fmt.Sprintf("%s-%s", t.Name(), randomString(6)) logger := zap.NewNop() if testing.Verbose() { logger = zaptest.NewLogger(t) } - n, err := new(reth.NodeBuilder). + b := new(reth.NodeBuilder). WithTestName(testName). WithLogger(logger). WithImage(reth.DefaultImage()). WithBin("ev-reth"). - WithDockerClient(dockerCli). - WithDockerNetworkID(dockerNetID). - WithGenesis([]byte(reth.DefaultEvolveGenesisJSON())). - Build(ctx) + WithDockerClient(client). + WithDockerNetworkID(networkID). + WithGenesis([]byte(reth.DefaultEvolveGenesisJSON())) + + for _, opt := range opts { + if opt != nil { + opt(b) + } + } + + n, err := b.Build(ctx) t.Cleanup(func() { _ = n.Remove(context.Background()) }) diff --git a/test/e2e/evm_contract_e2e_test.go b/test/e2e/evm_contract_e2e_test.go index 477b0801b..48bc2b2a3 100644 --- a/test/e2e/evm_contract_e2e_test.go +++ b/test/e2e/evm_contract_e2e_test.go @@ -3,15 +3,16 @@ package e2e import ( - "context" - "crypto/ecdsa" - "math/big" - "path/filepath" - "testing" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" + "context" + "crypto/ecdsa" + "math/big" + "path/filepath" + "testing" + "time" + + tastoradocker "github.com/celestiaorg/tastora/framework/docker" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -49,7 +50,7 @@ func TestEvmContractDeploymentAndInteraction(t *testing.T) { workDir := t.TempDir() sequencerHome := filepath.Join(workDir, "evm-sequencer") - client, _, cleanup := setupTestSequencer(t, sequencerHome) + client, _, cleanup := setupTestSequencer(t, sequencerHome) defer cleanup() ctx := t.Context() @@ -241,9 +242,10 @@ func TestEvmContractEvents(t *testing.T) { // setupTestSequencer sets up a single sequencer node for testing. // Returns the ethclient, genesis hash, and a cleanup function. func setupTestSequencer(t testing.TB, homeDir string, extraArgs ...string) (*ethclient.Client, string, func()) { - sut := NewSystemUnderTest(t) + sut := NewSystemUnderTest(t) - genesisHash, seqEthURL := setupSequencerOnlyTest(t, sut, homeDir, extraArgs...) + dcli, netID := tastoradocker.Setup(t) + genesisHash, seqEthURL := setupSequencerOnlyTest(t, sut, homeDir, dcli, netID, extraArgs...) t.Logf("Sequencer started at %s (Genesis: %s)", seqEthURL, genesisHash) client, err := ethclient.Dial(seqEthURL) diff --git a/test/e2e/evm_da_restart_e2e_test.go b/test/e2e/evm_da_restart_e2e_test.go index 80a46b9b7..cd7742328 100644 --- a/test/e2e/evm_da_restart_e2e_test.go +++ b/test/e2e/evm_da_restart_e2e_test.go @@ -24,6 +24,7 @@ import ( "testing" "time" + tastoradocker "github.com/celestiaorg/tastora/framework/docker" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" @@ -56,7 +57,8 @@ func TestEvmDARestartWithPendingBlocksE2E(t *testing.T) { sut := NewSystemUnderTest(t) // Setup sequencer and get genesis hash - genesisHash, seqURL := setupSequencerOnlyTest(t, sut, nodeHome) + dcli, netID := tastoradocker.Setup(t) + genesisHash, seqURL := setupSequencerOnlyTest(t, sut, nodeHome, dcli, netID) t.Logf("Genesis hash: %s", genesisHash) // Connect to EVM diff --git a/test/e2e/evm_force_inclusion_e2e_test.go b/test/e2e/evm_force_inclusion_e2e_test.go index 261944a9e..84a5d9ce1 100644 --- a/test/e2e/evm_force_inclusion_e2e_test.go +++ b/test/e2e/evm_force_inclusion_e2e_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + tastoradocker "github.com/celestiaorg/tastora/framework/docker" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/rs/zerolog" @@ -77,13 +78,15 @@ func setupSequencerWithForceInclusion(t *testing.T, sut *SystemUnderTest, nodeHo t.Helper() // Use common setup (no full node needed initially) - jwtSecret, _, genesisHash, endpoints, _ := setupCommonEVMTest(t, sut, false) + dcli, netID := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dcli, netID) + // Use env fields inline below to reduce local vars // Create passphrase file passphraseFile := createPassphraseFile(t, nodeHome) // Create JWT secret file - jwtSecretFile := createJWTSecretFile(t, nodeHome, jwtSecret) + jwtSecretFile := createJWTSecretFile(t, nodeHome, env.SequencerJWT) // Initialize sequencer node output, err := sut.RunCmd(evmSingleBinaryPath, @@ -102,25 +105,25 @@ func setupSequencerWithForceInclusion(t *testing.T, sut *SystemUnderTest, nodeHo args := []string{ "start", "--evm.jwt-secret-file", jwtSecretFile, - "--evm.genesis-hash", genesisHash, + "--evm.genesis-hash", env.GenesisHash, "--evnode.node.block_time", DefaultBlockTime, "--evnode.node.aggregator=true", "--evnode.signer.passphrase_file", passphraseFile, "--home", nodeHome, "--evnode.da.block_time", DefaultDABlockTime, - "--evnode.da.address", endpoints.GetDAAddress(), + "--evnode.da.address", env.Endpoints.GetDAAddress(), "--evnode.da.namespace", DefaultDANamespace, "--evnode.da.forced_inclusion_namespace", "forced-inc", - "--evnode.rpc.address", endpoints.GetRollkitRPCListen(), - "--evnode.p2p.listen_address", endpoints.GetRollkitP2PAddress(), - "--evm.engine-url", endpoints.GetSequencerEngineURL(), - "--evm.eth-url", endpoints.GetSequencerEthURL(), + "--evnode.rpc.address", env.Endpoints.GetRollkitRPCListen(), + "--evnode.p2p.listen_address", env.Endpoints.GetRollkitP2PAddress(), + "--evm.engine-url", env.Endpoints.GetSequencerEngineURL(), + "--evm.eth-url", env.Endpoints.GetSequencerEthURL(), "--force-inclusion-server", fiAddr, } sut.ExecCmd(evmSingleBinaryPath, args...) - sut.AwaitNodeUp(t, endpoints.GetRollkitRPCAddress(), NodeStartupTimeout) + sut.AwaitNodeUp(t, env.Endpoints.GetRollkitRPCAddress(), NodeStartupTimeout) - return genesisHash, endpoints.GetSequencerEthURL() + return env.GenesisHash, env.Endpoints.GetSequencerEthURL() } func TestEvmSequencerForceInclusionE2E(t *testing.T) { @@ -192,10 +195,11 @@ func TestEvmFullNodeForceInclusionE2E(t *testing.T) { // --- Start Sequencer Setup --- // We manually setup sequencer here because we need the force inclusion flag, // and we need to capture variables for full node setup. - jwtSecret, fullNodeJwtSecret, genesisHash, endpoints, _ := setupCommonEVMTest(t, sut, true) + dcli2, netID2 := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dcli2, netID2, WithFullNode()) passphraseFile := createPassphraseFile(t, sequencerHome) - jwtSecretFile := createJWTSecretFile(t, sequencerHome, jwtSecret) + jwtSecretFile := createJWTSecretFile(t, sequencerHome, env.SequencerJWT) output, err := sut.RunCmd(evmSingleBinaryPath, "init", @@ -212,38 +216,38 @@ func TestEvmFullNodeForceInclusionE2E(t *testing.T) { seqArgs := []string{ "start", "--evm.jwt-secret-file", jwtSecretFile, - "--evm.genesis-hash", genesisHash, + "--evm.genesis-hash", env.GenesisHash, "--evnode.node.block_time", DefaultBlockTime, "--evnode.node.aggregator=true", "--evnode.signer.passphrase_file", passphraseFile, "--home", sequencerHome, "--evnode.da.block_time", DefaultDABlockTime, - "--evnode.da.address", endpoints.GetDAAddress(), + "--evnode.da.address", env.Endpoints.GetDAAddress(), "--evnode.da.namespace", DefaultDANamespace, "--evnode.da.forced_inclusion_namespace", "forced-inc", - "--evnode.rpc.address", endpoints.GetRollkitRPCListen(), - "--evnode.p2p.listen_address", endpoints.GetRollkitP2PAddress(), - "--evm.engine-url", endpoints.GetSequencerEngineURL(), - "--evm.eth-url", endpoints.GetSequencerEthURL(), + "--evnode.rpc.address", env.Endpoints.GetRollkitRPCListen(), + "--evnode.p2p.listen_address", env.Endpoints.GetRollkitP2PAddress(), + "--evm.engine-url", env.Endpoints.GetSequencerEngineURL(), + "--evm.eth-url", env.Endpoints.GetSequencerEthURL(), "--force-inclusion-server", fiAddr, } sut.ExecCmd(evmSingleBinaryPath, seqArgs...) - sut.AwaitNodeUp(t, endpoints.GetRollkitRPCAddress(), NodeStartupTimeout) + sut.AwaitNodeUp(t, env.Endpoints.GetRollkitRPCAddress(), NodeStartupTimeout) t.Log("Sequencer is up with force inclusion enabled") // --- End Sequencer Setup --- // --- Start Full Node Setup --- // Reuse setupFullNode helper which handles genesis copying and node startup - setupFullNode(t, sut, fullNodeHome, sequencerHome, fullNodeJwtSecret, genesisHash, endpoints.GetRollkitP2PAddress(), endpoints) + setupFullNode(t, sut, fullNodeHome, sequencerHome, env.FullNodeJWT, env.GenesisHash, env.Endpoints.GetRollkitP2PAddress(), env.Endpoints) t.Log("Full node is up") // --- End Full Node Setup --- // Connect to clients - seqClient, err := ethclient.Dial(endpoints.GetSequencerEthURL()) + seqClient, err := ethclient.Dial(env.Endpoints.GetSequencerEthURL()) require.NoError(t, err) defer seqClient.Close() - fnClient, err := ethclient.Dial(endpoints.GetFullNodeEthURL()) + fnClient, err := ethclient.Dial(env.Endpoints.GetFullNodeEthURL()) require.NoError(t, err) defer fnClient.Close() @@ -284,10 +288,11 @@ func setupMaliciousSequencer(t *testing.T, sut *SystemUnderTest, nodeHome string t.Helper() // Use common setup with full node support - jwtSecret, fullNodeJwtSecret, genesisHash, endpoints, _ := setupCommonEVMTest(t, sut, true) + env := setupCommonEVMEnv(t, sut, WithFullNode()) + // Use env fields inline below to reduce local vars passphraseFile := createPassphraseFile(t, nodeHome) - jwtSecretFile := createJWTSecretFile(t, nodeHome, jwtSecret) + jwtSecretFile := createJWTSecretFile(t, nodeHome, env.SequencerJWT) output, err := sut.RunCmd(evmSingleBinaryPath, "init", @@ -303,25 +308,25 @@ func setupMaliciousSequencer(t *testing.T, sut *SystemUnderTest, nodeHome string seqArgs := []string{ "start", "--evm.jwt-secret-file", jwtSecretFile, - "--evm.genesis-hash", genesisHash, + "--evm.genesis-hash", env.GenesisHash, "--evnode.node.block_time", DefaultBlockTime, "--evnode.node.aggregator=true", "--evnode.signer.passphrase_file", passphraseFile, "--home", nodeHome, "--evnode.da.block_time", DefaultDABlockTime, - "--evnode.da.address", endpoints.GetDAAddress(), + "--evnode.da.address", env.Endpoints.GetDAAddress(), "--evnode.da.namespace", DefaultDANamespace, // CRITICAL: Set sequencer to listen to WRONG namespace - it won't see forced txs "--evnode.da.forced_inclusion_namespace", "wrong-namespace", - "--evnode.rpc.address", endpoints.GetRollkitRPCListen(), - "--evnode.p2p.listen_address", endpoints.GetRollkitP2PAddress(), - "--evm.engine-url", endpoints.GetSequencerEngineURL(), - "--evm.eth-url", endpoints.GetSequencerEthURL(), + "--evnode.rpc.address", env.Endpoints.GetRollkitRPCListen(), + "--evnode.p2p.listen_address", env.Endpoints.GetRollkitP2PAddress(), + "--evm.engine-url", env.Endpoints.GetSequencerEngineURL(), + "--evm.eth-url", env.Endpoints.GetSequencerEthURL(), } sut.ExecCmd(evmSingleBinaryPath, seqArgs...) - sut.AwaitNodeUp(t, endpoints.GetRollkitRPCAddress(), NodeStartupTimeout) + sut.AwaitNodeUp(t, env.Endpoints.GetRollkitRPCAddress(), NodeStartupTimeout) - return genesisHash, fullNodeJwtSecret, endpoints + return env.GenesisHash, env.FullNodeJWT, env.Endpoints } // setupFullNodeWithForceInclusionCheck sets up a full node that WILL verify forced inclusion txs diff --git a/test/e2e/evm_full_node_e2e_test.go b/test/e2e/evm_full_node_e2e_test.go index 32577fa82..63ee38975 100644 --- a/test/e2e/evm_full_node_e2e_test.go +++ b/test/e2e/evm_full_node_e2e_test.go @@ -29,6 +29,7 @@ import ( "testing" "time" + tastoradocker "github.com/celestiaorg/tastora/framework/docker" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" @@ -213,24 +214,25 @@ func setupSequencerWithFullNode(t *testing.T, sut *SystemUnderTest, sequencerHom t.Helper() // Common setup for both sequencer and full node - jwtSecret, fullNodeJwtSecret, genesisHash, endpoints, _ := setupCommonEVMTest(t, sut, true) + dcli, netID := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dcli, netID, WithFullNode()) // Setup sequencer - setupSequencerNode(t, sut, sequencerHome, jwtSecret, genesisHash, endpoints) + setupSequencerNode(t, sut, sequencerHome, env.SequencerJWT, env.GenesisHash, env.Endpoints) t.Log("Sequencer node is up") // Get P2P address and setup full node - sequencerP2PAddress := getNodeP2PAddress(t, sut, sequencerHome, endpoints.RollkitRPCPort) + sequencerP2PAddress := getNodeP2PAddress(t, sut, sequencerHome, env.Endpoints.RollkitRPCPort) t.Logf("Sequencer P2P address: %s", sequencerP2PAddress) - setupFullNode(t, sut, fullNodeHome, sequencerHome, fullNodeJwtSecret, genesisHash, sequencerP2PAddress, endpoints) + setupFullNode(t, sut, fullNodeHome, sequencerHome, env.FullNodeJWT, env.GenesisHash, sequencerP2PAddress, env.Endpoints) t.Log("Full node is up") // Connect to both EVM instances - sequencerClient, err := ethclient.Dial(endpoints.GetSequencerEthURL()) + sequencerClient, err := ethclient.Dial(env.Endpoints.GetSequencerEthURL()) require.NoError(t, err, "Should be able to connect to sequencer EVM") - fullNodeClient, err := ethclient.Dial(endpoints.GetFullNodeEthURL()) + fullNodeClient, err := ethclient.Dial(env.Endpoints.GetFullNodeEthURL()) require.NoError(t, err, "Should be able to connect to full node EVM") // Wait for P2P connections to establish @@ -254,7 +256,7 @@ func setupSequencerWithFullNode(t *testing.T, sut *SystemUnderTest, sequencerHom }, DefaultTestTimeout, 250*time.Millisecond, "P2P connections should be established") t.Log("P2P connections established") - return sequencerClient, fullNodeClient, endpoints + return sequencerClient, fullNodeClient, env.Endpoints } // TestEvmSequencerWithFullNodeE2E tests the full node synchronization functionality @@ -645,27 +647,28 @@ func setupSequencerWithFullNodeLazy(t *testing.T, sut *SystemUnderTest, sequence t.Helper() // Common setup for both sequencer and full node - jwtSecret, fullNodeJwtSecret, genesisHash, endpoints, _ := setupCommonEVMTest(t, sut, true) + dcli, netID := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dcli, netID, WithFullNode()) t.Logf("Generated test endpoints - Rollkit RPC: %s, P2P: %s, Full Node RPC: %s, P2P: %s, DA Port: %s", - endpoints.RollkitRPCPort, endpoints.RollkitP2PPort, endpoints.FullNodeRPCPort, endpoints.FullNodeP2PPort, endpoints.DAPort) + env.Endpoints.RollkitRPCPort, env.Endpoints.RollkitP2PPort, env.Endpoints.FullNodeRPCPort, env.Endpoints.FullNodeP2PPort, env.Endpoints.DAPort) // Setup sequencer in lazy mode - setupSequencerNodeLazy(t, sut, sequencerHome, jwtSecret, genesisHash, endpoints) + setupSequencerNodeLazy(t, sut, sequencerHome, env.SequencerJWT, env.GenesisHash, env.Endpoints) t.Log("Sequencer node (lazy mode) is up") // Get P2P address and setup full node - sequencerP2PAddress := getNodeP2PAddress(t, sut, sequencerHome, endpoints.RollkitRPCPort) + sequencerP2PAddress := getNodeP2PAddress(t, sut, sequencerHome, env.Endpoints.RollkitRPCPort) t.Logf("Sequencer P2P address: %s", sequencerP2PAddress) - setupFullNode(t, sut, fullNodeHome, sequencerHome, fullNodeJwtSecret, genesisHash, sequencerP2PAddress, endpoints) + setupFullNode(t, sut, fullNodeHome, sequencerHome, env.FullNodeJWT, env.GenesisHash, sequencerP2PAddress, env.Endpoints) t.Log("Full node is up") // Connect to both EVM instances - sequencerClient, err := ethclient.Dial(endpoints.GetSequencerEthURL()) + sequencerClient, err := ethclient.Dial(env.Endpoints.GetSequencerEthURL()) require.NoError(t, err, "Should be able to connect to sequencer EVM") - fullNodeClient, err := ethclient.Dial(endpoints.GetFullNodeEthURL()) + fullNodeClient, err := ethclient.Dial(env.Endpoints.GetFullNodeEthURL()) require.NoError(t, err, "Should be able to connect to full node EVM") // Wait for P2P connections to establish @@ -693,7 +696,7 @@ func setupSequencerWithFullNodeLazy(t *testing.T, sut *SystemUnderTest, sequence }, DefaultTestTimeout, 250*time.Millisecond, "P2P connections should be established") t.Log("P2P connections established") - return sequencerClient, fullNodeClient, endpoints + return sequencerClient, fullNodeClient, env.Endpoints } // TestEvmLazyModeSequencerE2E tests the lazy mode functionality where blocks are only @@ -925,7 +928,7 @@ func TestEvmLazyModeSequencerE2E(t *testing.T) { // // This function ensures both nodes are properly restarted and P2P connections are re-established. // The DA restart is handled by the shared restartDAAndSequencer/restartDAAndSequencerLazy functions. -func restartSequencerAndFullNode(t *testing.T, sut *SystemUnderTest, sequencerHome, fullNodeHome, jwtSecret, fullNodeJwtSecret, genesisHash string, useLazyMode bool, endpoints *TestEndpoints) { +func restartSequencerAndFullNode(t *testing.T, sut *SystemUnderTest, sequencerHome, fullNodeHome, jwtSecret, genesisHash string, useLazyMode bool, endpoints *TestEndpoints) { t.Helper() // Restart DA and sequencer first (following the pattern from TestEvmSequencerRestartRecoveryE2E) @@ -1038,31 +1041,32 @@ func testSequencerFullNodeRestart(t *testing.T, initialLazyMode, restartLazyMode t.Logf("Phase 1: Setting up sequencer (initial_lazy=%t) and full node with P2P connections...", initialLazyMode) t.Logf("Test mode: initial_lazy=%t, restart_lazy=%t", initialLazyMode, restartLazyMode) - // Get JWT secrets and setup common components first - jwtSecret, fullNodeJwtSecret, genesisHash, endpoints, _ := setupCommonEVMTest(t, sut, true) + // Get Docker client/network and common environment + dcli, netID := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dcli, netID, WithFullNode()) // Setup sequencer based on initial mode if initialLazyMode { - setupSequencerNodeLazy(t, sut, sequencerHome, jwtSecret, genesisHash, endpoints) + setupSequencerNodeLazy(t, sut, sequencerHome, env.SequencerJWT, env.GenesisHash, env.Endpoints) t.Log("Sequencer node (lazy mode) is up") } else { - setupSequencerNode(t, sut, sequencerHome, jwtSecret, genesisHash, endpoints) + setupSequencerNode(t, sut, sequencerHome, env.SequencerJWT, env.GenesisHash, env.Endpoints) t.Log("Sequencer node is up") } // Get P2P address and setup full node - sequencerP2PAddress := getNodeP2PAddress(t, sut, sequencerHome, endpoints.RollkitRPCPort) + sequencerP2PAddress := getNodeP2PAddress(t, sut, sequencerHome, env.Endpoints.RollkitRPCPort) t.Logf("Sequencer P2P address: %s", sequencerP2PAddress) - setupFullNode(t, sut, fullNodeHome, sequencerHome, fullNodeJwtSecret, genesisHash, sequencerP2PAddress, endpoints) + setupFullNode(t, sut, fullNodeHome, sequencerHome, env.FullNodeJWT, env.GenesisHash, sequencerP2PAddress, env.Endpoints) t.Log("Full node is up") // Connect to both EVM instances - sequencerClient, err := ethclient.Dial(endpoints.GetSequencerEthURL()) + sequencerClient, err := ethclient.Dial(env.Endpoints.GetSequencerEthURL()) require.NoError(t, err, "Should be able to connect to sequencer EVM") defer sequencerClient.Close() - fullNodeClient, err := ethclient.Dial(endpoints.GetFullNodeEthURL()) + fullNodeClient, err := ethclient.Dial(env.Endpoints.GetFullNodeEthURL()) require.NoError(t, err, "Should be able to connect to full node EVM") defer fullNodeClient.Close() @@ -1169,14 +1173,14 @@ func testSequencerFullNodeRestart(t *testing.T, initialLazyMode, restartLazyMode t.Log("Phase 3: Restarting both sequencer and full node...") // Restart both nodes with specified restart mode - restartSequencerAndFullNode(t, sut, sequencerHome, fullNodeHome, jwtSecret, fullNodeJwtSecret, genesisHash, restartLazyMode, endpoints) + restartSequencerAndFullNode(t, sut, sequencerHome, fullNodeHome, env.SequencerJWT, env.GenesisHash, restartLazyMode, env.Endpoints) // Reconnect to both EVM instances (connections lost during restart) - sequencerClient, err = ethclient.Dial(endpoints.GetSequencerEthURL()) + sequencerClient, err = ethclient.Dial(env.Endpoints.GetSequencerEthURL()) require.NoError(t, err, "Should be able to reconnect to sequencer EVM") defer sequencerClient.Close() - fullNodeClient, err = ethclient.Dial(endpoints.GetFullNodeEthURL()) + fullNodeClient, err = ethclient.Dial(env.Endpoints.GetFullNodeEthURL()) require.NoError(t, err, "Should be able to reconnect to full node EVM") defer fullNodeClient.Close() @@ -1390,7 +1394,7 @@ func testSequencerFullNodeRestart(t *testing.T, initialLazyMode, restartLazyMode } for _, blockHeight := range blocksToCheck { - verifyStateRootsMatch(t, endpoints.GetSequencerEthURL(), endpoints.GetFullNodeEthURL(), blockHeight) + verifyStateRootsMatch(t, env.Endpoints.GetSequencerEthURL(), env.Endpoints.GetFullNodeEthURL(), blockHeight) } // === PHASE 7: Final transaction verification === diff --git a/test/e2e/evm_sequencer_e2e_test.go b/test/e2e/evm_sequencer_e2e_test.go index 5ef3584e7..6bda37fbf 100644 --- a/test/e2e/evm_sequencer_e2e_test.go +++ b/test/e2e/evm_sequencer_e2e_test.go @@ -24,6 +24,7 @@ import ( "testing" "time" + tastoradocker "github.com/celestiaorg/tastora/framework/docker" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" @@ -49,7 +50,8 @@ func TestEvmSequencerComprehensiveE2E(t *testing.T) { sut := NewSystemUnderTest(t) // Setup sequencer once for all test phases - genesisHash, seqURL := setupSequencerOnlyTest(t, sut, nodeHome) + dockerClient, networkID := tastoradocker.Setup(t) + genesisHash, seqURL := setupSequencerOnlyTest(t, sut, nodeHome, dockerClient, networkID) t.Logf("Genesis hash: %s", genesisHash) // Connect to EVM once for all phases diff --git a/test/e2e/evm_spamoor_smoke_test.go b/test/e2e/evm_spamoor_smoke_test.go index 0c65fc3bc..80a2b9331 100644 --- a/test/e2e/evm_spamoor_smoke_test.go +++ b/test/e2e/evm_spamoor_smoke_test.go @@ -3,16 +3,17 @@ package e2e import ( - "context" - "fmt" - "net/http" - "path/filepath" - "testing" - "time" - - spamoor "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" - dto "github.com/prometheus/client_model/go" - "github.com/stretchr/testify/require" + "context" + "fmt" + "net/http" + "path/filepath" + "testing" + "time" + + tastoradocker "github.com/celestiaorg/tastora/framework/docker" + spamoor "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" + dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/require" ) // TestSpamoorSmoke spins up reth + sequencer and a Spamoor node, starts a few @@ -21,9 +22,10 @@ func TestSpamoorSmoke(t *testing.T) { t.Parallel() sut := NewSystemUnderTest(t) - // Bring up reth + local DA and start sequencer with default settings. - seqJWT, _, genesisHash, endpoints, rethNode := setupCommonEVMTest(t, sut, false) - sequencerHome := filepath.Join(t.TempDir(), "sequencer") + // Bring up reth + local DA and start sequencer with default settings. + dcli, netID := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dcli, netID) + sequencerHome := filepath.Join(t.TempDir(), "sequencer") // In-process OTLP/HTTP collector to capture ev-node spans. collector := newOTLPCollector(t) @@ -32,7 +34,7 @@ func TestSpamoorSmoke(t *testing.T) { }) // Start sequencer with tracing to our collector. - setupSequencerNode(t, sut, sequencerHome, seqJWT, genesisHash, endpoints, + setupSequencerNode(t, sut, sequencerHome, env.SequencerJWT, env.GenesisHash, env.Endpoints, "--evnode.instrumentation.tracing=true", "--evnode.instrumentation.tracing_endpoint", collector.endpoint(), "--evnode.instrumentation.tracing_sample_rate", "1.0", @@ -41,15 +43,15 @@ func TestSpamoorSmoke(t *testing.T) { t.Log("Sequencer node is up") // Start Spamoor within the same Docker network, targeting reth internal RPC. - ni, err := rethNode.GetNetworkInfo(context.Background()) + ni, err := env.RethNode.GetNetworkInfo(context.Background()) require.NoError(t, err, "failed to get network info") internalRPC := "http://" + ni.Internal.RPCAddress() - spBuilder := spamoor.NewNodeBuilder(t.Name()). - WithDockerClient(rethNode.DockerClient). - WithDockerNetworkID(rethNode.NetworkID). - WithLogger(rethNode.Logger). + spBuilder := spamoor.NewNodeBuilder(t.Name()). + WithDockerClient(env.RethNode.DockerClient). + WithDockerNetworkID(env.RethNode.NetworkID). + WithLogger(env.RethNode.Logger). WithRPCHosts(internalRPC). WithPrivateKey(TestPrivateKey) diff --git a/test/e2e/evm_test_common.go b/test/e2e/evm_test_common.go index 80aa21721..e7b4cd1bf 100644 --- a/test/e2e/evm_test_common.go +++ b/test/e2e/evm_test_common.go @@ -29,6 +29,7 @@ import ( "time" "github.com/celestiaorg/tastora/framework/docker/evstack/reth" + tastoratypes "github.com/celestiaorg/tastora/framework/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" @@ -505,17 +506,34 @@ func submitTransactionAndGetBlockNumber(t testing.TB, sequencerClients ...*ethcl return tx.Hash(), txBlockNumber } -// setupCommonEVMTest performs common setup for EVM tests including DA and EVM engine initialization. -// This helper reduces code duplication across multiple test functions. -// -// Parameters: -// - needsFullNode: whether to set up a full node EVM engine in addition to sequencer -// - daPort: optional DA port to use (if empty, uses default) -// -// Returns: jwtSecret, fullNodeJwtSecret (empty if needsFullNode=false), genesisHash -func setupCommonEVMTest(t testing.TB, sut *SystemUnderTest, needsFullNode bool) (string, string, string, *TestEndpoints, *reth.Node) { +// SetupOpt customizes the common EVM test setup without adding positional params. +type SetupOpt func(*setupConfig) + +type setupConfig struct { + needsFullNode bool + rethOpts []evmtest.RethNodeOpt +} + +// WithFullNode enables bringing up an additional full node in the test setup. +func WithFullNode() SetupOpt { + return func(c *setupConfig) { + c.needsFullNode = true + } +} + +// setupCommonEVMEnv creates and initializes ev-reth instances, while also initalizing the local ev-node instance +// managed by sut. If a full node is also required, we can use the WithFullNode() additional option. +func setupCommonEVMEnv(t testing.TB, sut *SystemUnderTest, client tastoratypes.TastoraDockerClient, networkID string, opts ...SetupOpt) *EVMEnv { t.Helper() + // Configuration via functional options + cfg := setupConfig{needsFullNode: false} + for _, opt := range opts { + if opt != nil { + opt(&cfg) + } + } + // Reset global nonce for each test to ensure clean state globalNonce = 0 @@ -531,7 +549,11 @@ func setupCommonEVMTest(t testing.TB, sut *SystemUnderTest, needsFullNode bool) sut.ExecCmd(localDABinary, "-port", dynEndpoints.DAPort) t.Logf("Started local DA on port %s", dynEndpoints.DAPort) - rethNode := evmtest.SetupTestRethNode(t) + require.NotNil(t, client, "docker client is required") + require.NotEmpty(t, networkID, "docker networkID is required") + dcli := client + netID := networkID + rethNode := evmtest.SetupTestRethNode(t, dcli, netID, cfg.rethOpts...) networkInfo, err := rethNode.GetNetworkInfo(context.Background()) require.NoError(t, err, "failed to get reth network info") @@ -540,8 +562,8 @@ func setupCommonEVMTest(t testing.TB, sut *SystemUnderTest, needsFullNode bool) var fnJWT string var rethFn *reth.Node - if needsFullNode { - rethFn = evmtest.SetupTestRethNode(t) + if cfg.needsFullNode { + rethFn = evmtest.SetupTestRethNode(t, dcli, netID, cfg.rethOpts...) fnJWT = rethFn.JWTSecretHex() } @@ -552,14 +574,31 @@ func setupCommonEVMTest(t testing.TB, sut *SystemUnderTest, needsFullNode bool) // Populate endpoints with both dynamic rollkit ports and dynamic engine ports dynEndpoints.SequencerEthPort = networkInfo.External.Ports.RPC dynEndpoints.SequencerEnginePort = networkInfo.External.Ports.Engine - if needsFullNode { + if cfg.needsFullNode { fnInfo, err := rethFn.GetNetworkInfo(context.Background()) require.NoError(t, err, "failed to get full node reth network info") dynEndpoints.FullNodeEthPort = fnInfo.External.Ports.RPC dynEndpoints.FullNodeEnginePort = fnInfo.External.Ports.Engine } - return seqJWT, fnJWT, genesisHash, dynEndpoints, rethNode + return &EVMEnv{ + SequencerJWT: seqJWT, + FullNodeJWT: fnJWT, + GenesisHash: genesisHash, + Endpoints: dynEndpoints, + RethNode: rethNode, + } +} + +// EVMEnv is a cohesive result for common EVM test setup. +// It consolidates the return values into a single struct +// to improve readability and extensibility at call sites. +type EVMEnv struct { + SequencerJWT string + FullNodeJWT string + GenesisHash string + Endpoints *TestEndpoints + RethNode *reth.Node } // checkBlockInfoAt retrieves block information at a specific height including state root. @@ -614,17 +653,16 @@ func checkBlockInfoAt(t testing.TB, ethURL string, blockHeight *uint64) (common. // - nodeHome: Directory path for sequencer node data // // Returns: genesisHash for the sequencer -func setupSequencerOnlyTest(t testing.TB, sut *SystemUnderTest, nodeHome string, extraArgs ...string) (string, string) { +func setupSequencerOnlyTest(t testing.TB, sut *SystemUnderTest, nodeHome string, client tastoratypes.TastoraDockerClient, networkID string, extraArgs ...string) (string, string) { t.Helper() // Use common setup (no full node needed) - jwtSecret, _, genesisHash, endpoints, _ := setupCommonEVMTest(t, sut, false) - + env := setupCommonEVMEnv(t, sut, client, networkID) // Initialize and start sequencer node - setupSequencerNode(t, sut, nodeHome, jwtSecret, genesisHash, endpoints, extraArgs...) + setupSequencerNode(t, sut, nodeHome, env.SequencerJWT, env.GenesisHash, env.Endpoints, extraArgs...) t.Log("Sequencer node is up") - return genesisHash, endpoints.GetSequencerEthURL() + return env.GenesisHash, env.Endpoints.GetSequencerEthURL() } // restartDAAndSequencer restarts both the local DA and sequencer node. diff --git a/test/e2e/failover_e2e_test.go b/test/e2e/failover_e2e_test.go index 8d1f0637c..85fdc2533 100644 --- a/test/e2e/failover_e2e_test.go +++ b/test/e2e/failover_e2e_test.go @@ -21,6 +21,7 @@ import ( "time" libshare "github.com/celestiaorg/go-square/v3/share" + tastoradocker "github.com/celestiaorg/tastora/framework/docker" "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -54,8 +55,10 @@ func TestLeaseFailoverE2E(t *testing.T) { workDir := t.TempDir() // Get JWT secrets and setup common components first - jwtSecret, fullNodeJwtSecret, genesisHash, testEndpoints, _ := setupCommonEVMTest(t, sut, true) - rethFn := evmtest.SetupTestRethNode(t) + dockerClient, networkID := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dockerClient, networkID, WithFullNode()) + // Use a fresh reth node on the same Docker network as used by the env setup. + rethFn := evmtest.SetupTestRethNode(t, dockerClient, networkID) jwtSecret3 := rethFn.JWTSecretHex() fnInfo, err := rethFn.GetNetworkInfo(context.Background()) require.NoError(t, err, "failed to get full node reth network info") @@ -79,17 +82,17 @@ func TestLeaseFailoverE2E(t *testing.T) { clusterNodes := &raftClusterNodes{ nodes: make(map[string]*nodeDetails), } - node1P2PAddr := testEndpoints.GetRollkitP2PAddress() - node2P2PAddr := testEndpoints.GetFullNodeP2PAddress() + node1P2PAddr := env.Endpoints.GetRollkitP2PAddress() + node2P2PAddr := env.Endpoints.GetFullNodeP2PAddress() node3P2PAddr := fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", mustGetAvailablePort(t)) // Start node1 (bootstrap mode) go func() { p2pPeers := node2P2PAddr + "," + node3P2PAddr - proc := setupRaftSequencerNode(t, sut, workDir, "node1", node1RaftAddr, jwtSecret, genesisHash, testEndpoints.GetDAAddress(), - bootstrapDir, raftCluster, p2pPeers, testEndpoints.GetRollkitRPCListen(), testEndpoints.GetRollkitP2PAddress(), - testEndpoints.GetSequencerEngineURL(), testEndpoints.GetSequencerEthURL(), true, passphraseFile) - clusterNodes.Set("node1", testEndpoints.GetRollkitRPCAddress(), proc, testEndpoints.GetSequencerEthURL(), node1RaftAddr, testEndpoints.GetRollkitP2PAddress(), testEndpoints.GetSequencerEngineURL(), testEndpoints.GetSequencerEthURL()) + proc := setupRaftSequencerNode(t, sut, workDir, "node1", node1RaftAddr, env.SequencerJWT, env.GenesisHash, env.Endpoints.GetDAAddress(), + bootstrapDir, raftCluster, p2pPeers, env.Endpoints.GetRollkitRPCListen(), env.Endpoints.GetRollkitP2PAddress(), + env.Endpoints.GetSequencerEngineURL(), env.Endpoints.GetSequencerEthURL(), true, passphraseFile) + clusterNodes.Set("node1", env.Endpoints.GetRollkitRPCAddress(), proc, env.Endpoints.GetSequencerEthURL(), node1RaftAddr, env.Endpoints.GetRollkitP2PAddress(), env.Endpoints.GetSequencerEngineURL(), env.Endpoints.GetSequencerEthURL()) t.Log("Node1 is up") }() @@ -97,8 +100,8 @@ func TestLeaseFailoverE2E(t *testing.T) { go func() { t.Log("Starting Node2") p2pPeers := node1P2PAddr + "," + node3P2PAddr - proc := setupRaftSequencerNode(t, sut, workDir, "node2", node2RaftAddr, fullNodeJwtSecret, genesisHash, testEndpoints.GetDAAddress(), bootstrapDir, raftCluster, p2pPeers, testEndpoints.GetFullNodeRPCListen(), testEndpoints.GetFullNodeP2PAddress(), testEndpoints.GetFullNodeEngineURL(), testEndpoints.GetFullNodeEthURL(), true, passphraseFile) - clusterNodes.Set("node2", testEndpoints.GetFullNodeRPCAddress(), proc, testEndpoints.GetFullNodeEthURL(), node2RaftAddr, testEndpoints.GetFullNodeP2PAddress(), testEndpoints.GetFullNodeEngineURL(), testEndpoints.GetFullNodeEthURL()) + proc := setupRaftSequencerNode(t, sut, workDir, "node2", node2RaftAddr, env.FullNodeJWT, env.GenesisHash, env.Endpoints.GetDAAddress(), bootstrapDir, raftCluster, p2pPeers, env.Endpoints.GetFullNodeRPCListen(), env.Endpoints.GetFullNodeP2PAddress(), env.Endpoints.GetFullNodeEngineURL(), env.Endpoints.GetFullNodeEthURL(), true, passphraseFile) + clusterNodes.Set("node2", env.Endpoints.GetFullNodeRPCAddress(), proc, env.Endpoints.GetFullNodeEthURL(), node2RaftAddr, env.Endpoints.GetFullNodeP2PAddress(), env.Endpoints.GetFullNodeEngineURL(), env.Endpoints.GetFullNodeEthURL()) t.Log("Node2 is up") }() @@ -109,7 +112,7 @@ func TestLeaseFailoverE2E(t *testing.T) { p2pPeers := node1P2PAddr + "," + node2P2PAddr node3RPCListen := fmt.Sprintf("127.0.0.1:%d", mustGetAvailablePort(t)) ethEngineURL := fmt.Sprintf("http://127.0.0.1:%s", fullNode3EnginePort) - proc := setupRaftSequencerNode(t, sut, workDir, "node3", node3RaftAddr, jwtSecret3, genesisHash, testEndpoints.GetDAAddress(), bootstrapDir, raftCluster, p2pPeers, node3RPCListen, node3P2PAddr, ethEngineURL, node3EthAddr, true, passphraseFile) + proc := setupRaftSequencerNode(t, sut, workDir, "node3", node3RaftAddr, jwtSecret3, env.GenesisHash, env.Endpoints.GetDAAddress(), bootstrapDir, raftCluster, p2pPeers, node3RPCListen, node3P2PAddr, ethEngineURL, node3EthAddr, true, passphraseFile) clusterNodes.Set("node3", "http://"+node3RPCListen, proc, node3EthAddr, node3RaftAddr, node3P2PAddr, ethEngineURL, node3EthAddr) t.Log("Node3 is up") }() @@ -138,7 +141,7 @@ func TestLeaseFailoverE2E(t *testing.T) { _ = clusterNodes.Details(oldLeader).Kill() const daStartHeight = 1 - lastDABlockOldLeader := queryLastDAHeight(t, daStartHeight, jwtSecret, testEndpoints.GetDAAddress()) + lastDABlockOldLeader := queryLastDAHeight(t, daStartHeight, env.SequencerJWT, env.Endpoints.GetDAAddress()) t.Log("+++ Last DA block by old leader: ", lastDABlockOldLeader) leaderElectionStart := time.Now() @@ -157,7 +160,7 @@ func TestLeaseFailoverE2E(t *testing.T) { // Verify DA progress var lastDABlockNewLeader uint64 require.Eventually(t, func() bool { - lastDABlockNewLeader = queryLastDAHeight(t, lastDABlockOldLeader, jwtSecret, testEndpoints.GetDAAddress()) + lastDABlockNewLeader = queryLastDAHeight(t, lastDABlockOldLeader, env.SequencerJWT, env.Endpoints.GetDAAddress()) return lastDABlockNewLeader > lastDABlockOldLeader }, 2*must(time.ParseDuration(DefaultDABlockTime)), 100*time.Millisecond) t.Logf("+++ Last DA block by new leader: %d\n", lastDABlockNewLeader) @@ -170,7 +173,7 @@ func TestLeaseFailoverE2E(t *testing.T) { } } oldDetails := clusterNodes.Details(oldLeader) - restartedNodeProcess := setupRaftSequencerNode(t, sut, workDir, oldLeader, oldDetails.raftAddr, jwtSecret, genesisHash, testEndpoints.GetDAAddress(), "", raftCluster, clusterNodes.Details(newLeader).p2pAddr, oldDetails.rpcAddr, oldDetails.p2pAddr, oldDetails.engineURL, oldDetails.ethAddr, false, passphraseFile) + restartedNodeProcess := setupRaftSequencerNode(t, sut, workDir, oldLeader, oldDetails.raftAddr, env.SequencerJWT, env.GenesisHash, env.Endpoints.GetDAAddress(), "", raftCluster, clusterNodes.Details(newLeader).p2pAddr, oldDetails.rpcAddr, oldDetails.p2pAddr, oldDetails.engineURL, oldDetails.ethAddr, false, passphraseFile) t.Log("Restarted old leader to sync with cluster: " + oldLeader) if IsNodeUp(t, oldDetails.rpcAddr, NodeStartupTimeout) { @@ -217,7 +220,7 @@ func TestLeaseFailoverE2E(t *testing.T) { return err == nil }, time.Second, 100*time.Millisecond) - lastDABlockNewLeader = queryLastDAHeight(t, lastDABlockNewLeader, jwtSecret, testEndpoints.GetDAAddress()) + lastDABlockNewLeader = queryLastDAHeight(t, lastDABlockNewLeader, env.SequencerJWT, env.Endpoints.GetDAAddress()) genesisHeight := state.InitialHeight verifyNoDoubleSigning(t, clusterNodes, genesisHeight, state.LastBlockHeight) @@ -225,12 +228,12 @@ func TestLeaseFailoverE2E(t *testing.T) { // wait for the next DA block to ensure all blocks are propagated require.Eventually(t, func() bool { before := lastDABlockNewLeader - lastDABlockNewLeader = queryLastDAHeight(t, lastDABlockNewLeader, jwtSecret, testEndpoints.GetDAAddress()) + lastDABlockNewLeader = queryLastDAHeight(t, lastDABlockNewLeader, env.SequencerJWT, env.Endpoints.GetDAAddress()) return before < lastDABlockNewLeader }, 2*must(time.ParseDuration(DefaultDABlockTime)), 100*time.Millisecond) t.Log("+++ Verifying no DA gaps...") - verifyDABlocks(t, daStartHeight, lastDABlockNewLeader, jwtSecret, testEndpoints.GetDAAddress(), genesisHeight, state.LastBlockHeight) + verifyDABlocks(t, daStartHeight, lastDABlockNewLeader, env.SequencerJWT, env.Endpoints.GetDAAddress(), genesisHeight, state.LastBlockHeight) // Cleanup processes clusterNodes.killAll() @@ -252,9 +255,10 @@ func TestHASequencerRollingRestartE2E(t *testing.T) { workDir := t.TempDir() - // Get JWT secrets and setup common components first - jwtSecret, fullNodeJwtSecret, genesisHash, testEndpoints, _ := setupCommonEVMTest(t, sut, true) - rethFn := evmtest.SetupTestRethNode(t) + // Get Docker and common environment + dcli, netID := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dcli, netID, WithFullNode()) + rethFn := evmtest.SetupTestRethNode(t, dcli, netID) jwtSecret3 := rethFn.JWTSecretHex() fnInfo, err := rethFn.GetNetworkInfo(context.Background()) require.NoError(t, err, "failed to get full node reth network info") @@ -278,17 +282,17 @@ func TestHASequencerRollingRestartE2E(t *testing.T) { clusterNodes := &raftClusterNodes{ nodes: make(map[string]*nodeDetails), } - node1P2PAddr := testEndpoints.GetRollkitP2PAddress() - node2P2PAddr := testEndpoints.GetFullNodeP2PAddress() + node1P2PAddr := env.Endpoints.GetRollkitP2PAddress() + node2P2PAddr := env.Endpoints.GetFullNodeP2PAddress() node3P2PAddr := fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", mustGetAvailablePort(t)) // Start node1 (bootstrap mode) go func() { p2pPeers := node2P2PAddr + "," + node3P2PAddr - proc := setupRaftSequencerNode(t, sut, workDir, "node1", node1RaftAddr, jwtSecret, genesisHash, testEndpoints.GetDAAddress(), - bootstrapDir, raftCluster, p2pPeers, testEndpoints.GetRollkitRPCListen(), testEndpoints.GetRollkitP2PAddress(), - testEndpoints.GetSequencerEngineURL(), testEndpoints.GetSequencerEthURL(), true, passphraseFile) - clusterNodes.Set("node1", testEndpoints.GetRollkitRPCAddress(), proc, testEndpoints.GetSequencerEthURL(), node1RaftAddr, testEndpoints.GetRollkitP2PAddress(), testEndpoints.GetSequencerEngineURL(), testEndpoints.GetSequencerEthURL()) + proc := setupRaftSequencerNode(t, sut, workDir, "node1", node1RaftAddr, env.SequencerJWT, env.GenesisHash, env.Endpoints.GetDAAddress(), + bootstrapDir, raftCluster, p2pPeers, env.Endpoints.GetRollkitRPCListen(), env.Endpoints.GetRollkitP2PAddress(), + env.Endpoints.GetSequencerEngineURL(), env.Endpoints.GetSequencerEthURL(), true, passphraseFile) + clusterNodes.Set("node1", env.Endpoints.GetRollkitRPCAddress(), proc, env.Endpoints.GetSequencerEthURL(), node1RaftAddr, env.Endpoints.GetRollkitP2PAddress(), env.Endpoints.GetSequencerEngineURL(), env.Endpoints.GetSequencerEthURL()) t.Log("Node1 is up") }() @@ -296,8 +300,8 @@ func TestHASequencerRollingRestartE2E(t *testing.T) { go func() { t.Log("Starting Node2") p2pPeers := node1P2PAddr + "," + node3P2PAddr - proc := setupRaftSequencerNode(t, sut, workDir, "node2", node2RaftAddr, fullNodeJwtSecret, genesisHash, testEndpoints.GetDAAddress(), bootstrapDir, raftCluster, p2pPeers, testEndpoints.GetFullNodeRPCListen(), testEndpoints.GetFullNodeP2PAddress(), testEndpoints.GetFullNodeEngineURL(), testEndpoints.GetFullNodeEthURL(), true, passphraseFile) - clusterNodes.Set("node2", testEndpoints.GetFullNodeRPCAddress(), proc, testEndpoints.GetFullNodeEthURL(), node2RaftAddr, testEndpoints.GetFullNodeP2PAddress(), testEndpoints.GetFullNodeEngineURL(), testEndpoints.GetFullNodeEthURL()) + proc := setupRaftSequencerNode(t, sut, workDir, "node2", node2RaftAddr, env.FullNodeJWT, env.GenesisHash, env.Endpoints.GetDAAddress(), bootstrapDir, raftCluster, p2pPeers, env.Endpoints.GetFullNodeRPCListen(), env.Endpoints.GetFullNodeP2PAddress(), env.Endpoints.GetFullNodeEngineURL(), env.Endpoints.GetFullNodeEthURL(), true, passphraseFile) + clusterNodes.Set("node2", env.Endpoints.GetFullNodeRPCAddress(), proc, env.Endpoints.GetFullNodeEthURL(), node2RaftAddr, env.Endpoints.GetFullNodeP2PAddress(), env.Endpoints.GetFullNodeEngineURL(), env.Endpoints.GetFullNodeEthURL()) t.Log("Node2 is up") }() @@ -308,7 +312,7 @@ func TestHASequencerRollingRestartE2E(t *testing.T) { p2pPeers := node1P2PAddr + "," + node2P2PAddr node3RPCListen := fmt.Sprintf("127.0.0.1:%d", mustGetAvailablePort(t)) ethEngineURL := fmt.Sprintf("http://127.0.0.1:%s", fullNode3EnginePort) - proc := setupRaftSequencerNode(t, sut, workDir, "node3", node3RaftAddr, jwtSecret3, genesisHash, testEndpoints.GetDAAddress(), bootstrapDir, raftCluster, p2pPeers, node3RPCListen, node3P2PAddr, ethEngineURL, node3EthAddr, true, passphraseFile) + proc := setupRaftSequencerNode(t, sut, workDir, "node3", node3RaftAddr, jwtSecret3, env.GenesisHash, env.Endpoints.GetDAAddress(), bootstrapDir, raftCluster, p2pPeers, node3RPCListen, node3P2PAddr, ethEngineURL, node3EthAddr, true, passphraseFile) clusterNodes.Set("node3", "http://"+node3RPCListen, proc, node3EthAddr, node3RaftAddr, node3P2PAddr, ethEngineURL, node3EthAddr) t.Log("Node3 is up") }() @@ -323,7 +327,7 @@ func TestHASequencerRollingRestartE2E(t *testing.T) { sut.AwaitNBlocks(t, 5, clusterNodes.Details(leaderNode).rpcAddr, 10*time.Second) const daStartHeight = 1 - initialDAHeight := queryLastDAHeight(t, daStartHeight, jwtSecret, testEndpoints.GetDAAddress()) + initialDAHeight := queryLastDAHeight(t, daStartHeight, env.SequencerJWT, env.Endpoints.GetDAAddress()) t.Logf("+++ Initial DA height: %d", initialDAHeight) // Calculate downtime per node @@ -356,9 +360,9 @@ func TestHASequencerRollingRestartE2E(t *testing.T) { getNodeJWT := func(nodeName string) string { switch nodeName { case "node1": - return jwtSecret + return env.SequencerJWT case "node2": - return fullNodeJwtSecret + return env.FullNodeJWT case "node3": return jwtSecret3 } @@ -390,8 +394,8 @@ func TestHASequencerRollingRestartE2E(t *testing.T) { time.Sleep(200 * time.Millisecond) } - restartedProc := setupRaftSequencerNode(t, sut, workDir, nodeName, nodeDetails.raftAddr, nodeJWT, genesisHash, - testEndpoints.GetDAAddress(), "", raftCluster, p2pPeers, + restartedProc := setupRaftSequencerNode(t, sut, workDir, nodeName, nodeDetails.raftAddr, nodeJWT, env.GenesisHash, + env.Endpoints.GetDAAddress(), "", raftCluster, p2pPeers, strings.TrimPrefix(nodeDetails.rpcAddr, "http://"), nodeDetails.p2pAddr, nodeDetails.engineURL, nodeDetails.ethAddr, false, passphraseFile) @@ -506,7 +510,7 @@ func TestHASequencerRollingRestartE2E(t *testing.T) { return err == nil }, time.Second, 100*time.Millisecond) - lastDABlock := queryLastDAHeight(t, initialDAHeight, jwtSecret, testEndpoints.GetDAAddress()) + lastDABlock := queryLastDAHeight(t, initialDAHeight, env.SequencerJWT, env.Endpoints.GetDAAddress()) genesisHeight := state.InitialHeight verifyNoDoubleSigning(t, clusterNodes, genesisHeight, state.LastBlockHeight) @@ -515,13 +519,13 @@ func TestHASequencerRollingRestartE2E(t *testing.T) { // Wait for the next DA block to ensure all blocks are propagated require.Eventually(t, func() bool { before := lastDABlock - lastDABlock = queryLastDAHeight(t, lastDABlock, jwtSecret, testEndpoints.GetDAAddress()) + lastDABlock = queryLastDAHeight(t, lastDABlock, env.SequencerJWT, env.Endpoints.GetDAAddress()) return before < lastDABlock }, 2*must(time.ParseDuration(DefaultDABlockTime)), 100*time.Millisecond) // Verify no DA gaps t.Log("+++ Verifying no DA gaps...") - verifyDABlocks(t, daStartHeight, lastDABlock, jwtSecret, testEndpoints.GetDAAddress(), genesisHeight, state.LastBlockHeight) + verifyDABlocks(t, daStartHeight, lastDABlock, env.SequencerJWT, env.Endpoints.GetDAAddress(), genesisHeight, state.LastBlockHeight) t.Log("+++ No DA gaps detected ✓") // Cleanup processes From 1a240610fee4fd573c95605d78b0a773e26317fe Mon Sep 17 00:00:00 2001 From: chatton Date: Mon, 23 Feb 2026 13:32:51 +0000 Subject: [PATCH 03/11] chore: fix test compilation errors, removed useless comments --- execution/evm/test/test_helpers.go | 2 -- test/e2e/evm_force_inclusion_e2e_test.go | 10 +++++----- test/e2e/evm_full_node_e2e_test.go | 1 - 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/execution/evm/test/test_helpers.go b/execution/evm/test/test_helpers.go index eed7f3950..23b7bb253 100644 --- a/execution/evm/test/test_helpers.go +++ b/execution/evm/test/test_helpers.go @@ -20,8 +20,6 @@ import ( "go.uber.org/zap/zaptest" ) -// (Docker client/network are now provided by callers) - // RethNodeOpt allows tests to customize the reth node builder before building. type RethNodeOpt func(b *reth.NodeBuilder) diff --git a/test/e2e/evm_force_inclusion_e2e_test.go b/test/e2e/evm_force_inclusion_e2e_test.go index 84a5d9ce1..00046e3b2 100644 --- a/test/e2e/evm_force_inclusion_e2e_test.go +++ b/test/e2e/evm_force_inclusion_e2e_test.go @@ -79,8 +79,7 @@ func setupSequencerWithForceInclusion(t *testing.T, sut *SystemUnderTest, nodeHo // Use common setup (no full node needed initially) dcli, netID := tastoradocker.Setup(t) - env := setupCommonEVMEnv(t, sut, dcli, netID) - // Use env fields inline below to reduce local vars + env := setupCommonEVMEnv(t, sut, dcli, netID) // Create passphrase file passphraseFile := createPassphraseFile(t, nodeHome) @@ -195,8 +194,8 @@ func TestEvmFullNodeForceInclusionE2E(t *testing.T) { // --- Start Sequencer Setup --- // We manually setup sequencer here because we need the force inclusion flag, // and we need to capture variables for full node setup. - dcli2, netID2 := tastoradocker.Setup(t) - env := setupCommonEVMEnv(t, sut, dcli2, netID2, WithFullNode()) + dockerClient, networkID := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dockerClient, networkID, WithFullNode()) passphraseFile := createPassphraseFile(t, sequencerHome) jwtSecretFile := createJWTSecretFile(t, sequencerHome, env.SequencerJWT) @@ -288,7 +287,8 @@ func setupMaliciousSequencer(t *testing.T, sut *SystemUnderTest, nodeHome string t.Helper() // Use common setup with full node support - env := setupCommonEVMEnv(t, sut, WithFullNode()) + dockerClient, networkID := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dockerClient, networkID, WithFullNode()) // Use env fields inline below to reduce local vars passphraseFile := createPassphraseFile(t, nodeHome) diff --git a/test/e2e/evm_full_node_e2e_test.go b/test/e2e/evm_full_node_e2e_test.go index 63ee38975..23a3dbcf8 100644 --- a/test/e2e/evm_full_node_e2e_test.go +++ b/test/e2e/evm_full_node_e2e_test.go @@ -1041,7 +1041,6 @@ func testSequencerFullNodeRestart(t *testing.T, initialLazyMode, restartLazyMode t.Logf("Phase 1: Setting up sequencer (initial_lazy=%t) and full node with P2P connections...", initialLazyMode) t.Logf("Test mode: initial_lazy=%t, restart_lazy=%t", initialLazyMode, restartLazyMode) - // Get Docker client/network and common environment dcli, netID := tastoradocker.Setup(t) env := setupCommonEVMEnv(t, sut, dcli, netID, WithFullNode()) From 3cae3eeb95ab17797fa3a713672ee1516ff42259 Mon Sep 17 00:00:00 2001 From: chatton Date: Mon, 23 Feb 2026 14:01:20 +0000 Subject: [PATCH 04/11] chore: addresing PR feedback --- execution/evm/test/test_helpers.go | 3 +-- test/e2e/evm_contract_e2e_test.go | 28 ++++++++++---------- test/e2e/evm_spamoor_smoke_test.go | 42 +++++++++++++++--------------- test/e2e/evm_test_common.go | 8 +++--- test/e2e/failover_e2e_test.go | 10 +++---- 5 files changed, 44 insertions(+), 47 deletions(-) diff --git a/execution/evm/test/test_helpers.go b/execution/evm/test/test_helpers.go index 23b7bb253..b7e96a923 100644 --- a/execution/evm/test/test_helpers.go +++ b/execution/evm/test/test_helpers.go @@ -60,11 +60,10 @@ func SetupTestRethNode(t testing.TB, client types.TastoraDockerClient, networkID } n, err := b.Build(ctx) + require.NoError(t, err) t.Cleanup(func() { _ = n.Remove(context.Background()) }) - - require.NoError(t, err) require.NoError(t, n.Start(ctx)) ni, err := n.GetNetworkInfo(ctx) diff --git a/test/e2e/evm_contract_e2e_test.go b/test/e2e/evm_contract_e2e_test.go index 48bc2b2a3..72e894adc 100644 --- a/test/e2e/evm_contract_e2e_test.go +++ b/test/e2e/evm_contract_e2e_test.go @@ -3,16 +3,16 @@ package e2e import ( - "context" - "crypto/ecdsa" - "math/big" - "path/filepath" - "testing" - "time" - - tastoradocker "github.com/celestiaorg/tastora/framework/docker" - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" + "context" + "crypto/ecdsa" + "math/big" + "path/filepath" + "testing" + "time" + + tastoradocker "github.com/celestiaorg/tastora/framework/docker" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -50,7 +50,7 @@ func TestEvmContractDeploymentAndInteraction(t *testing.T) { workDir := t.TempDir() sequencerHome := filepath.Join(workDir, "evm-sequencer") - client, _, cleanup := setupTestSequencer(t, sequencerHome) + client, _, cleanup := setupTestSequencer(t, sequencerHome) defer cleanup() ctx := t.Context() @@ -242,10 +242,10 @@ func TestEvmContractEvents(t *testing.T) { // setupTestSequencer sets up a single sequencer node for testing. // Returns the ethclient, genesis hash, and a cleanup function. func setupTestSequencer(t testing.TB, homeDir string, extraArgs ...string) (*ethclient.Client, string, func()) { - sut := NewSystemUnderTest(t) + sut := NewSystemUnderTest(t) - dcli, netID := tastoradocker.Setup(t) - genesisHash, seqEthURL := setupSequencerOnlyTest(t, sut, homeDir, dcli, netID, extraArgs...) + dcli, netID := tastoradocker.Setup(t) + genesisHash, seqEthURL := setupSequencerOnlyTest(t, sut, homeDir, dcli, netID, extraArgs...) t.Logf("Sequencer started at %s (Genesis: %s)", seqEthURL, genesisHash) client, err := ethclient.Dial(seqEthURL) diff --git a/test/e2e/evm_spamoor_smoke_test.go b/test/e2e/evm_spamoor_smoke_test.go index 80a2b9331..85a192bf3 100644 --- a/test/e2e/evm_spamoor_smoke_test.go +++ b/test/e2e/evm_spamoor_smoke_test.go @@ -3,17 +3,17 @@ package e2e import ( - "context" - "fmt" - "net/http" - "path/filepath" - "testing" - "time" - - tastoradocker "github.com/celestiaorg/tastora/framework/docker" - spamoor "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" - dto "github.com/prometheus/client_model/go" - "github.com/stretchr/testify/require" + "context" + "fmt" + "net/http" + "path/filepath" + "testing" + "time" + + tastoradocker "github.com/celestiaorg/tastora/framework/docker" + spamoor "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" + dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/require" ) // TestSpamoorSmoke spins up reth + sequencer and a Spamoor node, starts a few @@ -22,10 +22,10 @@ func TestSpamoorSmoke(t *testing.T) { t.Parallel() sut := NewSystemUnderTest(t) - // Bring up reth + local DA and start sequencer with default settings. - dcli, netID := tastoradocker.Setup(t) - env := setupCommonEVMEnv(t, sut, dcli, netID) - sequencerHome := filepath.Join(t.TempDir(), "sequencer") + // Bring up reth + local DA and start sequencer with default settings. + dcli, netID := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dcli, netID) + sequencerHome := filepath.Join(t.TempDir(), "sequencer") // In-process OTLP/HTTP collector to capture ev-node spans. collector := newOTLPCollector(t) @@ -34,7 +34,7 @@ func TestSpamoorSmoke(t *testing.T) { }) // Start sequencer with tracing to our collector. - setupSequencerNode(t, sut, sequencerHome, env.SequencerJWT, env.GenesisHash, env.Endpoints, + setupSequencerNode(t, sut, sequencerHome, env.SequencerJWT, env.GenesisHash, env.Endpoints, "--evnode.instrumentation.tracing=true", "--evnode.instrumentation.tracing_endpoint", collector.endpoint(), "--evnode.instrumentation.tracing_sample_rate", "1.0", @@ -43,15 +43,15 @@ func TestSpamoorSmoke(t *testing.T) { t.Log("Sequencer node is up") // Start Spamoor within the same Docker network, targeting reth internal RPC. - ni, err := env.RethNode.GetNetworkInfo(context.Background()) + ni, err := env.RethNode.GetNetworkInfo(context.Background()) require.NoError(t, err, "failed to get network info") internalRPC := "http://" + ni.Internal.RPCAddress() - spBuilder := spamoor.NewNodeBuilder(t.Name()). - WithDockerClient(env.RethNode.DockerClient). - WithDockerNetworkID(env.RethNode.NetworkID). - WithLogger(env.RethNode.Logger). + spBuilder := spamoor.NewNodeBuilder(t.Name()). + WithDockerClient(env.RethNode.DockerClient). + WithDockerNetworkID(env.RethNode.NetworkID). + WithLogger(env.RethNode.Logger). WithRPCHosts(internalRPC). WithPrivateKey(TestPrivateKey) diff --git a/test/e2e/evm_test_common.go b/test/e2e/evm_test_common.go index e7b4cd1bf..506c2a7f7 100644 --- a/test/e2e/evm_test_common.go +++ b/test/e2e/evm_test_common.go @@ -521,7 +521,7 @@ func WithFullNode() SetupOpt { } } -// setupCommonEVMEnv creates and initializes ev-reth instances, while also initalizing the local ev-node instance +// setupCommonEVMEnv creates and initializes ev-reth instances, while also initializing the local ev-node instance // managed by sut. If a full node is also required, we can use the WithFullNode() additional option. func setupCommonEVMEnv(t testing.TB, sut *SystemUnderTest, client tastoratypes.TastoraDockerClient, networkID string, opts ...SetupOpt) *EVMEnv { t.Helper() @@ -551,9 +551,7 @@ func setupCommonEVMEnv(t testing.TB, sut *SystemUnderTest, client tastoratypes.T require.NotNil(t, client, "docker client is required") require.NotEmpty(t, networkID, "docker networkID is required") - dcli := client - netID := networkID - rethNode := evmtest.SetupTestRethNode(t, dcli, netID, cfg.rethOpts...) + rethNode := evmtest.SetupTestRethNode(t, client, networkID, cfg.rethOpts...) networkInfo, err := rethNode.GetNetworkInfo(context.Background()) require.NoError(t, err, "failed to get reth network info") @@ -563,7 +561,7 @@ func setupCommonEVMEnv(t testing.TB, sut *SystemUnderTest, client tastoratypes.T var fnJWT string var rethFn *reth.Node if cfg.needsFullNode { - rethFn = evmtest.SetupTestRethNode(t, dcli, netID, cfg.rethOpts...) + rethFn = evmtest.SetupTestRethNode(t, client, networkID, cfg.rethOpts...) fnJWT = rethFn.JWTSecretHex() } diff --git a/test/e2e/failover_e2e_test.go b/test/e2e/failover_e2e_test.go index 85fdc2533..905b280cf 100644 --- a/test/e2e/failover_e2e_test.go +++ b/test/e2e/failover_e2e_test.go @@ -55,8 +55,8 @@ func TestLeaseFailoverE2E(t *testing.T) { workDir := t.TempDir() // Get JWT secrets and setup common components first - dockerClient, networkID := tastoradocker.Setup(t) - env := setupCommonEVMEnv(t, sut, dockerClient, networkID, WithFullNode()) + dockerClient, networkID := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dockerClient, networkID, WithFullNode()) // Use a fresh reth node on the same Docker network as used by the env setup. rethFn := evmtest.SetupTestRethNode(t, dockerClient, networkID) jwtSecret3 := rethFn.JWTSecretHex() @@ -256,9 +256,9 @@ func TestHASequencerRollingRestartE2E(t *testing.T) { workDir := t.TempDir() // Get Docker and common environment - dcli, netID := tastoradocker.Setup(t) - env := setupCommonEVMEnv(t, sut, dcli, netID, WithFullNode()) - rethFn := evmtest.SetupTestRethNode(t, dcli, netID) + dockerClient, networkID := tastoradocker.Setup(t) + env := setupCommonEVMEnv(t, sut, dockerClient, networkID, WithFullNode()) + rethFn := evmtest.SetupTestRethNode(t, dockerClient, networkID) jwtSecret3 := rethFn.JWTSecretHex() fnInfo, err := rethFn.GetNetworkInfo(context.Background()) require.NoError(t, err, "failed to get full node reth network info") From f3f8bac05f977283854427ec7572668ebc7065f4 Mon Sep 17 00:00:00 2001 From: chatton Date: Mon, 23 Feb 2026 15:16:28 +0000 Subject: [PATCH 05/11] chore: create common trace printing code --- test/e2e/evm_contract_bench_test.go | 88 +++++------------------------ test/e2e/evm_spamoor_smoke_test.go | 78 +++++++++++++++++++++---- test/e2e/evm_test_common.go | 77 +++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 84 deletions(-) diff --git a/test/e2e/evm_contract_bench_test.go b/test/e2e/evm_contract_bench_test.go index 5c0e31b3e..7fd67dcc5 100644 --- a/test/e2e/evm_contract_bench_test.go +++ b/test/e2e/evm_contract_bench_test.go @@ -11,7 +11,6 @@ import ( "net" "net/http" "path/filepath" - "sort" "sync" "testing" "time" @@ -203,81 +202,24 @@ func (c *otlpCollector) getSpans() []*tracepb.Span { return cp } -// printCollectedTraceReport aggregates collected spans by operation name and -// prints a timing breakdown. -func printCollectedTraceReport(b testing.TB, collector *otlpCollector) { - b.Helper() - - spans := collector.getSpans() - if len(spans) == 0 { - b.Logf("WARNING: no spans collected from ev-node") - return - } - - type stats struct { - count int - total time.Duration - min time.Duration - max time.Duration - } - m := make(map[string]*stats) - - for _, span := range spans { - // Duration: end - start in nanoseconds. - d := time.Duration(span.GetEndTimeUnixNano()-span.GetStartTimeUnixNano()) * time.Nanosecond - if d <= 0 { - continue - } - name := span.GetName() - s, ok := m[name] - if !ok { - s = &stats{min: d, max: d} - m[name] = s - } - s.count++ - s.total += d - if d < s.min { - s.min = d - } - if d > s.max { - s.max = d - } - } - - // Sort by total time descending. - names := make([]string, 0, len(m)) - for name := range m { - names = append(names, name) - } - sort.Slice(names, func(i, j int) bool { - return m[names[i]].total > m[names[j]].total - }) - - // Calculate overall total for percentages. - var overallTotal time.Duration - for _, s := range m { - overallTotal += s.total - } +// otlpSpanAdapter wraps an OTLP protobuf span to implement traceSpan. +type otlpSpanAdapter struct { + span *tracepb.Span +} - b.Logf("\n--- ev-node Trace Breakdown (%d spans collected) ---", len(spans)) - b.Logf("%-40s %6s %12s %12s %12s %7s", "OPERATION", "COUNT", "AVG", "MIN", "MAX", "% TOTAL") - for _, name := range names { - s := m[name] - avg := s.total / time.Duration(s.count) - pct := float64(s.total) / float64(overallTotal) * 100 - b.Logf("%-40s %6d %12s %12s %12s %6.1f%%", name, s.count, avg, s.min, s.max, pct) - } +func (a otlpSpanAdapter) SpanName() string { return a.span.GetName() } +func (a otlpSpanAdapter) SpanDuration() time.Duration { + return time.Duration(a.span.GetEndTimeUnixNano()-a.span.GetStartTimeUnixNano()) * time.Nanosecond +} - b.Logf("\n--- Time Distribution ---") - for _, name := range names { - s := m[name] - pct := float64(s.total) / float64(overallTotal) * 100 - bar := "" - for range int(pct / 2) { - bar += "█" - } - b.Logf("%-40s %5.1f%% %s", name, pct, bar) +func printCollectedTraceReport(b testing.TB, collector *otlpCollector) { + b.Helper() + raw := collector.getSpans() + spans := make([]traceSpan, len(raw)) + for i, s := range raw { + spans[i] = otlpSpanAdapter{span: s} } + printTraceReport(b, "ev-node", spans) } // waitForReceipt polls for a transaction receipt until it is available. diff --git a/test/e2e/evm_spamoor_smoke_test.go b/test/e2e/evm_spamoor_smoke_test.go index 3ad7142f2..41aba9314 100644 --- a/test/e2e/evm_spamoor_smoke_test.go +++ b/test/e2e/evm_spamoor_smoke_test.go @@ -169,8 +169,32 @@ func TestSpamoorSmoke(t *testing.T) { ok, err = jg.External.WaitForTraces(traceCtx, "ev-reth", 1, 2*time.Second) require.NoError(t, err, "error while waiting for ev-reth traces; UI: %s", jg.External.URL()) require.True(t, ok, "expected at least one trace from ev-reth; UI: %s", jg.External.URL()) - if traces, err := jg.External.Traces(traceCtx, "ev-reth", 3); err == nil && len(traces) > 0 { - t.Logf("sample ev-reth traces: %v", traces[0]) + + // fetch traces and print reports for both services. + // use a large limit to fetch all traces from the test run. + evNodeTraces, err := jg.External.Traces(traceCtx, "ev-node-smoke", 10000) + require.NoError(t, err, "failed to fetch ev-node-smoke traces from Jaeger") + evNodeSpans := extractSpansFromTraces(evNodeTraces) + printTraceReport(t, "ev-node-smoke", toTraceSpans(evNodeSpans)) + + evRethTraces, err := jg.External.Traces(traceCtx, "ev-reth", 10000) + require.NoError(t, err, "failed to fetch ev-reth traces from Jaeger") + evRethSpans := extractSpansFromTraces(evRethTraces) + printTraceReport(t, "ev-reth", toTraceSpans(evRethSpans)) + + // assert expected ev-node span names are present. + // these spans reliably appear during block production with transactions flowing. + expectedSpans := []string{ + "Engine.ForkchoiceUpdated", + "Executor.SetFinal", + "Executor.ExecuteTxs", + } + opNames := make(map[string]struct{}, len(evNodeSpans)) + for _, s := range evNodeSpans { + opNames[s.operationName] = struct{}{} + } + for _, name := range expectedSpans { + require.Contains(t, opNames, name, "expected span %q not found in ev-node-smoke traces", name) } require.Greater(t, sent, float64(0), "at least one transaction should have been sent") @@ -213,15 +237,47 @@ func sumCounter(f *dto.MetricFamily) float64 { } return sum } -func sumGauge(f *dto.MetricFamily) float64 { - if f == nil || f.GetType() != dto.MetricType_GAUGE { - return 0 - } - var sum float64 - for _, m := range f.GetMetric() { - if m.GetGauge() != nil && m.GetGauge().Value != nil { - sum += m.GetGauge().GetValue() + +// jaegerSpan holds the fields we extract from Jaeger's untyped JSON response. +type jaegerSpan struct { + operationName string + duration float64 // microseconds +} + +func (j jaegerSpan) SpanName() string { return j.operationName } +func (j jaegerSpan) SpanDuration() time.Duration { return time.Duration(j.duration) * time.Microsecond } + +// extractSpansFromTraces walks Jaeger's []any response and pulls out span operation names and durations. +func extractSpansFromTraces(traces []any) []jaegerSpan { + var out []jaegerSpan + for _, t := range traces { + traceMap, ok := t.(map[string]any) + if !ok { + continue + } + spans, ok := traceMap["spans"].([]any) + if !ok { + continue + } + for _, s := range spans { + spanMap, ok := s.(map[string]any) + if !ok { + continue + } + name, _ := spanMap["operationName"].(string) + dur, _ := spanMap["duration"].(float64) + if name != "" { + out = append(out, jaegerSpan{operationName: name, duration: dur}) + } } } - return sum + return out +} + +func toTraceSpans(spans []jaegerSpan) []traceSpan { + out := make([]traceSpan, len(spans)) + for i, s := range spans { + out[i] = s + } + return out } diff --git a/test/e2e/evm_test_common.go b/test/e2e/evm_test_common.go index c4e9727ce..b81823b15 100644 --- a/test/e2e/evm_test_common.go +++ b/test/e2e/evm_test_common.go @@ -23,6 +23,7 @@ import ( "os" "path/filepath" "regexp" + "sort" "strconv" "strings" "testing" @@ -845,3 +846,79 @@ func verifyNoBlockProduction(t testing.TB, client *ethclient.Client, duration ti t.Logf("✅ %s maintained height %d for %v (no new blocks produced)", nodeName, initialHeight, duration) } + +// traceSpan is a common interface for span data from different sources (OTLP collector, Jaeger API). +type traceSpan interface { + SpanName() string + SpanDuration() time.Duration +} + +// printTraceReport aggregates spans by operation name and prints a timing breakdown. +func printTraceReport(t testing.TB, label string, spans []traceSpan) { + t.Helper() + if len(spans) == 0 { + t.Logf("WARNING: no spans found for %s", label) + return + } + + type stats struct { + count int + total time.Duration + min time.Duration + max time.Duration + } + m := make(map[string]*stats) + for _, span := range spans { + d := span.SpanDuration() + if d <= 0 { + continue + } + name := span.SpanName() + s, ok := m[name] + if !ok { + s = &stats{min: d, max: d} + m[name] = s + } + s.count++ + s.total += d + if d < s.min { + s.min = d + } + if d > s.max { + s.max = d + } + } + + names := make([]string, 0, len(m)) + for name := range m { + names = append(names, name) + } + sort.Slice(names, func(i, j int) bool { + return m[names[i]].total > m[names[j]].total + }) + + var overallTotal time.Duration + for _, s := range m { + overallTotal += s.total + } + + t.Logf("\n--- %s Trace Breakdown (%d spans) ---", label, len(spans)) + t.Logf("%-40s %6s %12s %12s %12s %7s", "OPERATION", "COUNT", "AVG", "MIN", "MAX", "% TOTAL") + for _, name := range names { + s := m[name] + avg := s.total / time.Duration(s.count) + pct := float64(s.total) / float64(overallTotal) * 100 + t.Logf("%-40s %6d %12s %12s %12s %6.1f%%", name, s.count, avg, s.min, s.max, pct) + } + + t.Logf("\n--- %s Time Distribution ---", label) + for _, name := range names { + s := m[name] + pct := float64(s.total) / float64(overallTotal) * 100 + bar := "" + for range int(pct / 2) { + bar += "█" + } + t.Logf("%-40s %5.1f%% %s", name, pct, bar) + } +} From 85531fc3aa981de01865fef382595ccab8bd2480 Mon Sep 17 00:00:00 2001 From: chatton Date: Mon, 23 Feb 2026 16:07:51 +0000 Subject: [PATCH 06/11] chore: adding assertions on addtional spans --- test/e2e/evm_spamoor_smoke_test.go | 45 ++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/test/e2e/evm_spamoor_smoke_test.go b/test/e2e/evm_spamoor_smoke_test.go index 41aba9314..7a24ea362 100644 --- a/test/e2e/evm_spamoor_smoke_test.go +++ b/test/e2e/evm_spamoor_smoke_test.go @@ -39,7 +39,7 @@ func TestSpamoorSmoke(t *testing.T) { env := setupCommonEVMEnv(t, sut, dcli, netID, WithRethOpts(func(b *reth.NodeBuilder) { b.WithEnv( - "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="+jg.IngestHTTPEndpoint()+"/v1/traces", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="+jg.Internal.IngestHTTPEndpoint()+"/v1/traces", "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http", "RUST_LOG=info", "OTEL_SDK_DISABLED=false", @@ -48,10 +48,8 @@ func TestSpamoorSmoke(t *testing.T) { ) sequencerHome := filepath.Join(t.TempDir(), "sequencer") - // ev-node runs on the host, so use Jaeger's host-mapped OTLP/HTTP port (external address). - jinfo, err := jg.GetNetworkInfo(ctx) - require.NoError(t, err, "failed to get jaeger network info") - otlpHTTP := fmt.Sprintf("http://127.0.0.1:%s", jinfo.External.Ports.HTTP) + // ev-node runs on the host, so use Jaeger's external OTLP/HTTP endpoint. + otlpHTTP := jg.External.IngestHTTPEndpoint() // Start sequencer with tracing to Jaeger collector. setupSequencerNode(t, sut, sequencerHome, env.SequencerJWT, env.GenesisHash, env.Endpoints, @@ -162,13 +160,13 @@ func TestSpamoorSmoke(t *testing.T) { traceCtx, cancel := context.WithTimeout(ctx, 3*time.Minute) defer cancel() ok, err := jg.External.WaitForTraces(traceCtx, "ev-node-smoke", 1, 2*time.Second) - require.NoError(t, err, "error while waiting for Jaeger traces; UI: %s", jg.QueryHostURL()) - require.True(t, ok, "expected at least one trace in Jaeger; UI: %s", jg.QueryHostURL()) + require.NoError(t, err, "error while waiting for Jaeger traces; UI: %s", jg.External.QueryURL()) + require.True(t, ok, "expected at least one trace in Jaeger; UI: %s", jg.External.QueryURL()) // Also wait for traces from ev-reth and print a small sample. ok, err = jg.External.WaitForTraces(traceCtx, "ev-reth", 1, 2*time.Second) - require.NoError(t, err, "error while waiting for ev-reth traces; UI: %s", jg.External.URL()) - require.True(t, ok, "expected at least one trace from ev-reth; UI: %s", jg.External.URL()) + require.NoError(t, err, "error while waiting for ev-reth traces; UI: %s", jg.External.QueryURL()) + require.True(t, ok, "expected at least one trace from ev-reth; UI: %s", jg.External.QueryURL()) // fetch traces and print reports for both services. // use a large limit to fetch all traces from the test run. @@ -185,9 +183,21 @@ func TestSpamoorSmoke(t *testing.T) { // assert expected ev-node span names are present. // these spans reliably appear during block production with transactions flowing. expectedSpans := []string{ - "Engine.ForkchoiceUpdated", - "Executor.SetFinal", + "BlockExecutor.ProduceBlock", + "BlockExecutor.ApplyBlock", + "BlockExecutor.CreateBlock", + "BlockExecutor.ValidateBlock", + "BlockExecutor.RetrieveBatch", "Executor.ExecuteTxs", + "Executor.SetFinal", + "Engine.ForkchoiceUpdated", + "Engine.NewPayload", + "Engine.GetPayload", + "Eth.GetBlockByNumber", + "Sequencer.GetNextBatch", + "DASubmitter.SubmitHeaders", + "DASubmitter.SubmitData", + "DA.Submit", } opNames := make(map[string]struct{}, len(evNodeSpans)) for _, s := range evNodeSpans { @@ -197,6 +207,19 @@ func TestSpamoorSmoke(t *testing.T) { require.Contains(t, opNames, name, "expected span %q not found in ev-node-smoke traces", name) } + // assert expected ev-reth span names are present. + expectedRethSpans := []string{ + "Storage trie", + "cache_for", + } + rethOpNames := make(map[string]struct{}, len(evRethSpans)) + for _, s := range evRethSpans { + rethOpNames[s.operationName] = struct{}{} + } + for _, name := range expectedRethSpans { + require.Contains(t, rethOpNames, name, "expected span %q not found in ev-reth traces", name) + } + require.Greater(t, sent, float64(0), "at least one transaction should have been sent") require.Zero(t, fail, "no transactions should have failed") } From 6e224373e0d9de2e381385a6d0a50340d31d127c Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 24 Feb 2026 08:54:47 +0000 Subject: [PATCH 07/11] deps: tidy all --- test/e2e/go.mod | 3 +-- test/e2e/go.sum | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/e2e/go.mod b/test/e2e/go.mod index cd119464c..46710aa54 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -5,7 +5,7 @@ go 1.25.6 require ( cosmossdk.io/math v1.5.3 github.com/celestiaorg/go-square/v3 v3.0.2 - github.com/celestiaorg/tastora v0.15.0 + github.com/celestiaorg/tastora v0.16.0 github.com/cosmos/cosmos-sdk v0.53.6 github.com/cosmos/ibc-go/v8 v8.8.0 github.com/ethereum/go-ethereum v1.17.0 @@ -22,7 +22,6 @@ require ( ) replace ( - github.com/celestiaorg/tastora => ../../../../celestiaorg/tastora github.com/evstack/ev-node => ../../ github.com/evstack/ev-node/core => ../../core github.com/evstack/ev-node/execution/evm => ../../execution/evm diff --git a/test/e2e/go.sum b/test/e2e/go.sum index 0d6619217..782994eb2 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -145,6 +145,8 @@ github.com/celestiaorg/go-square/v3 v3.0.2 h1:eSQOgNII8inK9IhiBZ+6GADQeWbRq4HYY7 github.com/celestiaorg/go-square/v3 v3.0.2/go.mod h1:oFReMLsSDMRs82ICFEeFQFCqNvwdsbIM1BzCcb0f7dM= github.com/celestiaorg/nmt v0.24.2 h1:LlpJSPOd6/Lw1Ig6HUhZuqiINHLka/ZSRTBzlNJpchg= github.com/celestiaorg/nmt v0.24.2/go.mod h1:vgLBpWBi8F5KLxTdXSwb7AU4NhiIQ1AQRGa+PzdcLEA= +github.com/celestiaorg/tastora v0.16.0 h1:V4ctGcvVR8thy4ulvrHagrTfdNfuCHOTsCYoKVRQ75U= +github.com/celestiaorg/tastora v0.16.0/go.mod h1:C867PBm6Ne6e/1JlmsRqcLeJ6RHAuMoMRCvwJzV/q8g= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= From 5106f55ea5fb7152b9d1ee8980c7688a533ee553 Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 24 Feb 2026 08:59:10 +0000 Subject: [PATCH 08/11] chore: address PR feedback --- test/e2e/evm_spamoor_smoke_test.go | 22 +++++++--------------- test/e2e/evm_test_common.go | 4 +++- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/test/e2e/evm_spamoor_smoke_test.go b/test/e2e/evm_spamoor_smoke_test.go index 7a24ea362..144792880 100644 --- a/test/e2e/evm_spamoor_smoke_test.go +++ b/test/e2e/evm_spamoor_smoke_test.go @@ -61,7 +61,7 @@ func TestSpamoorSmoke(t *testing.T) { t.Log("Sequencer node is up") // Start Spamoor within the same Docker network, targeting reth internal RPC. - ni, err := env.RethNode.GetNetworkInfo(context.Background()) + ni, err := env.RethNode.GetNetworkInfo(ctx) require.NoError(t, err, "failed to get network info") internalRPC := "http://" + ni.Internal.RPCAddress() @@ -78,7 +78,6 @@ func TestSpamoorSmoke(t *testing.T) { WithRPCHosts(internalRPC). WithPrivateKey(TestPrivateKey) - ctx = t.Context() spNode, err := spBuilder.Build(ctx) require.NoError(t, err, "failed to build sp node") @@ -136,7 +135,8 @@ func TestSpamoorSmoke(t *testing.T) { t.Cleanup(func() { _ = api.DeleteSpammer(idToDelete) }) } - // Allow additional time to accumulate activity. + // allow spamoor enough time to generate transaction throughput + // so that the expected tracing spans appear in Jaeger. time.Sleep(60 * time.Second) // Fetch parsed metrics and print a concise summary. @@ -154,6 +154,7 @@ func TestSpamoorSmoke(t *testing.T) { var peerCountHex string require.NoError(t, rpcCli.CallContext(ctx, &peerCountHex, "net_peerCount")) t.Logf("reth head: %d -> %d, net_peerCount=%s", h1, h2, strings.TrimSpace(peerCountHex)) + require.Greater(t, h2, h1, "reth head should have advanced") // Verify Jaeger received traces from ev-node. // Service name is set above via --evnode.instrumentation.tracing_service_name "ev-node-smoke". @@ -207,18 +208,9 @@ func TestSpamoorSmoke(t *testing.T) { require.Contains(t, opNames, name, "expected span %q not found in ev-node-smoke traces", name) } - // assert expected ev-reth span names are present. - expectedRethSpans := []string{ - "Storage trie", - "cache_for", - } - rethOpNames := make(map[string]struct{}, len(evRethSpans)) - for _, s := range evRethSpans { - rethOpNames[s.operationName] = struct{}{} - } - for _, name := range expectedRethSpans { - require.Contains(t, rethOpNames, name, "expected span %q not found in ev-reth traces", name) - } + // ev-reth span names are internal to the Rust OTLP exporter and may change + // across versions, so we only assert that spans were collected at all. + require.NotEmpty(t, evRethSpans, "expected at least one span from ev-reth") require.Greater(t, sent, float64(0), "at least one transaction should have been sent") require.Zero(t, fail, "no transactions should have failed") diff --git a/test/e2e/evm_test_common.go b/test/e2e/evm_test_common.go index b81823b15..5ddaf935b 100644 --- a/test/e2e/evm_test_common.go +++ b/test/e2e/evm_test_common.go @@ -466,7 +466,7 @@ func setupFullNode(t testing.TB, sut *SystemUnderTest, fullNodeHome, sequencerHo } // Global nonce counter to ensure unique nonces across multiple transaction submissions -var globalNonce uint64 = 0 +var globalNonce uint64 // submitTransactionAndGetBlockNumber submits a transaction to the sequencer and returns inclusion details. // This function: @@ -593,6 +593,7 @@ func setupCommonEVMEnv(t testing.TB, sut *SystemUnderTest, client tastoratypes.T GenesisHash: genesisHash, Endpoints: dynEndpoints, RethNode: rethNode, + FullNode: rethFn, } } @@ -605,6 +606,7 @@ type EVMEnv struct { GenesisHash string Endpoints *TestEndpoints RethNode *reth.Node + FullNode *reth.Node } // checkBlockInfoAt retrieves block information at a specific height including state root. From ac17c37eaa4151d18ff725e608df3187a4934efa Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 24 Feb 2026 09:07:15 +0000 Subject: [PATCH 09/11] chore: removed unnessesdary test --- test/e2e/evm_spamoor_smoke_test.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/test/e2e/evm_spamoor_smoke_test.go b/test/e2e/evm_spamoor_smoke_test.go index 144792880..84e021b78 100644 --- a/test/e2e/evm_spamoor_smoke_test.go +++ b/test/e2e/evm_spamoor_smoke_test.go @@ -145,17 +145,6 @@ func TestSpamoorSmoke(t *testing.T) { sent := sumCounter(metrics["spamoor_transactions_sent_total"]) fail := sumCounter(metrics["spamoor_transactions_failed_total"]) - // Probe ev-reth via JSON-RPC as proxy metrics: head height should advance; peer count should be >= 0. - h1, err := ethCli.BlockNumber(ctx) - require.NoError(t, err, "failed to query initial block number") - time.Sleep(5 * time.Second) - h2, err := ethCli.BlockNumber(ctx) - require.NoError(t, err, "failed to query subsequent block number") - var peerCountHex string - require.NoError(t, rpcCli.CallContext(ctx, &peerCountHex, "net_peerCount")) - t.Logf("reth head: %d -> %d, net_peerCount=%s", h1, h2, strings.TrimSpace(peerCountHex)) - require.Greater(t, h2, h1, "reth head should have advanced") - // Verify Jaeger received traces from ev-node. // Service name is set above via --evnode.instrumentation.tracing_service_name "ev-node-smoke". traceCtx, cancel := context.WithTimeout(ctx, 3*time.Minute) @@ -210,6 +199,7 @@ func TestSpamoorSmoke(t *testing.T) { // ev-reth span names are internal to the Rust OTLP exporter and may change // across versions, so we only assert that spans were collected at all. + // TODO: check for more specific spans once implemented. require.NotEmpty(t, evRethSpans, "expected at least one span from ev-reth") require.Greater(t, sent, float64(0), "at least one transaction should have been sent") From 754e52a24a3f2fd3fc06f687eaf98aa1cc6f1dca Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 24 Feb 2026 09:26:39 +0000 Subject: [PATCH 10/11] chore: removed unused imports --- test/e2e/evm_spamoor_smoke_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/e2e/evm_spamoor_smoke_test.go b/test/e2e/evm_spamoor_smoke_test.go index 84e021b78..377c45e3c 100644 --- a/test/e2e/evm_spamoor_smoke_test.go +++ b/test/e2e/evm_spamoor_smoke_test.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" "path/filepath" - "strings" "testing" "time" @@ -65,11 +64,6 @@ func TestSpamoorSmoke(t *testing.T) { require.NoError(t, err, "failed to get network info") internalRPC := "http://" + ni.Internal.RPCAddress() - // Preferred typed clients from tastora's reth node helpers - ethCli, err := env.RethNode.GetEthClient(ctx) - require.NoError(t, err, "failed to get ethclient") - rpcCli, err := env.RethNode.GetRPCClient(ctx) - require.NoError(t, err, "failed to get rpc client") spBuilder := spamoor.NewNodeBuilder(t.Name()). WithDockerClient(env.RethNode.DockerClient). From 926b8823cc8a98273fdc10f2d68855e40b448298 Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 24 Feb 2026 10:01:54 +0000 Subject: [PATCH 11/11] chore: removed non-existant trace --- test/e2e/evm_spamoor_smoke_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/evm_spamoor_smoke_test.go b/test/e2e/evm_spamoor_smoke_test.go index 377c45e3c..ca172948a 100644 --- a/test/e2e/evm_spamoor_smoke_test.go +++ b/test/e2e/evm_spamoor_smoke_test.go @@ -170,7 +170,6 @@ func TestSpamoorSmoke(t *testing.T) { "BlockExecutor.ProduceBlock", "BlockExecutor.ApplyBlock", "BlockExecutor.CreateBlock", - "BlockExecutor.ValidateBlock", "BlockExecutor.RetrieveBatch", "Executor.ExecuteTxs", "Executor.SetFinal",