|
| 1 | +//go:build fibre |
| 2 | + |
| 3 | +package cnfibertest |
| 4 | + |
| 5 | +import ( |
| 6 | + "context" |
| 7 | + "crypto/rand" |
| 8 | + "net" |
| 9 | + "testing" |
| 10 | + "time" |
| 11 | + |
| 12 | + "github.com/cosmos/cosmos-sdk/crypto/hd" |
| 13 | + "github.com/cosmos/cosmos-sdk/crypto/keyring" |
| 14 | + "github.com/cristalhq/jwt/v5" |
| 15 | + "github.com/stretchr/testify/require" |
| 16 | + "go.uber.org/fx" |
| 17 | + |
| 18 | + appfibre "github.com/celestiaorg/celestia-app/v8/fibre" |
| 19 | + |
| 20 | + "github.com/celestiaorg/celestia-node/api/client" |
| 21 | + "github.com/celestiaorg/celestia-node/api/rpc/perms" |
| 22 | + "github.com/celestiaorg/celestia-node/nodebuilder" |
| 23 | + "github.com/celestiaorg/celestia-node/nodebuilder/node" |
| 24 | + "github.com/celestiaorg/celestia-node/nodebuilder/p2p" |
| 25 | + stateapi "github.com/celestiaorg/celestia-node/nodebuilder/state" |
| 26 | +) |
| 27 | + |
| 28 | +// Bridge bundles an in-process celestia-node bridge node and the admin |
| 29 | +// JWT that grants it authenticated RPC access. The adapter's ReadConfig |
| 30 | +// needs both the address and the token for Blob.Subscribe to work. |
| 31 | +type Bridge struct { |
| 32 | + Node *nodebuilder.Node |
| 33 | + AdminToken string |
| 34 | +} |
| 35 | + |
| 36 | +// RPCAddr returns a WebSocket URL the adapter uses in |
| 37 | +// Config.ReadConfig.BridgeDAAddr. WebSocket (not HTTP) is required |
| 38 | +// because Blob.Subscribe returns a channel; go-jsonrpc only supports |
| 39 | +// channel-returning methods over a streaming transport. |
| 40 | +func (b *Bridge) RPCAddr() string { |
| 41 | + return "ws://" + b.Node.RPCServer.ListenAddr() |
| 42 | +} |
| 43 | + |
| 44 | +// StartBridge brings up an in-process celestia-node bridge connected to |
| 45 | +// the Network's consensus gRPC endpoint. Mirrors celestia-node's |
| 46 | +// api/client test helpers so TestShowcase has a real JSON-RPC server for |
| 47 | +// Blob.Subscribe. |
| 48 | +func StartBridge(t *testing.T, ctx context.Context, network *Network) *Bridge { |
| 49 | + t.Helper() |
| 50 | + |
| 51 | + cfg := nodebuilder.DefaultConfig(node.Bridge) |
| 52 | + |
| 53 | + ip, port, err := net.SplitHostPort(network.ConsensusGRPCAddr()) |
| 54 | + require.NoError(t, err, "splitting consensus gRPC addr") |
| 55 | + cfg.Core.IP = ip |
| 56 | + cfg.Core.Port = port |
| 57 | + // Pin the bridge RPC to an ephemeral port; the test discovers it via |
| 58 | + // Node.RPCServer.ListenAddr() after Start. |
| 59 | + cfg.RPC.Port = "0" |
| 60 | + |
| 61 | + tempDir := t.TempDir() |
| 62 | + store := nodebuilder.MockStore(t, cfg) |
| 63 | + |
| 64 | + auth, adminToken := bridgeAuth(t) |
| 65 | + kr := bridgeKeyring(t, tempDir) |
| 66 | + |
| 67 | + bn, err := nodebuilder.New(node.Bridge, p2p.Private, store, |
| 68 | + auth, |
| 69 | + stateapi.WithKeyring(kr), |
| 70 | + stateapi.WithKeyName(stateapi.AccountName(bridgeSigningKey)), |
| 71 | + fx.Replace(node.StorePath(tempDir)), |
| 72 | + ) |
| 73 | + require.NoError(t, err, "constructing bridge node") |
| 74 | + |
| 75 | + require.NoError(t, bn.Start(ctx), "starting bridge node") |
| 76 | + t.Cleanup(func() { |
| 77 | + stopCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second) |
| 78 | + defer cancel() |
| 79 | + _ = bn.Stop(stopCtx) |
| 80 | + }) |
| 81 | + |
| 82 | + return &Bridge{ |
| 83 | + Node: bn, |
| 84 | + AdminToken: adminToken, |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +// bridgeSigningKey is the keyring account the bridge uses for its own |
| 89 | +// tx submissions. Distinct from the client's account so the two keyrings |
| 90 | +// don't collide. |
| 91 | +const bridgeSigningKey = "bridge-signer" |
| 92 | + |
| 93 | +func bridgeKeyring(t *testing.T, tempDir string) keyring.Keyring { |
| 94 | + t.Helper() |
| 95 | + |
| 96 | + kr, err := client.KeyringWithNewKey(client.KeyringConfig{ |
| 97 | + KeyName: bridgeSigningKey, |
| 98 | + BackendName: keyring.BackendTest, |
| 99 | + }, tempDir) |
| 100 | + require.NoError(t, err, "creating bridge keyring") |
| 101 | + |
| 102 | + // The Fibre module on the bridge expects a key under |
| 103 | + // appfibre.DefaultKeyName to exist, even though our client never uses |
| 104 | + // the bridge's Fibre module for Upload/Download. Without it, |
| 105 | + // fx.Start fails during fibre module wiring. |
| 106 | + _, _, err = kr.NewMnemonic( |
| 107 | + appfibre.DefaultKeyName, |
| 108 | + keyring.English, "", "", hd.Secp256k1, |
| 109 | + ) |
| 110 | + require.NoError(t, err, "provisioning bridge fibre key") |
| 111 | + return kr |
| 112 | +} |
| 113 | + |
| 114 | +// bridgeAuth creates an HS256 JWT signer pair and an admin token. The |
| 115 | +// returned fx option injects the signer/verifier into the node; the |
| 116 | +// token is what the adapter puts in ReadConfig.DAAuthToken. |
| 117 | +func bridgeAuth(t *testing.T) (fx.Option, string) { |
| 118 | + t.Helper() |
| 119 | + |
| 120 | + key := make([]byte, 32) |
| 121 | + _, err := rand.Read(key) |
| 122 | + require.NoError(t, err, "rand.Read jwt key") |
| 123 | + |
| 124 | + signer, err := jwt.NewSignerHS(jwt.HS256, key) |
| 125 | + require.NoError(t, err) |
| 126 | + verifier, err := jwt.NewVerifierHS(jwt.HS256, key) |
| 127 | + require.NoError(t, err) |
| 128 | + |
| 129 | + token, err := perms.NewTokenWithPerms(signer, perms.AllPerms) |
| 130 | + require.NoError(t, err) |
| 131 | + |
| 132 | + return fx.Decorate(func() (jwt.Signer, jwt.Verifier, error) { |
| 133 | + return signer, verifier, nil |
| 134 | + }), string(token) |
| 135 | +} |
0 commit comments