Skip to content

Commit 7eb9c59

Browse files
committed
add da posting integration test
1 parent 4a610cb commit 7eb9c59

File tree

1 file changed

+361
-0
lines changed

1 file changed

+361
-0
lines changed
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
package e2e
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"encoding/base64"
7+
"fmt"
8+
"io"
9+
"net"
10+
"net/http"
11+
"os"
12+
"strings"
13+
"testing"
14+
"time"
15+
16+
cosmosmath "cosmossdk.io/math"
17+
libshare "github.com/celestiaorg/go-square/v3/share"
18+
tastoradocker "github.com/celestiaorg/tastora/framework/docker"
19+
"github.com/celestiaorg/tastora/framework/docker/container"
20+
tastoracosmos "github.com/celestiaorg/tastora/framework/docker/cosmos"
21+
tastorada "github.com/celestiaorg/tastora/framework/docker/dataavailability"
22+
"github.com/celestiaorg/tastora/framework/docker/evstack"
23+
"github.com/celestiaorg/tastora/framework/testutil/wait"
24+
tastoratypes "github.com/celestiaorg/tastora/framework/types"
25+
sdk "github.com/cosmos/cosmos-sdk/types"
26+
"github.com/cosmos/cosmos-sdk/types/module/testutil"
27+
"github.com/cosmos/cosmos-sdk/x/auth"
28+
"github.com/cosmos/cosmos-sdk/x/bank"
29+
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
30+
"github.com/cosmos/cosmos-sdk/x/gov"
31+
"github.com/cosmos/ibc-go/v8/modules/apps/transfer"
32+
blobrpc "github.com/evstack/ev-node/pkg/da/jsonrpc"
33+
"github.com/stretchr/testify/require"
34+
)
35+
36+
// TestEvNode_PostsToDA spins up celestia-app, a celestia bridge node and an
37+
// EV Node (aggregator) via tastora, then verifies the EV Node actually posts
38+
// data to DA by confirming blobs exist in the ev-data namespace via the DA
39+
// JSON-RPC client.
40+
func TestEvNode_PostsToDA(t *testing.T) {
41+
if testing.Short() {
42+
t.Skip("skip integration in short mode")
43+
}
44+
45+
configurePrefixOnce.Do(configureCelestiaBech32Prefix)
46+
47+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
48+
defer cancel()
49+
50+
uniqueTestName := fmt.Sprintf("%s-%d", t.Name(), time.Now().UnixNano())
51+
52+
dockerClient, networkID := tastoradocker.Setup(t)
53+
t.Cleanup(tastoradocker.Cleanup(t, dockerClient))
54+
55+
encCfg := testutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, bank.AppModuleBasic{}, transfer.AppModuleBasic{}, gov.AppModuleBasic{})
56+
57+
// 1) Start celestia-app chain
58+
chainImage := container.Image{
59+
Repository: "ghcr.io/celestiaorg/celestia-app",
60+
Version: "v5.0.10",
61+
UIDGID: "10001:10001",
62+
}
63+
64+
chainBuilder := tastoracosmos.NewChainBuilderWithTestName(t, uniqueTestName).
65+
WithDockerClient(dockerClient).
66+
WithDockerNetworkID(networkID).
67+
WithImage(chainImage).
68+
WithEncodingConfig(&encCfg).
69+
WithAdditionalStartArgs(
70+
"--force-no-bbr",
71+
"--grpc.enable",
72+
"--grpc.address", "0.0.0.0:9090",
73+
"--rpc.grpc_laddr=tcp://0.0.0.0:9098",
74+
"--rpc.laddr=tcp://0.0.0.0:26657",
75+
"--timeout-commit", "1s",
76+
"--minimum-gas-prices", "0utia",
77+
).
78+
WithNode(tastoracosmos.NewChainNodeConfigBuilder().Build())
79+
80+
chain, err := chainBuilder.Build(ctx)
81+
require.NoError(t, err, "build celestia-app chain")
82+
require.NoError(t, chain.Start(ctx), "start celestia-app chain")
83+
84+
chainID := chain.GetChainID()
85+
genesisHash, err := fetchGenesisHash(ctx, chain)
86+
require.NoError(t, err, "genesis hash")
87+
88+
chainNetInfo, err := chain.GetNodes()[0].GetNetworkInfo(ctx)
89+
require.NoError(t, err, "chain network info")
90+
coreHost := chainNetInfo.Internal.Hostname
91+
92+
// 2) Start celestia-node (bridge)
93+
daImage := container.Image{
94+
Repository: "ghcr.io/celestiaorg/celestia-node",
95+
Version: "v0.28.4-mocha",
96+
UIDGID: "10001:10001",
97+
}
98+
99+
daNetwork, err := tastorada.NewNetworkBuilderWithTestName(t, uniqueTestName).
100+
WithDockerClient(dockerClient).
101+
WithDockerNetworkID(networkID).
102+
WithImage(daImage).
103+
WithNodes(tastorada.NewNodeBuilder().WithNodeType(tastoratypes.BridgeNode).Build()).
104+
Build(ctx)
105+
require.NoError(t, err, "build da network")
106+
107+
bridge := daNetwork.GetBridgeNodes()[0]
108+
err = bridge.Start(ctx,
109+
tastorada.WithChainID(chainID),
110+
tastorada.WithAdditionalStartArguments(
111+
"--p2p.network", chainID,
112+
"--core.ip", coreHost,
113+
"--rpc.addr", "0.0.0.0",
114+
),
115+
tastorada.WithEnvironmentVariables(map[string]string{
116+
"CELESTIA_CUSTOM": tastoratypes.BuildCelestiaCustomEnvVar(chainID, genesisHash, ""),
117+
"P2P_NETWORK": chainID,
118+
}),
119+
)
120+
require.NoError(t, err, "start bridge node")
121+
122+
bridgeWallet, err := bridge.GetWallet()
123+
require.NoError(t, err, "bridge wallet")
124+
125+
// 3) Wait for chain to be live then fund bridge wallet
126+
validatorNode := chain.GetNodes()[0].(*tastoracosmos.ChainNode)
127+
128+
err = wait.ForCondition(ctx, 2*time.Minute, time.Second, func() (bool, error) {
129+
c, err := validatorNode.GetRPCClient()
130+
if err != nil {
131+
return false, nil
132+
}
133+
if _, err := c.Status(ctx); err != nil {
134+
return false, nil
135+
}
136+
h, err := validatorNode.Height(ctx)
137+
if err != nil {
138+
return false, nil
139+
}
140+
return h >= 3, nil
141+
})
142+
require.NoError(t, err, "validator RPC ready")
143+
144+
// fund the bridge wallet via CLI to avoid JSON-RPC decoding issues
145+
faucetKey := "faucet"
146+
sendAmt := sdk.NewInt64Coin(chain.Config.Denom, 5_000_000_000)
147+
rpcNode := fmt.Sprintf("tcp://%s:26657", coreHost)
148+
149+
cmd := []string{
150+
validatorNode.BinaryName,
151+
"tx", "bank", "send",
152+
faucetKey,
153+
bridgeWallet.FormattedAddress,
154+
sendAmt.String(),
155+
"--chain-id", chainID,
156+
"--home", validatorNode.HomeDir(),
157+
"--keyring-backend", "test",
158+
"--node", rpcNode,
159+
"--fees", fmt.Sprintf("1000%s", chain.Config.Denom),
160+
"--broadcast-mode", "sync",
161+
"--yes",
162+
}
163+
stdout, stderr, err := validatorNode.Exec(ctx, cmd, nil)
164+
require.NoErrorf(t, err, "fund bridge wallet via CLI: %s", string(stderr))
165+
require.Contains(t, string(stdout), "code: 0", "bank send succeeded")
166+
167+
bankQuery := banktypes.NewQueryClient(chain.GetNode().GrpcConn)
168+
err = wait.ForCondition(ctx, 2*time.Minute, time.Second, func() (bool, error) {
169+
bal, err := bankQuery.Balance(ctx, &banktypes.QueryBalanceRequest{
170+
Address: bridgeWallet.FormattedAddress,
171+
Denom: chain.Config.Denom,
172+
})
173+
if err != nil {
174+
return false, nil
175+
}
176+
return bal.Balance != nil && bal.Balance.Amount.GT(cosmosmath.NewInt(0)), nil
177+
})
178+
require.NoError(t, err, "bridge wallet funded")
179+
180+
bridgeNetInfo, err := bridge.GetNetworkInfo(ctx)
181+
require.NoError(t, err, "bridge network info")
182+
183+
// wait for celestia-node RPC port to become reachable
184+
err = wait.ForCondition(ctx, 2*time.Minute, time.Second, func() (bool, error) {
185+
hostPort := fmt.Sprintf("127.0.0.1:%s", bridgeNetInfo.External.Ports.RPC)
186+
conn, err := net.DialTimeout("tcp", hostPort, 2*time.Second)
187+
if err != nil {
188+
return false, nil
189+
}
190+
_ = conn.Close()
191+
return true, nil
192+
})
193+
require.NoError(t, err, "bridge RPC reachable")
194+
195+
// 4) Start EV Node (aggregator) pointing at DA
196+
evNodeChain, err := evstack.NewChainBuilderWithTestName(t, uniqueTestName).
197+
WithChainID("evchain-test").
198+
WithBinaryName("testapp").
199+
WithAggregatorPassphrase("12345678").
200+
WithImage(getEvNodeImage()).
201+
WithDockerClient(dockerClient).
202+
WithDockerNetworkID(networkID).
203+
WithNode(evstack.NewNodeBuilder().WithAggregator(true).Build()).
204+
Build(ctx)
205+
require.NoError(t, err, "build ev node chain")
206+
207+
evNode := evNodeChain.GetNodes()[0]
208+
require.NoError(t, evNode.Init(ctx), "ev node init")
209+
210+
authToken, err := bridge.GetAuthToken()
211+
require.NoError(t, err, "bridge auth token")
212+
213+
daAddress := fmt.Sprintf("http://%s", bridgeNetInfo.Internal.RPCAddress())
214+
headerNamespaceStr := "ev-header"
215+
dataNamespaceStr := "ev-data"
216+
dataNamespaceHash := sha256.Sum256([]byte(dataNamespaceStr))
217+
dataNamespace := libshare.MustNewV0Namespace(dataNamespaceHash[:10])
218+
219+
require.NoError(t, evNode.Start(ctx,
220+
"--evnode.da.address", daAddress,
221+
"--evnode.da.auth_token", authToken,
222+
"--evnode.rpc.address", "0.0.0.0:7331",
223+
"--evnode.da.namespace", headerNamespaceStr,
224+
"--evnode.da.data_namespace", dataNamespaceStr,
225+
"--kv-endpoint", "0.0.0.0:8080",
226+
), "start ev node")
227+
228+
evNetInfo, err := evNode.GetNetworkInfo(ctx)
229+
require.NoError(t, err, "ev node network info")
230+
httpAddr := evNetInfo.External.HTTPAddress()
231+
require.NotEmpty(t, httpAddr)
232+
parts := strings.Split(httpAddr, ":")
233+
require.Len(t, parts, 2)
234+
host, port := parts[0], parts[1]
235+
if host == "0.0.0.0" {
236+
host = "localhost"
237+
}
238+
cli, err := newHTTPClient(host, port)
239+
require.NoError(t, err)
240+
241+
// 5) Submit a tx to ev-node to trigger block production + DA posting
242+
key, value := "da-key", "da-value"
243+
_, err = cli.Post(ctx, "/tx", key, value)
244+
require.NoError(t, err)
245+
246+
require.Eventually(t, func() bool {
247+
res, err := cli.Get(ctx, "/kv?key="+key)
248+
if err != nil {
249+
return false
250+
}
251+
return string(res) == value
252+
}, 30*time.Second, time.Second, "ev-node should serve the kv value")
253+
254+
// 6) Assert data landed on DA via celestia-node blob RPC (namespace ev-data)
255+
daRPCAddr := fmt.Sprintf("http://127.0.0.1:%s", bridgeNetInfo.External.Ports.RPC)
256+
daClient, err := blobrpc.NewClient(ctx, daRPCAddr, authToken, "")
257+
require.NoError(t, err, "new da client")
258+
defer daClient.Close()
259+
260+
validator := chain.GetNodes()[0].(*tastoracosmos.ChainNode)
261+
tmRPC, err := validator.GetRPCClient()
262+
require.NoError(t, err, "tm rpc client")
263+
264+
var pfbHeight int64
265+
require.Eventually(t, func() bool {
266+
res, err := tmRPC.TxSearch(ctx, "message.action='/celestia.blob.v1.MsgPayForBlobs'", false, nil, nil, "desc")
267+
if err != nil || len(res.Txs) == 0 {
268+
return false
269+
}
270+
dataNSB64 := base64.StdEncoding.EncodeToString(dataNamespace.Bytes())
271+
for _, tx := range res.Txs {
272+
if tx.TxResult.Code != 0 {
273+
continue
274+
}
275+
for _, ev := range tx.TxResult.Events {
276+
if ev.Type != "celestia.blob.v1.EventPayForBlobs" {
277+
continue
278+
}
279+
for _, attr := range ev.Attributes {
280+
if string(attr.Key) == "namespaces" && strings.Contains(string(attr.Value), dataNSB64) {
281+
pfbHeight = tx.Height
282+
return true
283+
}
284+
}
285+
}
286+
}
287+
return false
288+
}, 2*time.Minute, 5*time.Second, "expected a PayForBlobs tx on celestia-app")
289+
290+
require.Eventually(t, func() bool {
291+
if pfbHeight == 0 {
292+
return false
293+
}
294+
for h := pfbHeight; h <= pfbHeight+10; h++ {
295+
blobs, err := daClient.Blob.GetAll(ctx, uint64(h), []libshare.Namespace{dataNamespace})
296+
if err != nil {
297+
t.Logf("GetAll data height=%d err=%v", h, err)
298+
continue
299+
}
300+
if len(blobs) > 0 {
301+
return true
302+
}
303+
}
304+
return false
305+
}, 6*time.Minute, 5*time.Second, "expected blob in DA for namespace ev-data")
306+
}
307+
308+
// newHTTPClient is a small helper to avoid importing the docker_e2e client.
309+
func newHTTPClient(host, port string) (*httpClient, error) {
310+
return &httpClient{baseURL: fmt.Sprintf("http://%s:%s", host, port)}, nil
311+
}
312+
313+
type httpClient struct {
314+
baseURL string
315+
}
316+
317+
func (c *httpClient) Get(ctx context.Context, path string) ([]byte, error) {
318+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
319+
if err != nil {
320+
return nil, err
321+
}
322+
resp, err := http.DefaultClient.Do(req)
323+
if err != nil {
324+
return nil, err
325+
}
326+
defer resp.Body.Close()
327+
if resp.StatusCode != http.StatusOK {
328+
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
329+
}
330+
return io.ReadAll(resp.Body)
331+
}
332+
333+
func (c *httpClient) Post(ctx context.Context, path, key, value string) ([]byte, error) {
334+
body := strings.NewReader(fmt.Sprintf("%s=%s", key, value))
335+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, body)
336+
if err != nil {
337+
return nil, err
338+
}
339+
req.Header.Set("Content-Type", "text/plain")
340+
resp, err := http.DefaultClient.Do(req)
341+
if err != nil {
342+
return nil, err
343+
}
344+
defer resp.Body.Close()
345+
return io.ReadAll(resp.Body)
346+
}
347+
348+
// getEvNodeImage resolves the EV Node image to use for the test.
349+
// Falls back to EV_NODE_IMAGE_REPO:EV_NODE_IMAGE_TAG or evstack:local-dev.
350+
func getEvNodeImage() container.Image {
351+
repo := strings.TrimSpace(getEnvDefault("EV_NODE_IMAGE_REPO", "evstack"))
352+
tag := strings.TrimSpace(getEnvDefault("EV_NODE_IMAGE_TAG", "local-dev"))
353+
return container.NewImage(repo, tag, "10001:10001")
354+
}
355+
356+
func getEnvDefault(key, def string) string {
357+
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
358+
return v
359+
}
360+
return def
361+
}

0 commit comments

Comments
 (0)