|
| 1 | +//go:build evm |
| 2 | + |
| 3 | +package e2e |
| 4 | + |
| 5 | +import ( |
| 6 | + "context" |
| 7 | + "fmt" |
| 8 | + "net/http" |
| 9 | + "path/filepath" |
| 10 | + "testing" |
| 11 | + "time" |
| 12 | + |
| 13 | + spamoor "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" |
| 14 | + dto "github.com/prometheus/client_model/go" |
| 15 | + "github.com/stretchr/testify/require" |
| 16 | +) |
| 17 | + |
| 18 | +// TestSpamoorSmoke spins up reth + sequencer and a Spamoor node, starts a few |
| 19 | +// basic spammers, waits briefly, then prints a concise metrics summary. |
| 20 | +func TestSpamoorSmoke(t *testing.T) { |
| 21 | + t.Parallel() |
| 22 | + |
| 23 | + sut := NewSystemUnderTest(t) |
| 24 | + // Bring up reth + local DA and start sequencer with default settings. |
| 25 | + seqJWT, _, genesisHash, endpoints, rethNode := setupCommonEVMTest(t, sut, false) |
| 26 | + sequencerHome := filepath.Join(t.TempDir(), "sequencer") |
| 27 | + |
| 28 | + // In-process OTLP/HTTP collector to capture ev-node spans. |
| 29 | + collector := newOTLPCollector(t) |
| 30 | + t.Cleanup(collector.close) |
| 31 | + |
| 32 | + // Start sequencer with tracing to our collector. |
| 33 | + setupSequencerNode(t, sut, sequencerHome, seqJWT, genesisHash, endpoints, |
| 34 | + "--evnode.instrumentation.tracing=true", |
| 35 | + "--evnode.instrumentation.tracing_endpoint", collector.endpoint(), |
| 36 | + "--evnode.instrumentation.tracing_sample_rate", "1.0", |
| 37 | + "--evnode.instrumentation.tracing_service_name", "ev-node-smoke", |
| 38 | + ) |
| 39 | + t.Log("Sequencer node is up") |
| 40 | + |
| 41 | + // Start Spamoor within the same Docker network, targeting reth internal RPC. |
| 42 | + ni, err := rethNode.GetNetworkInfo(context.Background()) |
| 43 | + require.NoError(t, err, "failed to get network info") |
| 44 | + |
| 45 | + internalRPC := "http://" + ni.Internal.RPCAddress() |
| 46 | + |
| 47 | + spBuilder := spamoor.NewNodeBuilder(t.Name()). |
| 48 | + WithDockerClient(rethNode.DockerClient). |
| 49 | + WithDockerNetworkID(rethNode.NetworkID). |
| 50 | + WithLogger(rethNode.Logger). |
| 51 | + WithRPCHosts(internalRPC). |
| 52 | + WithPrivateKey(TestPrivateKey) |
| 53 | + |
| 54 | + ctx := context.Background() |
| 55 | + spNode, err := spBuilder.Build(ctx) |
| 56 | + require.NoError(t, err, "failed to build sp node") |
| 57 | + |
| 58 | + t.Cleanup(func() { _ = spNode.Remove(context.Background()) }) |
| 59 | + require.NoError(t, spNode.Start(ctx), "failed to start spamoor node") |
| 60 | + |
| 61 | + // Wait for daemon readiness. |
| 62 | + spInfo, err := spNode.GetNetworkInfo(ctx) |
| 63 | + require.NoError(t, err, "failed to get network info") |
| 64 | + |
| 65 | + apiAddr := "http://127.0.0.1:" + spInfo.External.Ports.HTTP |
| 66 | + requireHTTP(t, apiAddr+"/api/spammers", 30*time.Second) |
| 67 | + api := spNode.API() |
| 68 | + |
| 69 | + // Basic scenarios (structs that YAML-marshal into the daemon config). |
| 70 | + eoatx := map[string]any{ |
| 71 | + "throughput": 100, |
| 72 | + "total_count": 3000, |
| 73 | + "max_pending": 4000, |
| 74 | + "max_wallets": 300, |
| 75 | + "amount": 100, |
| 76 | + "random_amount": true, |
| 77 | + "random_target": true, |
| 78 | + "base_fee": 20, // gwei |
| 79 | + "tip_fee": 2, // gwei |
| 80 | + "refill_amount": "1000000000000000000", |
| 81 | + "refill_balance": "500000000000000000", |
| 82 | + "refill_interval": 600, |
| 83 | + } |
| 84 | + |
| 85 | + gasburner := map[string]any{ |
| 86 | + "throughput": 25, |
| 87 | + "total_count": 2000, |
| 88 | + "max_pending": 8000, |
| 89 | + "max_wallets": 500, |
| 90 | + "gas_units_to_burn": 3000000, |
| 91 | + "base_fee": 20, |
| 92 | + "tip_fee": 5, |
| 93 | + "rebroadcast": 5, |
| 94 | + "refill_amount": "5000000000000000000", |
| 95 | + "refill_balance": "2000000000000000000", |
| 96 | + "refill_interval": 300, |
| 97 | + } |
| 98 | + |
| 99 | + var ids []int |
| 100 | + id, err := api.CreateSpammer("smoke-eoatx", spamoor.ScenarioEOATX, eoatx, true) |
| 101 | + require.NoError(t, err, "failed to create eoatx spammer") |
| 102 | + ids = append(ids, id) |
| 103 | + id, err = api.CreateSpammer("smoke-gasburner", spamoor.ScenarioGasBurnerTX, gasburner, true) |
| 104 | + require.NoError(t, err, "failed to create gasburner spammer") |
| 105 | + ids = append(ids, id) |
| 106 | + |
| 107 | + for _, id := range ids { |
| 108 | + idToDelete := id |
| 109 | + t.Cleanup(func() { _ = api.DeleteSpammer(idToDelete) }) |
| 110 | + } |
| 111 | + |
| 112 | + // Allow additional time to accumulate activity. |
| 113 | + time.Sleep(60 * time.Second) |
| 114 | + |
| 115 | + // Fetch parsed metrics and print a concise summary. |
| 116 | + if mfs, err := api.GetMetrics(); err == nil && mfs != nil { |
| 117 | + sent := sumCounter(mfs["spamoor_transactions_sent_total"]) |
| 118 | + fail := sumCounter(mfs["spamoor_transactions_failed_total"]) |
| 119 | + pend := sumGauge(mfs["spamoor_pending_transactions"]) |
| 120 | + gas := sumCounter(mfs["spamoor_block_gas_usage"]) |
| 121 | + t.Logf("Spamoor summary: sent=%.0f failed=%.0f pending=%.0f block_gas=%.0f", sent, fail, pend, gas) |
| 122 | + } else { |
| 123 | + t.Logf("metrics unavailable or parse error: %v", err) |
| 124 | + } |
| 125 | + |
| 126 | + time.Sleep(2 * time.Second) |
| 127 | + printCollectedTraceReport(t, collector) |
| 128 | + |
| 129 | + // TODO: test should pass / fail based on results |
| 130 | +} |
| 131 | + |
| 132 | +// --- helpers --- |
| 133 | + |
| 134 | +func requireHTTP(t *testing.T, url string, timeout time.Duration) { |
| 135 | + t.Helper() |
| 136 | + client := &http.Client{Timeout: 200 * time.Millisecond} |
| 137 | + deadline := time.Now().Add(timeout) |
| 138 | + var lastErr error |
| 139 | + for time.Now().Before(deadline) { |
| 140 | + resp, err := client.Get(url) |
| 141 | + if err == nil { |
| 142 | + _ = resp.Body.Close() |
| 143 | + if resp.StatusCode >= 200 && resp.StatusCode < 300 { |
| 144 | + return |
| 145 | + } |
| 146 | + lastErr = fmt.Errorf("status %d", resp.StatusCode) |
| 147 | + } else { |
| 148 | + lastErr = err |
| 149 | + } |
| 150 | + time.Sleep(100 * time.Millisecond) |
| 151 | + } |
| 152 | + t.Fatalf("daemon not ready at %s: %v", url, lastErr) |
| 153 | +} |
| 154 | + |
| 155 | +// Metric family helpers. |
| 156 | +func sumCounter(f *dto.MetricFamily) float64 { |
| 157 | + if f == nil || f.GetType() != dto.MetricType_COUNTER { |
| 158 | + return 0 |
| 159 | + } |
| 160 | + var sum float64 |
| 161 | + for _, m := range f.GetMetric() { |
| 162 | + if m.GetCounter() != nil && m.GetCounter().Value != nil { |
| 163 | + sum += m.GetCounter().GetValue() |
| 164 | + } |
| 165 | + } |
| 166 | + return sum |
| 167 | +} |
| 168 | +func sumGauge(f *dto.MetricFamily) float64 { |
| 169 | + if f == nil || f.GetType() != dto.MetricType_GAUGE { |
| 170 | + return 0 |
| 171 | + } |
| 172 | + var sum float64 |
| 173 | + for _, m := range f.GetMetric() { |
| 174 | + if m.GetGauge() != nil && m.GetGauge().Value != nil { |
| 175 | + sum += m.GetGauge().GetValue() |
| 176 | + } |
| 177 | + } |
| 178 | + return sum |
| 179 | +} |
0 commit comments