From bab7af8a6b6ed235602c1ba2a79e96f6d8b77e6f Mon Sep 17 00:00:00 2001 From: Yuji Ito Date: Tue, 12 Nov 2024 15:44:13 +0100 Subject: [PATCH] feat(namada): Support Namada test (#1296) --- .codespellrc | 4 +- chain/cosmos/chain_node.go | 2 +- chain/cosmos/sidecar.go | 2 +- chain/ethereum/ethererum_chain.go | 2 +- chain/internal/tendermint/tendermint_node.go | 2 +- chain/namada/namada_chain.go | 1316 ++++++++++++++++++ chain/namada/namada_node.go | 343 +++++ chain/namada/wallet.go | 59 + chain/penumbra/penumbra_app_node.go | 2 +- chain/penumbra/penumbra_client_node.go | 2 +- chain/polkadot/parachain_node.go | 2 +- chain/polkadot/relay_chain_node.go | 2 +- chain/thorchain/sidecar.go | 2 +- chain/thorchain/thornode.go | 2 +- chain/utxo/utxo_chain.go | 2 +- chainfactory.go | 3 + chainspec.go | 2 +- configuredChains.yaml | 14 + dockerutil/container_lifecycle.go | 14 +- dockerutil/setup.go | 92 ++ examples/namada/namada_chain_test.go | 285 ++++ go.mod | 1 + go.sum | 2 + ibc/types.go | 1 + interchain.go | 18 +- relayer/docker.go | 2 +- relayer/hermes/hermes_config.go | 27 +- relayer/hermes/hermes_relayer.go | 17 +- 28 files changed, 2198 insertions(+), 24 deletions(-) create mode 100644 chain/namada/namada_chain.go create mode 100644 chain/namada/namada_node.go create mode 100644 chain/namada/wallet.go create mode 100644 examples/namada/namada_chain_test.go diff --git a/.codespellrc b/.codespellrc index f2350b52f..8b3dfc667 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,4 +1,4 @@ [codespell] skip = *.pulsar.go,*.pb.go,*.pb.gw.go,*.json,*.git,*.bin,*.sum,*.mod,query_test.go,*.sol -ignore-words-list = usera,pres,crate -quiet-level = 3 \ No newline at end of file +ignore-words-list = usera,pres,crate,nam +quiet-level = 3 diff --git a/chain/cosmos/chain_node.go b/chain/cosmos/chain_node.go index 488487a32..4c06365a0 100644 --- a/chain/cosmos/chain_node.go +++ b/chain/cosmos/chain_node.go @@ -1188,7 +1188,7 @@ func (tn *ChainNode) CreateNodeContainer(ctx context.Context) error { tn.log.Info("Port overrides", fields...) } - return tn.containerLifecycle.CreateContainer(ctx, tn.TestName, tn.NetworkID, tn.Image, usingPorts, tn.Bind(), nil, tn.HostName(), cmd, chainCfg.Env, []string{}) + return tn.containerLifecycle.CreateContainer(ctx, tn.TestName, tn.NetworkID, tn.Image, usingPorts, "", tn.Bind(), nil, tn.HostName(), cmd, chainCfg.Env, []string{}) } func (tn *ChainNode) StartContainer(ctx context.Context) error { diff --git a/chain/cosmos/sidecar.go b/chain/cosmos/sidecar.go index e6b3b7fd5..7dcf88a03 100644 --- a/chain/cosmos/sidecar.go +++ b/chain/cosmos/sidecar.go @@ -107,7 +107,7 @@ func (s *SidecarProcess) logger() *zap.Logger { } func (s *SidecarProcess) CreateContainer(ctx context.Context) error { - return s.containerLifecycle.CreateContainer(ctx, s.TestName, s.NetworkID, s.Image, s.ports, s.Bind(), nil, s.HostName(), s.startCmd, s.env, []string{}) + return s.containerLifecycle.CreateContainer(ctx, s.TestName, s.NetworkID, s.Image, s.ports, "", s.Bind(), nil, s.HostName(), s.startCmd, s.env, []string{}) } func (s *SidecarProcess) StartContainer(ctx context.Context) error { diff --git a/chain/ethereum/ethererum_chain.go b/chain/ethereum/ethererum_chain.go index 9c93b5081..7c22e324e 100644 --- a/chain/ethereum/ethererum_chain.go +++ b/chain/ethereum/ethererum_chain.go @@ -159,7 +159,7 @@ func (c *EthereumChain) Start(ctx context.Context, cmd []string, mount []mount.M c.log.Info("Port overrides", fields...) } - err := c.containerLifecycle.CreateContainer(ctx, c.testName, c.networkID, c.cfg.Images[0], usingPorts, c.Bind(), mount, c.HostName(), cmd, nil, []string{}) + err := c.containerLifecycle.CreateContainer(ctx, c.testName, c.networkID, c.cfg.Images[0], usingPorts, "", c.Bind(), mount, c.HostName(), cmd, nil, []string{}) if err != nil { return err } diff --git a/chain/internal/tendermint/tendermint_node.go b/chain/internal/tendermint/tendermint_node.go index 64043ecf6..46f027d37 100644 --- a/chain/internal/tendermint/tendermint_node.go +++ b/chain/internal/tendermint/tendermint_node.go @@ -275,7 +275,7 @@ func (tn *TendermintNode) CreateNodeContainer(ctx context.Context, additionalFla cmd := []string{chainCfg.Bin, "start", "--home", tn.HomeDir()} cmd = append(cmd, additionalFlags...) - return tn.containerLifecycle.CreateContainer(ctx, tn.TestName, tn.NetworkID, tn.Image, sentryPorts, tn.Bind(), nil, tn.HostName(), cmd, nil, []string{}) + return tn.containerLifecycle.CreateContainer(ctx, tn.TestName, tn.NetworkID, tn.Image, sentryPorts, "", tn.Bind(), nil, tn.HostName(), cmd, nil, []string{}) } func (tn *TendermintNode) StopContainer(ctx context.Context) error { diff --git a/chain/namada/namada_chain.go b/chain/namada/namada_chain.go new file mode 100644 index 000000000..66dcf3c3d --- /dev/null +++ b/chain/namada/namada_chain.go @@ -0,0 +1,1316 @@ +package namada + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "io" + stdmath "math" + "net/http" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "go.uber.org/zap" + "golang.org/x/sync/errgroup" + + "cosmossdk.io/math" + + cometbft "github.com/cometbft/cometbft/abci/types" + + "github.com/strangelove-ventures/interchaintest/v8/chain/internal/tendermint" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/testutil" +) + +const ( + NamAddress = "tnam1qxgfw7myv4dh0qna4hq0xdg6lx77fzl7dcem8h7e" + NamTokenDenom = int64(6) + MaspAddress = "tnam1pcqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzmefah" + gasPayerAlias = "gas-payer" +) + +type NamadaChain struct { + log *zap.Logger + testName string + cfg ibc.ChainConfig + NumValidators int + numFullNodes int + Validators NamadaNodes + FullNodes NamadaNodes + + isRunning bool +} + +// New instance of NamadaChain. +func NewNamadaChain(testName string, chainConfig ibc.ChainConfig, numValidators int, numFullNodes int, log *zap.Logger) *NamadaChain { + return &NamadaChain{ + log: log, + testName: testName, + cfg: chainConfig, + NumValidators: numValidators, + numFullNodes: numFullNodes, + } +} + +// Chain config. +func (c *NamadaChain) Config() ibc.ChainConfig { + return c.cfg +} + +// Initialize the chain. +func (c *NamadaChain) Initialize(ctx context.Context, testName string, cli *client.Client, networkID string) error { + chainCfg := c.Config() + for _, image := range chainCfg.Images { + rc, err := cli.ImagePull( + ctx, + image.Repository+":"+image.Version, + types.ImagePullOptions{ + Platform: "amd64", + }) + if err != nil { + c.log.Error("Failed to pull image", + zap.Error(err), + zap.String("repository", image.Repository), + zap.String("tag", image.Version), + ) + } else { + _, _ = io.Copy(io.Discard, rc) + _ = rc.Close() + } + } + + for i := 0; i < c.NumValidators; i++ { + nn, err := NewNamadaNode(ctx, c.log, c, i, true, testName, cli, networkID, chainCfg.Images[0]) + if err != nil { + return err + } + c.Validators = append(c.Validators, nn) + } + + for i := 0; i < c.numFullNodes; i++ { + nn, err := NewNamadaNode(ctx, c.log, c, i, false, testName, cli, networkID, chainCfg.Images[0]) + if err != nil { + return err + } + c.FullNodes = append(c.FullNodes, nn) + } + + tempBaseDir, err := os.MkdirTemp("", "namada") + if err != nil { + return err + } + defer os.RemoveAll(tempBaseDir) + + c.log.Debug("Temporary base directory", + zap.String("path", tempBaseDir), + ) + c.isRunning = false + + return nil +} + +// Start to set up. +func (c *NamadaChain) Start(testName string, ctx context.Context, additionalGenesisWallets ...ibc.WalletAmount) error { + err := c.downloadTemplates(ctx) + if err != nil { + return fmt.Errorf("downloading template files failed: %v", err) + } + err = c.downloadWasms(ctx) + if err != nil { + return fmt.Errorf("downloading wasm files failed: %v", err) + } + + err = c.setValidators(ctx) + if err != nil { + return fmt.Errorf("setting validators failed: %v", err) + } + + err = c.initAccounts(ctx, additionalGenesisWallets...) + if err != nil { + return fmt.Errorf("initializing accounts failed: %v", err) + } + + err = c.updateParameters(ctx) + if err != nil { + return fmt.Errorf("updating parameters failed: %v", err) + } + + err = c.initNetwork(ctx) + if err != nil { + return fmt.Errorf("init-network failed: %v", err) + } + + eg, egCtx := errgroup.WithContext(ctx) + for _, n := range c.Validators { + eg.Go(func() error { + if err := c.copyGenesisFiles(egCtx, n); err != nil { + return err + } + return n.CreateContainer(egCtx) + }) + } + + for _, n := range c.FullNodes { + eg.Go(func() error { + if err := c.copyGenesisFiles(egCtx, n); err != nil { + return err + } + return n.CreateContainer(egCtx) + }) + } + if err := eg.Wait(); err != nil { + return err + } + + eg, egCtx = errgroup.WithContext(ctx) + for _, n := range c.Validators { + eg.Go(func() error { + return n.StartContainer(egCtx) + }) + } + + for _, n := range c.FullNodes { + eg.Go(func() error { + return n.StartContainer(egCtx) + }) + } + if err := eg.Wait(); err != nil { + return err + } + + if err := testutil.WaitForBlocks(ctx, 2, c.getNode()); err != nil { + return err + } + + c.isRunning = true + + return nil +} + +func (c *NamadaChain) getNode() *NamadaNode { + return c.Validators[0] +} + +// Execute a command. +func (c *NamadaChain) Exec(ctx context.Context, cmd []string, env []string) (stdout, stderr []byte, err error) { + return c.getNode().Exec(ctx, cmd, env) +} + +// Exports the chain state at the specific height. +func (c *NamadaChain) ExportState(ctx context.Context, height int64) (string, error) { + panic("implement me") +} + +// Get the RPC address. +func (c *NamadaChain) GetRPCAddress() string { + return fmt.Sprintf("http://%s:26657", c.getNode().HostName()) +} + +// Get the gRPC address. This isn't used for Namada. +func (c *NamadaChain) GetGRPCAddress() string { + // Returns a dummy address because Namada doesn't support gRPC + return fmt.Sprintf("http://%s:9090", c.getNode().HostName()) +} + +// Get the host RPC address. +func (c *NamadaChain) GetHostRPCAddress() string { + return "http://" + c.getNode().hostRPCPort +} + +// Get the host peer address. +func (c *NamadaChain) GetHostPeerAddress() string { + return c.getNode().hostP2PPort +} + +// Get the host gRPC address. +func (c *NamadaChain) GetHostGRPCAddress() string { + panic("No gRPC address for Namada") +} + +// Get Namada home directory. +func (c *NamadaChain) HomeDir() string { + return c.getNode().HomeDir() +} + +// Create a test key. +func (c *NamadaChain) CreateKey(ctx context.Context, keyName string) error { + var err error + cmd := []string{ + c.cfg.Bin, + "wallet", + "--base-dir", + c.HomeDir(), + "gen", + "--alias", + keyName, + "--unsafe-dont-encrypt", + } + if !c.isRunning { + cmd = append(cmd, "--pre-genesis") + } + _, _, err = c.Exec(ctx, cmd, c.Config().Env) + + return err +} + +// Recovery a test key. +func (c *NamadaChain) RecoverKey(ctx context.Context, keyName, mnemonic string) error { + cmd := []string{ + "echo", + mnemonic, + "|", + c.cfg.Bin, + "wallet", + "--base-dir", + c.HomeDir(), + "derive", + "--alias", + keyName, + "--unsafe-dont-encrypt", + } + _, _, err := c.Exec(ctx, cmd, c.Config().Env) + + return err +} + +// Get the Namada address. +func (c *NamadaChain) GetAddress(ctx context.Context, keyName string) ([]byte, error) { + cmd := []string{ + c.cfg.Bin, + "wallet", + "--base-dir", + c.HomeDir(), + "find", + "--alias", + keyName, + } + if !c.isRunning { + cmd = append(cmd, "--pre-genesis") + } + output, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return nil, fmt.Errorf("getting an address failed with name %q: %w", keyName, err) + } + outputStr := string(output) + re := regexp.MustCompile(`(tnam|znam|zvknam)[0-9a-z]+`) + address := re.FindString(outputStr) + + if address == "" { + return nil, fmt.Errorf("no address with name %q: %w", keyName, err) + } + + return []byte(address), nil +} + +// Get the key alias. +func (c *NamadaChain) getAlias(ctx context.Context, address string) (string, error) { + cmd := []string{ + c.cfg.Bin, + "wallet", + "--base-dir", + c.HomeDir(), + "find", + "--address", + address, + } + if !c.isRunning { + cmd = append(cmd, "--pre-genesis") + } + output, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return "", fmt.Errorf("getting the alias failed with address %s: %w", address, err) + } + outputStr := string(output) + re := regexp.MustCompile(`Found alias (\S+)`) + matches := re.FindStringSubmatch(outputStr) + if len(matches) < 2 { + return "", fmt.Errorf("no alias found: %s", outputStr) + } + alias := matches[1] + + return alias, nil +} + +// Send funds to a wallet from a user account. +func (c *NamadaChain) SendFunds(ctx context.Context, keyName string, amount ibc.WalletAmount) error { + var transferCmd string + if strings.HasPrefix(amount.Address, "znam") { + transferCmd = "shield" + } else { + transferCmd = "transparent-transfer" + } + cmd := []string{ + c.cfg.Bin, + "client", + "--base-dir", + c.HomeDir(), + transferCmd, + "--source", + keyName, + "--target", + amount.Address, + "--token", + amount.Denom, + "--amount", + amount.Amount.String(), + "--node", + c.GetRPCAddress(), + } + _, _, err := c.Exec(ctx, cmd, c.Config().Env) + + return err +} + +// Send funds to a wallet from a user account with a memo. +func (c *NamadaChain) SendFundsWithNote(ctx context.Context, keyName string, amount ibc.WalletAmount, note string) (string, error) { + var transferCmd string + if strings.HasPrefix(amount.Address, "znam") { + transferCmd = "shield" + } else { + transferCmd = "transparent-transfer" + } + cmd := []string{ + c.cfg.Bin, + "client", + "--base-dir", + c.HomeDir(), + transferCmd, + "--source", + keyName, + "--target", + amount.Address, + "--token", + amount.Denom, + "--amount", + amount.Amount.String(), + "--memo", + note, + "--node", + c.GetRPCAddress(), + } + _, _, err := c.Exec(ctx, cmd, c.Config().Env) + + return note, err +} + +// Send on IBC transfer. +func (c *NamadaChain) SendIBCTransfer(ctx context.Context, channelID, keyName string, amount ibc.WalletAmount, options ibc.TransferOptions) (ibc.Tx, error) { + cmd := []string{ + c.cfg.Bin, + "client", + "--base-dir", + c.HomeDir(), + "ibc-transfer", + "--source", + keyName, + "--receiver", + amount.Address, + "--token", + amount.Denom, + "--amount", + amount.Amount.String(), + "--channel-id", + channelID, + "--node", + c.GetRPCAddress(), + } + + if c.Config().Gas != "" { + _, err := strconv.ParseInt(c.Config().Gas, 10, 64) + if err != nil { + return ibc.Tx{}, fmt.Errorf("invalid gas limit: %s", c.Config().Gas) + } + cmd = append(cmd, "--gas-limit", c.Config().Gas) + } + + if options.Port != "" { + cmd = append(cmd, "--port-id", options.Port) + } + + if options.Memo != "" { + cmd = append(cmd, "--ibc-memo", options.Memo) + } + + if options.Timeout != nil { + if options.Timeout.NanoSeconds > 0 { + timestamp := time.Unix(0, int64(options.Timeout.NanoSeconds)) + currentTime := time.Now() + if currentTime.After(timestamp) { + return ibc.Tx{}, fmt.Errorf("invalid timeout timestamp: %d", options.Timeout.NanoSeconds) + } + offset := int64(timestamp.Sub(currentTime).Seconds()) + cmd = append(cmd, "--timeout-sec-offset", strconv.FormatInt(offset, 10)) + } + + if options.Timeout.Height > 0 { + cmd = append(cmd, "--timeout-height", strconv.FormatInt(options.Timeout.Height, 10)) + } + } + + if strings.HasPrefix(keyName, "shielded") { + cmd = append(cmd, "--gas-payer", gasPayerAlias) + } + + output, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return ibc.Tx{}, fmt.Errorf("the transaction failed: %s, %v", output, err) + } + outputStr := string(output) + c.log.Log(zap.InfoLevel, outputStr) + + re := regexp.MustCompile(`Transaction hash: ([0-9A-F]+)`) + matches := re.FindStringSubmatch(outputStr) + var txHash string + if len(matches) > 1 { + txHash = matches[1] + } else { + return ibc.Tx{}, fmt.Errorf("the transaction failed: %s", outputStr) + } + + re = regexp.MustCompile(`Transaction ([0-9A-F]+) was successfully applied at height (\d+), consuming (\d+) gas units`) + matchesAll := re.FindAllStringSubmatch(outputStr, -1) + if len(matches) == 0 { + return ibc.Tx{}, fmt.Errorf("the transaction failed: %s", outputStr) + } + + var height int64 + var gas int64 + for _, match := range matchesAll { + if len(match) == 4 { + // it is ok to overwrite them of the last transaction + height, _ = strconv.ParseInt(match[2], 10, 64) + gas, _ = strconv.ParseInt(match[3], 10, 64) + } + } + + tx := ibc.Tx{ + TxHash: txHash, + Height: height, + GasSpent: gas, + } + + results, err := c.getNode().Client.BlockResults(ctx, &height) + if err != nil { + return ibc.Tx{}, fmt.Errorf("checking the events failed: %v", err) + } + const evType = "send_packet" + tendermintEvents := results.EndBlockEvents + var events []cometbft.Event + for _, event := range tendermintEvents { + if event.Type != evType { + continue + } + jsonEvent, err := json.Marshal(event) + if err != nil { + return ibc.Tx{}, fmt.Errorf("converting an events failed: %v", err) + } + var event cometbft.Event + err = json.Unmarshal(jsonEvent, &event) + if err != nil { + return ibc.Tx{}, fmt.Errorf("converting an event failed: %v", err) + } + events = append(events, event) + } + + var ( + seq, _ = tendermint.AttributeValue(events, evType, "packet_sequence") + srcPort, _ = tendermint.AttributeValue(events, evType, "packet_src_port") + srcChan, _ = tendermint.AttributeValue(events, evType, "packet_src_channel") + dstPort, _ = tendermint.AttributeValue(events, evType, "packet_dst_port") + dstChan, _ = tendermint.AttributeValue(events, evType, "packet_dst_channel") + timeoutHeight, _ = tendermint.AttributeValue(events, evType, "packet_timeout_height") + timeoutTS, _ = tendermint.AttributeValue(events, evType, "packet_timeout_timestamp") + dataHex, _ = tendermint.AttributeValue(events, evType, "packet_data_hex") + ) + tx.Packet.SourcePort = srcPort + tx.Packet.SourceChannel = srcChan + tx.Packet.DestPort = dstPort + tx.Packet.DestChannel = dstChan + tx.Packet.TimeoutHeight = timeoutHeight + + data, err := hex.DecodeString(dataHex) + if err != nil { + return tx, fmt.Errorf("malformed data hex %s: %w", dataHex, err) + } + tx.Packet.Data = data + + seqNum, err := strconv.ParseUint(seq, 10, 64) + if err != nil { + return tx, fmt.Errorf("invalid packet sequence from events %s: %w", seq, err) + } + tx.Packet.Sequence = seqNum + + timeoutNano, err := strconv.ParseUint(timeoutTS, 10, 64) + if err != nil { + return tx, fmt.Errorf("invalid packet timestamp timeout %s: %w", timeoutTS, err) + } + tx.Packet.TimeoutTimestamp = ibc.Nanoseconds(timeoutNano) + + return tx, err +} + +// Shielded transfer (shielded account to shielded account) on Namada. +func (c *NamadaChain) ShieldedTransfer(ctx context.Context, keyName string, amount ibc.WalletAmount) error { + cmd := []string{ + c.cfg.Bin, + "client", + "--base-dir", + c.HomeDir(), + "transfer", + "--source", + keyName, + "--target", + amount.Address, + "--token", + amount.Denom, + "--amount", + amount.Amount.String(), + "--gas-payer", + gasPayerAlias, + "--node", + c.GetRPCAddress(), + } + _, _, err := c.Exec(ctx, cmd, c.Config().Env) + + return err +} + +// Generate an IBC shielding transfer for the following shielding transfer via IBC. +func (c *NamadaChain) GenIbcShieldingTransfer(ctx context.Context, channelID string, amount ibc.WalletAmount, options ibc.TransferOptions) (string, error) { + var portID string + if options.Port == "" { + portID = "transfer" + } else { + portID = options.Port + } + + cmd := []string{ + c.cfg.Bin, + "client", + "--base-dir", + c.HomeDir(), + "ibc-gen-shielding", + "--output-folder-path", + c.HomeDir(), + "--target", + amount.Address, + "--token", + amount.Denom, + "--amount", + amount.Amount.String(), + "--port-id", + portID, + "--channel-id", + channelID, + "--node", + c.GetRPCAddress(), + } + output, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return "", fmt.Errorf("failed to generate the IBC shielding transfer: %v", err) + } + outputStr := string(output) + + re := regexp.MustCompile(`Output IBC shielding transfer for ([^\s]+) to (.+)`) + matches := re.FindStringSubmatch(outputStr) + var path string + if len(matches) > 2 { + path = matches[2] + } else { + return "", fmt.Errorf("failed to get the file path of the IBC shielding transfer") + } + relPath, _ := filepath.Rel(c.HomeDir(), path) + shieldingTransfer, err := c.getNode().ReadFile(ctx, relPath) + if err != nil { + return "", fmt.Errorf("failed to read the IBC shielding transfer file: %v", err) + } + + return string(shieldingTransfer), nil +} + +// Get the current block height. +func (c *NamadaChain) Height(ctx context.Context) (int64, error) { + return c.getNode().Height(ctx) +} + +// Get the balance with the key alias, not the address. +func (c *NamadaChain) GetBalance(ctx context.Context, keyName string, denom string) (math.Int, error) { + if strings.HasPrefix(keyName, "shielded") { + cmd := []string{ + c.cfg.Bin, + "client", + "--base-dir", + c.HomeDir(), + "shielded-sync", + "--viewing-keys", + keyName, + "--node", + c.GetRPCAddress(), + } + output, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return math.NewInt(0), fmt.Errorf("shielded-sync failed: error %s, output %s", err, output) + } + } + + cmd := []string{ + c.cfg.Bin, + "client", + "--base-dir", + c.HomeDir(), + "balance", + "--token", + denom, + "--owner", + keyName, + "--node", + c.GetRPCAddress(), + } + output, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return math.NewInt(0), fmt.Errorf("getting the balance failed: error %s, output %s", err, output) + } + outputStr := string(output) + lines := strings.Split(outputStr, "\n") + // Parse the balance from the output like `nam: 1000.000000` + re := regexp.MustCompile(`:\s*(\d+(\.\d+)?)$`) + + ret := math.NewInt(0) + for _, line := range lines { + if strings.Contains(line, "Last committed masp epoch") { + continue + } + + matches := re.FindStringSubmatch(line) + if len(matches) > 1 { + amount, err := strconv.ParseFloat(matches[1], 64) + if err != nil { + return math.NewInt(0), fmt.Errorf("parsing the amount failed: %s", outputStr) + } + var multiplier float64 + if denom == c.Config().Denom { + multiplier = stdmath.Pow(10, float64(*c.Config().CoinDecimals)) + } else { + // IBC token denom is always zero + multiplier = 1.0 + } + // the result should be an integer + ret = math.NewInt(int64(amount * multiplier)) + } + } + + return ret, err +} + +// Get the gas fees. +func (c *NamadaChain) GetGasFeesInNativeDenom(gasPaid int64) int64 { + panic("implement me") +} + +// All acks at the height. +func (c *NamadaChain) Acknowledgements(ctx context.Context, height int64) ([]ibc.PacketAcknowledgement, error) { + panic("implement me") +} + +// All timeouts at the height. +func (c *NamadaChain) Timeouts(ctx context.Context, height int64) ([]ibc.PacketTimeout, error) { + panic("implement me") +} + +// Build a Namada wallet. Generates a spending key when the keyName prefixed with "shielded". +func (c *NamadaChain) BuildWallet(ctx context.Context, keyName string, mnemonic string) (ibc.Wallet, error) { + if mnemonic != "" { + if err := c.RecoverKey(ctx, keyName, mnemonic); err != nil { + return nil, fmt.Errorf("failed to recover key with name %q on chain %s: %w", keyName, c.cfg.Name, err) + } + + addrBytes, err := c.GetAddress(ctx, keyName) + if err != nil { + return nil, fmt.Errorf("failed to get account address for key %q on chain %s: %w", keyName, c.cfg.Name, err) + } + + return NewWallet(keyName, addrBytes, mnemonic, c.cfg), nil + } + + if !c.isRunning { + return c.createGenesisKey(ctx, keyName) + } else { + return c.createKeyAndMnemonic(ctx, keyName, strings.HasPrefix(keyName, "shielded")) + } +} + +// Build a Namada wallet for a relayer. +func (c *NamadaChain) BuildRelayerWallet(ctx context.Context, keyName string) (ibc.Wallet, error) { + return c.createKeyAndMnemonic(ctx, keyName, false) +} + +// Create an established account key for genesis. +func (c *NamadaChain) createGenesisKey(ctx context.Context, keyName string) (ibc.Wallet, error) { + alias := fmt.Sprintf("%s-key", keyName) + _, err := c.createKeyAndMnemonic(ctx, alias, false) + if err != nil { + return &NamadaWallet{}, err + } + + transactionPath := filepath.Join(c.HomeDir(), fmt.Sprintf("established-account-tx-%s.toml", keyName)) + address, err := c.initGenesisEstablishedAccount(ctx, alias, transactionPath) + if err != nil { + return &NamadaWallet{}, err + } + + if err := c.addAddress(ctx, keyName, address); err != nil { + return &NamadaWallet{}, err + } + + return NewWallet(keyName, []byte(address), "", c.cfg), nil +} + +func (c *NamadaChain) createKeyAndMnemonic(ctx context.Context, keyName string, isShielded bool) (ibc.Wallet, error) { + cmd := []string{ + c.cfg.Bin, + "wallet", + "--base-dir", + c.HomeDir(), + "gen", + "--alias", + keyName, + "--unsafe-dont-encrypt", + } + if isShielded && !c.isRunning { + return nil, fmt.Errorf("generating a shielded account in pre-genesis is not allowed in this test") + } + if isShielded { + cmd = append(cmd, "--shielded") + } + if !c.isRunning { + cmd = append(cmd, "--pre-genesis") + } + output, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return nil, fmt.Errorf("failed to generate an account for key %q on chain %s: %w", keyName, c.cfg.Name, err) + } + outputStr := string(output) + re := regexp.MustCompile(`[a-z]+(?:\s+[a-z]+){23}`) + mnemonic := re.FindString(outputStr) + + addrBytes, err := c.GetAddress(ctx, keyName) + if err != nil { + return nil, fmt.Errorf("failed to get account address for key %q on chain %s: %w", keyName, c.cfg.Name, err) + } + + wallet := NewWallet(keyName, addrBytes, mnemonic, c.Config()) + + // Generate a payment address + if isShielded { + cmd = []string{ + c.cfg.Bin, + "wallet", + "--base-dir", + c.HomeDir(), + "gen-payment-addr", + "--alias", + wallet.PaymentAddressKeyName(), + "--key", + keyName, + } + if !c.isRunning { + cmd = append(cmd, "--pre-genesis") + } + _, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return nil, fmt.Errorf("failed to generate a payment address for key %q on chain %s: %w", keyName, c.Config().Name, err) + } + + addrBytes, err := c.GetAddress(ctx, wallet.PaymentAddressKeyName()) + if err != nil { + return nil, fmt.Errorf("failed to get account address for key %q on chain %s: %w", keyName, c.cfg.Name, err) + } + // replace the address with the payment address + wallet = NewWallet(keyName, addrBytes, mnemonic, c.Config()) + } + + return wallet, nil +} + +func (c *NamadaChain) addAddress(ctx context.Context, keyName, address string) error { + cmd := []string{ + c.cfg.Bin, + "wallet", + "--base-dir", + c.HomeDir(), + "add", + "--alias", + keyName, + "--value", + address, + } + if !c.isRunning { + cmd = append(cmd, "--pre-genesis") + } + _, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return fmt.Errorf("address couldn't be added: %v", err) + } + + return nil +} + +func (c *NamadaChain) downloadTemplates(ctx context.Context) error { + baseURL := fmt.Sprintf("https://raw.githubusercontent.com/anoma/namada/%s/genesis/localnet", c.Config().Images[0].Version) + files := []string{ + "parameters.toml", + "tokens.toml", + "validity-predicates.toml", + } + destDir := "templates" + + for _, file := range files { + url := fmt.Sprintf("%s/%s", baseURL, file) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("failed to download the file %s: %w", file, err) + } + resp, err := (&http.Client{}).Do(req) + if err != nil { + return fmt.Errorf("failed to download the file %s: %w", file, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download the file %s: %d", file, resp.StatusCode) + } + + var buf bytes.Buffer + if _, err := io.Copy(&buf, resp.Body); err != nil { + return fmt.Errorf("failed to read the file: %w", err) + } + err = c.getNode().writeFile(ctx, filepath.Join(destDir, file), buf.Bytes()) + if err != nil { + return fmt.Errorf("failed to write the file %s: %w", file, err) + } + } + + return nil +} + +func (c *NamadaChain) downloadWasms(ctx context.Context) error { + url := fmt.Sprintf("https://github.com/anoma/namada/releases/download/%s/namada-%s-Linux-x86_64.tar.gz", c.Config().Images[0].Version, c.Config().Images[0].Version) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("failed to download the release file: %w", err) + } + resp, err := (&http.Client{}).Do(req) + if err != nil { + return fmt.Errorf("failed to download the release file: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download the release file: %d", resp.StatusCode) + } + filePath := "release.tar.gz" + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to open the release file: %w", err) + } + defer file.Close() + _, err = io.Copy(file, resp.Body) + if err != nil { + return fmt.Errorf("failed to write the release file: %w", err) + } + + file, err = os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open the release file: %w", err) + } + defer file.Close() + gzr, err := gzip.NewReader(file) + if err != nil { + return fmt.Errorf("failed to create gzip reader: %w", err) + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + destDir := "wasm" + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("failed to read tar file: %w", err) + } + + if header.Typeflag == tar.TypeReg { + if strings.HasSuffix(header.Name, ".wasm") || strings.HasSuffix(header.Name, ".json") { + var buf bytes.Buffer + limitedReader := io.LimitReader(tr, 10*1024*1024) + if _, err := io.Copy(&buf, limitedReader); err != nil { + return fmt.Errorf("failed to read the file: %w", err) + } + fileName := filepath.Base(header.Name) + err = c.getNode().writeFile(ctx, filepath.Join(destDir, fileName), buf.Bytes()) + if err != nil { + return fmt.Errorf("failed to write the file: %w", err) + } + } + } + } + + err = os.Remove(filePath) + if err != nil { + return fmt.Errorf("failed to delete the release file: %v", err) + } + + return nil +} + +func (c *NamadaChain) setValidators(ctx context.Context) error { + transactionPath := filepath.Join(c.HomeDir(), "transactions.toml") + destTransactionsPath := filepath.Join(c.HomeDir(), "templates", "transactions.toml") + cmd := []string{ + "touch", + destTransactionsPath, + } + _, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return fmt.Errorf("making transactions.toml failed: %v", err) + } + + for i := 0; i < c.NumValidators; i++ { + alias := fmt.Sprintf("validator-%d-balance-key", i) + validatorAlias := fmt.Sprintf("validator-%d", i) + + // Generate a validator key + cmd := []string{ + c.cfg.Bin, + "wallet", + "--base-dir", + c.HomeDir(), + "--pre-genesis", + "gen", + "--alias", + alias, + "--unsafe-dont-encrypt", + } + _, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return fmt.Errorf("validator key couldn't be generated: %v", err) + } + + // Initialize an established account of the validator + validatorAddress, err := c.initGenesisEstablishedAccount(ctx, alias, transactionPath) + if err != nil { + return err + } + + // Add the validator address + if err := c.addAddress(ctx, validatorAlias, validatorAddress); err != nil { + return fmt.Errorf("validator address couldn't be added: %v", err) + } + + netAddress, err := c.Validators[i].netAddress(ctx) + if err != nil { + return err + } + + // Initialize a genesis validator + cmd = []string{ + c.cfg.Bin, + "client", + "--base-dir", + c.HomeDir(), + "utils", + "init-genesis-validator", + "--alias", + validatorAlias, + "--address", + validatorAddress, + "--path", + transactionPath, + "--net-address", + netAddress, + "--commission-rate", + "0.05", + "--max-commission-rate-change", + "0.01", + "--email", + "null@null.net", + "--self-bond-amount", + "100000", + "--unsafe-dont-encrypt", + } + output, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return fmt.Errorf("initializing a genesis validator failed: %v, %s", err, output) + } + + cmd = []string{ + c.cfg.Bin, + "client", + "--base-dir", + c.HomeDir(), + "utils", + "sign-genesis-txs", + "--alias", + validatorAlias, + "--path", + transactionPath, + "--output", + transactionPath, + } + _, _, err = c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return fmt.Errorf("signing genesis transactions failed: %v", err) + } + + cmd = []string{ + "sh", + "-c", + fmt.Sprintf(`cat %s >> %s`, transactionPath, destTransactionsPath), + } + _, _, err = c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return fmt.Errorf("appending the transaction failed: %v", err) + } + } + + return nil +} + +func (c *NamadaChain) initAccounts(ctx context.Context, additionalGenesisWallets ...ibc.WalletAmount) error { + templateDir := filepath.Join(c.HomeDir(), "templates") + balancePath := filepath.Join(templateDir, "balances.toml") + + // Initialize balances.toml + cmd := []string{ + "sh", + "-c", + fmt.Sprintf(`echo [token.NAM] > %s`, balancePath), + } + _, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return fmt.Errorf("initializing balances.toml failed: %v", err) + } + + // for validators + for i := 0; i < c.NumValidators; i++ { + addr, err := c.GetAddress(ctx, fmt.Sprintf("validator-%d", i)) + if err != nil { + return err + } + line := fmt.Sprintf(`%s = "2000000"`, string(addr)) + cmd := []string{ + "sh", + "-c", + fmt.Sprintf(`echo '%s' >> %s`, line, balancePath), + } + _, _, err = c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return fmt.Errorf("appending the balance to balances.toml failed: %v", err) + } + } + + // for a gas payer + gasPayer, err := c.createKeyAndMnemonic(ctx, gasPayerAlias, false) + if err != nil { + return err + } + gasPayerAmount := ibc.WalletAmount{ + Address: gasPayer.FormattedAddress(), + Denom: c.Config().Denom, + Amount: math.NewInt(1000000000), + } + additionalGenesisWallets = append(additionalGenesisWallets, gasPayerAmount) + + for _, wallet := range additionalGenesisWallets { + line := fmt.Sprintf(`%s = "%s"`, wallet.Address, wallet.Amount) + cmd := []string{ + "sh", + "-c", + fmt.Sprintf(`echo '%s' >> %s`, line, balancePath), + } + _, _, err = c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return fmt.Errorf("appending the balance to balances.toml failed: %v", err) + } + // Add the key balance + alias, err := c.getAlias(ctx, wallet.Address) + if err != nil { + return err + } + keyAddress, err := c.GetAddress(ctx, fmt.Sprintf("%s-key", alias)) + if err != nil { + // skip when the account is implicit + continue + } + line = fmt.Sprintf(`%s = "%s"`, keyAddress, wallet.Amount) + cmd = []string{ + "sh", + "-c", + fmt.Sprintf(`echo '%s' >> %s`, line, balancePath), + } + _, _, err = c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return fmt.Errorf("appending the balance to balances.toml failed: %v", err) + } + } + destTransactionsPath := filepath.Join(templateDir, "transactions.toml") + cmd = []string{ + "sh", + "-c", + fmt.Sprintf("find %s -name 'established-account-tx-*.toml' -exec cat {} + >> %s", c.HomeDir(), destTransactionsPath), + } + _, _, err = c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return fmt.Errorf("appending establish account tx: %w", err) + } + + return nil +} + +func (c *NamadaChain) initGenesisEstablishedAccount(ctx context.Context, keyName, transactionPath string) (string, error) { + cmd := []string{ + c.cfg.Bin, + "client", + "--base-dir", + c.HomeDir(), + "utils", + "init-genesis-established-account", + "--aliases", + keyName, + "--path", + transactionPath, + } + output, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return "", fmt.Errorf("initializing a validator account failed: %v", err) + } + outputStr := string(output) + // Trim ANSI escape sequence + ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`) + outputStr = ansiRegex.ReplaceAllString(outputStr, "") + re := regexp.MustCompile(`Derived established account address: (\S+)`) + matches := re.FindStringSubmatch(outputStr) + if len(matches) < 2 { + return "", fmt.Errorf("no established account address found: %s", outputStr) + } + addr := matches[1] + + return addr, nil +} + +func (c *NamadaChain) updateParameters(ctx context.Context) error { + templateDir := filepath.Join(c.HomeDir(), "templates") + paramPath := filepath.Join(templateDir, "parameters.toml") + + cmd := []string{ + "sed", + "-i", + // for enough trusting period + "-e", + "s/epochs_per_year = [0-9_]\\+/epochs_per_year = 365/", + // delete steward addresses + "-e", + "s/\"tnam.*//", + // IBC mint limit + "-e", + "s/default_mint_limit = \"[0-9]\\+\"/default_mint_limit = \"1000000000000\"/", + // IBC throughput limit + "-e", + "s/default_per_epoch_throughput_limit = \"[0-9]\\+\"/default_per_epoch_throughput_limit = \"1000000000000\"/", + paramPath, + } + _, _, err := c.Exec(ctx, cmd, c.Config().Env) + return err +} + +func (c *NamadaChain) initNetwork(ctx context.Context) error { + templatesDir := filepath.Join(c.HomeDir(), "templates") + wasmDir := filepath.Join(c.HomeDir(), "wasm") + checksumsPath := filepath.Join(wasmDir, "checksums.json") + genesisTime := time.Now().UTC().Format("2006-01-02T15:04:05.000000000-07:00") + cmd := []string{ + c.cfg.Bin, + "client", + "--base-dir", + c.HomeDir(), + "utils", + "init-network", + "--templates-path", + templatesDir, + "--chain-prefix", + "namada-test", + "--wasm-checksums-path", + checksumsPath, + "--wasm-dir", + wasmDir, + "--genesis-time", + genesisTime, + "--archive-dir", + c.HomeDir(), + } + output, _, err := c.Exec(ctx, cmd, c.Config().Env) + if err != nil { + return fmt.Errorf("init-network failed: %v", err) + } + outputStr := string(output) + + re := regexp.MustCompile(`Derived chain ID: (\S+)`) + matches := re.FindStringSubmatch(outputStr) + if len(matches) < 2 { + return fmt.Errorf("no chain ID: %s", outputStr) + } + c.cfg.ChainID = matches[1] + + return nil +} + +func (c *NamadaChain) copyGenesisFiles(ctx context.Context, n *NamadaNode) error { + archivePath := fmt.Sprintf("%s.tar.gz", c.Config().ChainID) + content, err := c.getNode().ReadFile(ctx, archivePath) + if err != nil { + return fmt.Errorf("failed to read the archive file: %w", err) + } + + err = n.writeFile(ctx, archivePath, content) + if err != nil { + return fmt.Errorf("failed to write the archive file: %w", err) + } + + walletPath := filepath.Join("pre-genesis", "wallet.toml") + content, err = c.getNode().ReadFile(ctx, walletPath) + if err != nil { + return fmt.Errorf("failed to read the wallet file: %w", err) + } + err = n.writeFile(ctx, "wallet.toml", content) + if err != nil { + return fmt.Errorf("failed to write the wallet file: %w", err) + } + + if n.Validator { + validatorAlias := fmt.Sprintf("validator-%d", n.Index) + validatorWalletPath := filepath.Join("pre-genesis", validatorAlias, "validator-wallet.toml") + content, err = c.getNode().ReadFile(ctx, validatorWalletPath) + if err != nil { + return fmt.Errorf("failed to read the validator wallet file: %w", err) + } + err = n.writeFile(ctx, validatorWalletPath, content) + if err != nil { + return fmt.Errorf("failed to write the validator wallet file: %w", err) + } + } + + return nil +} diff --git a/chain/namada/namada_node.go b/chain/namada/namada_node.go new file mode 100644 index 000000000..717ba48b6 --- /dev/null +++ b/chain/namada/namada_node.go @@ -0,0 +1,343 @@ +package namada + +import ( + "context" + "fmt" + "net" + "path/filepath" + "strings" + "time" + + "github.com/avast/retry-go/v4" + "github.com/docker/docker/api/types" + volumetypes "github.com/docker/docker/api/types/volume" + dockerclient "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + // To use a legacy tendermint client. + rpcclient "github.com/tendermint/tendermint/rpc/client" + rpchttp "github.com/tendermint/tendermint/rpc/client/http" + libclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" + "go.uber.org/zap" + + "github.com/strangelove-ventures/interchaintest/v8/dockerutil" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/testutil" +) + +type NamadaNode struct { + Index int + Validator bool + TestName string + Chain ibc.Chain + DockerClient *dockerclient.Client + Client rpcclient.Client + Image ibc.DockerImage + VolumeName string + NetworkID string + + log *zap.Logger + + containerLifecycle *dockerutil.ContainerLifecycle + + // Ports set during StartContainer. + hostP2PPort string + hostRPCPort string +} + +// Collection of NamadaNode. +type NamadaNodes []*NamadaNode + +const ( + p2pPort = "26656/tcp" + rpcPort = "26657/tcp" +) + +func NewNamadaNode( + ctx context.Context, + log *zap.Logger, + chain *NamadaChain, + index int, + validator bool, + testName string, + dockerClient *dockerclient.Client, + networkID string, + image ibc.DockerImage, +) (*NamadaNode, error) { + nn := &NamadaNode{ + Index: index, + Validator: validator, + TestName: testName, + Chain: chain, + DockerClient: dockerClient, + Image: image, + NetworkID: networkID, + + log: log.With( + zap.Bool("validator", validator), + zap.Int("i", index), + ), + } + + nn.containerLifecycle = dockerutil.NewContainerLifecycle(log, dockerClient, nn.Name()) + + nv, err := dockerClient.VolumeCreate(ctx, volumetypes.CreateOptions{ + Labels: map[string]string{ + dockerutil.CleanupLabel: testName, + dockerutil.NodeOwnerLabel: nn.Name(), + }, + }) + if err != nil { + return nil, fmt.Errorf("creating namada volume: %w", err) + } + + nn.VolumeName = nv.Name + if err := dockerutil.SetVolumeOwner(ctx, dockerutil.VolumeOwnerOptions{ + Log: log, + + Client: dockerClient, + + VolumeName: nn.VolumeName, + ImageRef: nn.Image.Ref(), + TestName: nn.TestName, + UidGid: image.UIDGID, + }); err != nil { + return nil, fmt.Errorf("set namada volume owner: %w", err) + } + + return nn, nil +} + +func (n *NamadaNode) Exec(ctx context.Context, cmd []string, env []string) ([]byte, []byte, error) { + job := dockerutil.NewImage(n.logger(), n.DockerClient, n.NetworkID, n.TestName, n.Image.Repository, n.Image.Version) + opts := dockerutil.ContainerOptions{ + Env: env, + Binds: n.Bind(), + } + res := job.Run(ctx, cmd, opts) + return res.Stdout, res.Stderr, res.Err +} + +func (n *NamadaNode) logger() *zap.Logger { + return n.log.With( + zap.String("chain_id", n.Chain.Config().ChainID), + zap.String("test", n.TestName), + ) +} + +// Name of the test node container. +func (n *NamadaNode) Name() string { + return fmt.Sprintf("%s-%s-%d-%s", n.Chain.Config().ChainID, n.NodeType(), n.Index, n.TestName) +} + +func (n *NamadaNode) HostName() string { + return dockerutil.CondenseHostName(n.Name()) +} + +func (n *NamadaNode) Bind() []string { + return []string{fmt.Sprintf("%s:%s", n.VolumeName, n.HomeDir())} +} + +// Home directory in the Docker filesystem. +func (n *NamadaNode) HomeDir() string { + return "/home/namada" +} + +func (n *NamadaNode) NodeType() string { + nodeType := "fn" + if n.Validator { + nodeType = "val" + } + return nodeType +} + +func (n *NamadaNode) NewRPCClient(addr string) error { + httpClient, err := libclient.DefaultHTTPClient(addr) + if err != nil { + return err + } + + httpClient.Timeout = 10 * time.Second + rpcClient, err := rpchttp.NewWithClient(addr, "/websocket", httpClient) + if err != nil { + return err + } + + n.Client = rpcClient + return nil +} + +func (n *NamadaNode) Height(ctx context.Context) (int64, error) { + stat, err := n.Client.Status(ctx) + if err != nil { + return 0, fmt.Errorf("tendermint client status: %w", err) + } + return stat.SyncInfo.LatestBlockHeight, nil +} + +func (n *NamadaNode) CreateContainer(ctx context.Context) error { + setConfigDir := fmt.Sprintf("NAMADA_NETWORK_CONFIGS_DIR=%s", n.HomeDir()) + + joinNetworkCmd := fmt.Sprintf(`%s %s client --base-dir %s utils join-network --add-persistent-peers --chain-id %s --allow-duplicate-ip`, setConfigDir, n.Chain.Config().Bin, n.HomeDir(), n.Chain.Config().ChainID) + if n.Validator { + joinNetworkCmd += " --genesis-validator " + fmt.Sprintf("validator-%d", n.Index) + } + + configPath := fmt.Sprintf("%s/%s/config.toml", n.HomeDir(), n.Chain.Config().ChainID) + c := make(testutil.Toml) + p2p := make(testutil.Toml) + p2p["laddr"] = "0.0.0.0:26657" + c["ledger.cometbft.p2p"] = p2p + err := testutil.ModifyTomlConfigFile(ctx, n.logger(), n.DockerClient, n.TestName, n.VolumeName, configPath, c) + if err != nil { + return err + } + + mvCmd := "echo 'starting a validator node'" + if !n.Validator { + mvCmd = fmt.Sprintf(`mv %s/wallet.toml %s/%s`, n.HomeDir(), n.HomeDir(), n.Chain.Config().ChainID) + } + + ledgerCmd := fmt.Sprintf(`%s node --base-dir %s ledger`, n.Chain.Config().Bin, n.HomeDir()) + + cmd := []string{ + "sh", + "-c", + fmt.Sprintf(`%s && %s && %s`, joinNetworkCmd, mvCmd, ledgerCmd), + } + + exposedPorts := nat.PortMap{ + nat.Port(p2pPort): {}, + nat.Port(rpcPort): {}, + } + + netAddr, err := n.netAddress(ctx) + if err != nil { + return err + } + ipAddr := strings.Split(netAddr, ":")[0] + return n.containerLifecycle.CreateContainer(ctx, n.TestName, n.NetworkID, n.Image, exposedPorts, ipAddr, n.Bind(), nil, n.HostName(), cmd, n.Chain.Config().Env, []string{}) +} + +func (n *NamadaNode) StartContainer(ctx context.Context) error { + if err := n.containerLifecycle.StartContainer(ctx); err != nil { + return err + } + + hostPorts, err := n.containerLifecycle.GetHostPorts(ctx, p2pPort, rpcPort) + if err != nil { + return err + } + rpcPort := hostPorts[1] + err = n.NewRPCClient(fmt.Sprintf("tcp://%s", rpcPort)) + if err != nil { + return err + } + + n.hostP2PPort, n.hostRPCPort = hostPorts[0], hostPorts[1] + + time.Sleep(5 * time.Second) + err = n.WaitMaspFileDownload(ctx) + if err != nil { + return fmt.Errorf("failed to download MASP files: %v", err) + } + + return retry.Do(func() error { + stat, err := n.Client.Status(ctx) + if err != nil { + return err + } + if stat != nil && stat.SyncInfo.CatchingUp { + return fmt.Errorf("still catching up: height(%d) catching-up(%t)", + stat.SyncInfo.LatestBlockHeight, stat.SyncInfo.CatchingUp) + } + return nil + }, retry.Context(ctx), retry.DelayType(retry.BackOffDelay)) +} + +func (n *NamadaNode) WaitMaspFileDownload(ctx context.Context) error { + maspDir := ".masp-params" + requiredFiles := []string{ + "masp-spend.params", + "masp-output.params", + "masp-convert.params", + } + + fr := dockerutil.NewFileRetriever(n.logger(), n.DockerClient, n.TestName) + for _, file := range requiredFiles { + relPath := filepath.Join(maspDir, file) + timeout := 5 * time.Minute + timeoutChan := time.After(timeout) + size := -1 + completed := false + for !completed { + select { + case <-timeoutChan: + return fmt.Errorf("downloading masp files isn't completed") + default: + f, err := fr.SingleFileContent(ctx, n.VolumeName, relPath) + if err != nil { + time.Sleep(2 * time.Second) + continue + } + if size != len(f) { + size = len(f) + time.Sleep(2 * time.Second) + continue + } + completed = true + } + } + } + + return nil +} + +func (n *NamadaNode) netAddress(ctx context.Context) (string, error) { + var index int + if n.Validator { + index = n.Index + 128 + } else { + index = n.Index + 192 + } + networkResource, err := n.DockerClient.NetworkInspect(ctx, n.NetworkID, types.NetworkInspectOptions{}) + if err != nil { + return "", fmt.Errorf("failed to get the network resource: %v", err) + } + for _, config := range networkResource.IPAM.Config { + if config.Subnet != "" { + ip, ipNet, err := net.ParseCIDR(config.Subnet) + if err != nil { + return "", fmt.Errorf("failed to parse the subnet: %v", err) + } + + ip = ip.To4() + if ip == nil { + return "", fmt.Errorf("subnet is not IPv4") + } + + ones, bits := ipNet.Mask.Size() + if index < 0 || index >= 1< 0 { cfg.Images[0].Version = s.Version } diff --git a/configuredChains.yaml b/configuredChains.yaml index db3293db8..2c265b402 100644 --- a/configuredChains.yaml +++ b/configuredChains.yaml @@ -558,6 +558,20 @@ lumnetwork: uid-gid: 1025:1025 no-host-mount: false +namada: + name: namada + type: namada + bin: namada + bech32-prefix: tnam + denom: nam + gas-prices: 0.000001tnam1qxgfw7myv4dh0qna4hq0xdg6lx77fzl7dcem8h7e + gas-adjustment: 1.1 + trusting-period: 48h + images: + - repository: ghcr.io/anoma/namada + uid-gid: 1000:1000 + no-host-mount: false + neutron: name: neutron type: cosmos diff --git a/dockerutil/container_lifecycle.go b/dockerutil/container_lifecycle.go index 338447e01..ec2d3ec63 100644 --- a/dockerutil/container_lifecycle.go +++ b/dockerutil/container_lifecycle.go @@ -46,6 +46,7 @@ func (c *ContainerLifecycle) CreateContainer( networkID string, image ibc.DockerImage, ports nat.PortMap, + ipAddr string, volumeBinds []string, mounts []mount.Mount, hostName string, @@ -77,6 +78,17 @@ func (c *ContainerLifecycle) CreateContainer( c.preStartListeners = listeners + var endpointSettings network.EndpointSettings + if ipAddr == "" { + endpointSettings = network.EndpointSettings{} + } else { + endpointSettings = network.EndpointSettings{ + IPAMConfig: &network.EndpointIPAMConfig{ + IPv4Address: ipAddr, + }, + } + } + cc, err := c.client.ContainerCreate( ctx, &container.Config{ @@ -102,7 +114,7 @@ func (c *ContainerLifecycle) CreateContainer( }, &network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ - networkID: {}, + networkID: &endpointSettings, }, }, nil, diff --git a/dockerutil/setup.go b/dockerutil/setup.go index 3143d2c44..643b042e0 100644 --- a/dockerutil/setup.go +++ b/dockerutil/setup.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "fmt" + "math/rand" + "net" "os" "strings" "time" @@ -12,6 +14,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" "github.com/docker/docker/errdefs" ) @@ -76,8 +79,26 @@ func DockerSetup(t DockerSetupTestingT) (*client.Client, string) { DockerCleanup(t, cli)() name := fmt.Sprintf("%s-%s", ICTDockerPrefix, RandLowerCaseLetterString(8)) + octet := uint8(rand.Intn(256)) + baseSubnet := fmt.Sprintf("172.%d.0.0/16", octet) + usedSubnets, err := getUsedSubnets(cli) + if err != nil { + panic(fmt.Errorf("failed to get used subnets: %v", err)) + } + subnet, err := findAvailableSubnet(baseSubnet, usedSubnets) + if err != nil { + panic(fmt.Errorf("failed to find an available subnet: %v", err)) + } network, err := cli.NetworkCreate(context.TODO(), name, types.NetworkCreate{ CheckDuplicate: true, + Driver: "bridge", + IPAM: &network.IPAM{ + Config: []network.IPAMConfig{ + { + Subnet: subnet, + }, + }, + }, Labels: map[string]string{CleanupLabel: t.Name()}, }) @@ -88,6 +109,77 @@ func DockerSetup(t DockerSetupTestingT) (*client.Client, string) { return cli, network.ID } +func getUsedSubnets(cli *client.Client) (map[string]bool, error) { + usedSubnets := make(map[string]bool) + networks, err := cli.NetworkList(context.TODO(), types.NetworkListOptions{}) + if err != nil { + return nil, err + } + + for _, net := range networks { + for _, config := range net.IPAM.Config { + if config.Subnet != "" { + usedSubnets[config.Subnet] = true + } + } + } + return usedSubnets, nil +} + +func findAvailableSubnet(baseSubnet string, usedSubnets map[string]bool) (string, error) { + ip, ipNet, err := net.ParseCIDR(baseSubnet) + if err != nil { + return "", fmt.Errorf("invalid base subnet: %v", err) + } + + for { + if isSubnetUsed(ipNet.String(), usedSubnets) { + incrementIP(ip, 2) + ipNet.IP = ip + continue + } + + for subIP := ip.Mask(ipNet.Mask); ipNet.Contains(subIP); incrementIP(subIP, 1) { + subnet := fmt.Sprintf("%s/24", subIP) + + if !isSubnetUsed(subnet, usedSubnets) { + return subnet, nil + } + } + + incrementIP(ip, 2) + ipNet.IP = ip + } +} + +func isSubnetUsed(subnet string, usedSubnets map[string]bool) bool { + _, targetNet, err := net.ParseCIDR(subnet) + if err != nil { + return true + } + + for usedSubnet := range usedSubnets { + _, usedNet, err := net.ParseCIDR(usedSubnet) + if err != nil { + continue + } + + if usedNet.Contains(targetNet.IP) || targetNet.Contains(usedNet.IP) { + return true + } + } + return false +} + +func incrementIP(ip net.IP, incrementLevel int) { + for j := len(ip) - incrementLevel; j >= 0; j-- { + ip[j]++ + if ip[j] > 0 { + break + } + } +} + // DockerCleanup will clean up Docker containers, networks, and the other various config files generated in testing. func DockerCleanup(t DockerSetupTestingT, cli *client.Client) func() { return func() { diff --git a/examples/namada/namada_chain_test.go b/examples/namada/namada_chain_test.go new file mode 100644 index 000000000..856a03b63 --- /dev/null +++ b/examples/namada/namada_chain_test.go @@ -0,0 +1,285 @@ +package namada_test + +import ( + "context" + "fmt" + stdmath "math" + "strconv" + "testing" + "time" + + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + + "cosmossdk.io/math" + "github.com/strangelove-ventures/interchaintest/v8" + namadachain "github.com/strangelove-ventures/interchaintest/v8/chain/namada" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/relayer" + "github.com/strangelove-ventures/interchaintest/v8/testreporter" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestNamadaNetwork(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + + t.Parallel() + client, network := interchaintest.DockerSetup(t) + + nv := 1 + fn := 0 + + coinDecimals := namadachain.NamTokenDenom + chains, err := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{ + {Name: "gaia", Version: "v19.2.0", ChainConfig: ibc.ChainConfig{ + GasPrices: "1uatom", + }}, + { + Name: "namada", + Version: "v0.44.1", + ChainConfig: ibc.ChainConfig{ + ChainID: "namada-test", + Denom: namadachain.NamAddress, + Gas: "250000", + CoinDecimals: &coinDecimals, + Bin: "namada", + }, + NumValidators: &nv, + NumFullNodes: &fn, + }, + }, + ).Chains(t.Name()) + require.NoError(t, err, "failed to get namada chain") + chain := chains[0] + namada := chains[1].(*namadachain.NamadaChain) + + // Relayer Factory + r := interchaintest.NewBuiltinRelayerFactory(ibc.Hermes, zaptest.NewLogger(t), + relayer.CustomDockerImage( + "ghcr.io/heliaxdev/hermes", + "v1.10.4-namada-beta17-rc2@sha256:a95ede57f63ebb4c70aa4ca0bfb7871a5d43cd76d17b1ad62f5d31a9465d65af", + "2000:2000", + )). + Build(t, client, network) + + // Prep Interchain + const ibcPath = "namada-ibc-test" + ic := interchaintest.NewInterchain(). + AddChain(chain). + AddChain(namada). + AddRelayer(r, "relayer"). + AddLink(interchaintest.InterchainLink{ + Chain1: chain, + Chain2: namada, + Relayer: r, + Path: ibcPath, + }) + + // Log location + f, err := interchaintest.CreateLogFile(fmt.Sprintf("%d.json", time.Now().Unix())) + require.NoError(t, err) + // Reporter/logs + rep := testreporter.NewReporter(f) + eRep := rep.RelayerExecReporter(t) + + ctx := context.Background() + + // Build interchain + require.NoError(t, ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + SkipPathCreation: false, + })) + + t.Cleanup(func() { + err := ic.Close() + if err != nil { + panic(err) + } + }) + + initBalance := math.NewInt(1_000_000) + + gasSpent, _ := strconv.ParseInt(namada.Config().Gas, 10, 64) + namadaGasSpent := math.NewInt(gasSpent) + tokenDenom := math.NewInt(int64(stdmath.Pow(10, float64(*namada.Config().CoinDecimals)))) + namadaInitBalance := initBalance.Mul(tokenDenom) + + users := interchaintest.GetAndFundTestUsers(t, ctx, "user", initBalance, chain, namada) + chainUser := users[0] + namadaUser := users[1] + + chainUserBalInitial, err := chain.GetBalance(ctx, chainUser.FormattedAddress(), chain.Config().Denom) + require.NoError(t, err) + require.True(t, chainUserBalInitial.Equal(initBalance)) + + namadaUserBalInitial, err := namada.GetBalance(ctx, namadaUser.KeyName(), namada.Config().Denom) + require.NoError(t, err) + require.True(t, namadaUserBalInitial.Equal(namadaInitBalance)) + + // Get Channel ID + chainChannelInfo, err := r.GetChannels(ctx, eRep, chain.Config().ChainID) + require.NoError(t, err) + chainChannelID := chainChannelInfo[0].ChannelID + namadaChannelInfo, err := r.GetChannels(ctx, eRep, namada.Config().ChainID) + require.NoError(t, err) + namadaChannelID := namadaChannelInfo[0].ChannelID + + // 1. Send Transaction from the chain to Namada + amountToSend := math.NewInt(1) + dstAddress := namadaUser.FormattedAddress() + transfer := ibc.WalletAmount{ + Address: dstAddress, + Denom: chain.Config().Denom, + Amount: amountToSend, + } + tx, err := chain.SendIBCTransfer(ctx, chainChannelID, chainUser.KeyName(), transfer, ibc.TransferOptions{}) + require.NoError(t, err) + require.NoError(t, tx.Validate()) + + // relay packets + require.NoError(t, r.Flush(ctx, eRep, ibcPath, chainChannelID)) + + // test source wallet has decreased funds + expectedBal := chainUserBalInitial.Sub(amountToSend).Sub(math.NewInt(tx.GasSpent)) + chainUserBalAfter1, err := chain.GetBalance(ctx, chainUser.FormattedAddress(), chain.Config().Denom) + require.NoError(t, err) + require.True(t, chainUserBalAfter1.Equal(expectedBal)) + + // Test destination wallet has increased funds + dstIbcTrace := transfertypes.GetPrefixedDenom("transfer", namadaChannelID, chain.Config().Denom) + namadaUserIbcBalAfter1, err := namada.GetBalance(ctx, namadaUser.KeyName(), dstIbcTrace) + require.NoError(t, err) + require.True(t, namadaUserIbcBalAfter1.Equal(amountToSend)) + + // 2. Send Transaction from Namada to the chain + amountToSend = math.NewInt(1) + dstAddress = chainUser.FormattedAddress() + transfer = ibc.WalletAmount{ + Address: dstAddress, + Denom: namada.Config().Denom, + Amount: amountToSend, + } + tx, err = namada.SendIBCTransfer(ctx, namadaChannelID, namadaUser.KeyName(), transfer, ibc.TransferOptions{}) + require.NoError(t, err) + require.NoError(t, tx.Validate()) + + // relay packets + require.NoError(t, r.Flush(ctx, eRep, ibcPath, namadaChannelID)) + + // test source wallet has decreased funds + expectedBal = namadaUserBalInitial.Sub(amountToSend.Mul(tokenDenom)).Sub(namadaGasSpent) + namadaUserBalAfter2, err := namada.GetBalance(ctx, namadaUser.KeyName(), namada.Config().Denom) + require.NoError(t, err) + require.True(t, namadaUserBalAfter2.Equal(expectedBal)) + + // test destination wallet has increased funds + srcDenomTrace := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom("transfer", chainChannelID, namada.Config().Denom)) + dstIbcDenom := srcDenomTrace.IBCDenom() + chainUserIbcBalAfter2, err := chain.GetBalance(ctx, chainUser.FormattedAddress(), dstIbcDenom) + require.NoError(t, err) + require.True(t, chainUserIbcBalAfter2.Equal(amountToSend.Mul(tokenDenom))) + + // 3. Shielding transfer (chain -> Namada's shielded account) test + // generate a shielded account + users = interchaintest.GetAndFundTestUsers(t, ctx, "shielded", initBalance, namada) + namadaShieldedUser := users[0].(*namadachain.NamadaWallet) + namadaShieldedUserBalInitial, err := namada.GetBalance(ctx, namadaShieldedUser.KeyName(), namada.Config().Denom) + require.NoError(t, err) + require.True(t, namadaShieldedUserBalInitial.Equal(namadaInitBalance)) + + amountToSend = math.NewInt(1) + destAddress, err := namada.GetAddress(ctx, namadaShieldedUser.PaymentAddressKeyName()) + require.NoError(t, err) + transfer = ibc.WalletAmount{ + Address: string(destAddress), + Denom: chain.Config().Denom, + Amount: amountToSend, + } + // generate the IBC shielding transfer from the destination Namada + shieldedTransfer, err := namada.GenIbcShieldingTransfer(ctx, namadaChannelID, transfer, ibc.TransferOptions{}) + require.NoError(t, err) + + // replace the destination address with the MASP address + // because it has been already set in the IBC shielding transfer + transfer.Address = namadachain.MaspAddress + tx, err = chain.SendIBCTransfer(ctx, chainChannelID, chainUser.KeyName(), transfer, ibc.TransferOptions{ + Memo: shieldedTransfer, + }) + require.NoError(t, err) + require.NoError(t, tx.Validate()) + + // relay packets + require.NoError(t, r.Flush(ctx, eRep, ibcPath, chainChannelID)) + + // test source wallet has decreased funds + expectedBal = chainUserBalAfter1.Sub(amountToSend).Sub(math.NewInt(tx.GasSpent)) + chainUserBalAfter3, err := chain.GetBalance(ctx, chainUser.FormattedAddress(), chain.Config().Denom) + require.NoError(t, err) + require.True(t, chainUserBalAfter3.Equal(expectedBal)) + + // test destination wallet has increased funds + dstIbcTrace = transfertypes.GetPrefixedDenom("transfer", namadaChannelID, chain.Config().Denom) + namadaShieldedUserIbcBalAfter3, err := namada.GetBalance(ctx, namadaShieldedUser.KeyName(), dstIbcTrace) + require.NoError(t, err) + require.True(t, namadaShieldedUserIbcBalAfter3.Equal(amountToSend)) + + // 4. Shielded transfer (Shielded account 1 -> Shielded account 2) on Namada + // generate another shielded account + users = interchaintest.GetAndFundTestUsers(t, ctx, "shielded", initBalance, namada) + namadaShieldedUser2 := users[0].(*namadachain.NamadaWallet) + namadaShieldedUser2BalInitial, err := namada.GetBalance(ctx, namadaShieldedUser2.KeyName(), namada.Config().Denom) + require.NoError(t, err) + require.True(t, namadaShieldedUser2BalInitial.Equal(namadaInitBalance)) + + amountToSend = math.NewInt(1) + transfer = ibc.WalletAmount{ + Address: namadaShieldedUser2.FormattedAddress(), + Denom: dstIbcTrace, + Amount: amountToSend, + } + err = namada.ShieldedTransfer(ctx, namadaShieldedUser.KeyName(), transfer) + require.NoError(t, err) + require.NoError(t, tx.Validate()) + + // test source wallet has decreased funds + expectedBal = namadaShieldedUserIbcBalAfter3.Sub(amountToSend) + namadaShieldedUserBalAfter4, err := namada.GetBalance(ctx, namadaShieldedUser.KeyName(), dstIbcTrace) + require.NoError(t, err) + require.True(t, namadaShieldedUserBalAfter4.Equal(expectedBal)) + + // test destination wallet has increased funds + namadaShieldedUser2IbcBalAfter4, err := namada.GetBalance(ctx, namadaShieldedUser2.KeyName(), dstIbcTrace) + require.NoError(t, err) + require.True(t, namadaShieldedUser2IbcBalAfter4.Equal(amountToSend)) + + // 5. Unshielding transfer (Namada's shielded account 2 -> chain) test + amountToSend = math.NewInt(1) + dstAddress = chainUser.FormattedAddress() + transfer = ibc.WalletAmount{ + Address: dstAddress, + Denom: dstIbcTrace, + Amount: amountToSend, + } + tx, err = namada.SendIBCTransfer(ctx, namadaChannelID, namadaShieldedUser2.KeyName(), transfer, ibc.TransferOptions{}) + require.NoError(t, err) + require.NoError(t, tx.Validate()) + + // relay packets + require.NoError(t, r.Flush(ctx, eRep, ibcPath, namadaChannelID)) + + // test source wallet has decreased funds + expectedBal = namadaShieldedUser2IbcBalAfter4.Sub(amountToSend) + namadaShieldedUser2BalAfter5, err := namada.GetBalance(ctx, namadaShieldedUser2.KeyName(), dstIbcTrace) + require.NoError(t, err) + require.True(t, namadaShieldedUser2BalAfter5.Equal(expectedBal)) + + // test destination wallet has increased funds + expectedBal = chainUserBalAfter3.Add(amountToSend) + chainUserIbcBalAfter4, err := chain.GetBalance(ctx, chainUser.FormattedAddress(), chain.Config().Denom) + require.NoError(t, err) + require.True(t, chainUserIbcBalAfter4.Equal(expectedBal)) +} diff --git a/go.mod b/go.mod index 0802b3b5f..a5057e8f6 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 + github.com/tendermint/tendermint v0.38.0-dev github.com/tidwall/gjson v1.17.1 github.com/tyler-smith/go-bip32 v1.0.0 github.com/tyler-smith/go-bip39 v1.1.0 diff --git a/go.sum b/go.sum index 317b3d890..c2f9805b2 100644 --- a/go.sum +++ b/go.sum @@ -1165,6 +1165,8 @@ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDd github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +github.com/tendermint/tendermint v0.38.0-dev h1:yX4zsEgTF9PxlLmhx9XAPTGH2E9FSlqSpHcY7sW7Vb8= +github.com/tendermint/tendermint v0.38.0-dev/go.mod h1:EHKmaqObmcGysoRr7krxXoxxhUDyYWbKvvRYJ9tCGWY= github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= diff --git a/ibc/types.go b/ibc/types.go index d01170bf8..917bbcaad 100644 --- a/ibc/types.go +++ b/ibc/types.go @@ -31,6 +31,7 @@ const ( Ethereum = "ethereum" Thorchain = "thorchain" UTXO = "utxo" + Namada = "namada" ) // ChainConfig defines the chain parameters requires to run an interchaintest testnet for a chain. diff --git a/interchain.go b/interchain.go index 8cca04eaa..64ff03051 100644 --- a/interchain.go +++ b/interchain.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "path/filepath" "github.com/docker/docker/client" "go.uber.org/zap" @@ -12,7 +13,9 @@ import ( sdkmath "cosmossdk.io/math" "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/chain/namada" "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/relayer/hermes" "github.com/strangelove-ventures/interchaintest/v8/testreporter" ) @@ -610,7 +613,7 @@ func (ic *Interchain) generateRelayerWallets(ctx context.Context) error { for r, chains := range relayerChains { for _, c := range chains { // Just an ephemeral unique name, only for the local use of the keyring. - accountName := ic.relayers[r] + "-" + ic.chains[c] + accountName := ic.chains[c] newWallet, err := c.BuildRelayerWallet(ctx, accountName) if err != nil { return err @@ -644,6 +647,19 @@ func (ic *Interchain) configureRelayerKeys(ctx context.Context, rep *testreporte return fmt.Errorf("failed to configure relayer %s for chain %s: %w", ic.relayers[r], chainName, err) } + if c.Config().Type == "namada" { + // Copy Namada wallet to the relayer container + walletPath := filepath.Join("pre-genesis", "wallet.toml") + wallet, err := c.(*namada.NamadaChain).Validators[0].ReadFile(ctx, walletPath) + if err != nil { + return err + } + relativeWalletFilePath := fmt.Sprintf("%s/wallet.toml", c.Config().ChainID) + if err := r.(*hermes.Relayer).WriteFileToHomeDir(ctx, relativeWalletFilePath, wallet); err != nil { + return fmt.Errorf("failed to copy Namada wallet file: %w", err) + } + } + if err := r.RestoreKey(ctx, rep, c.Config(), chainName, diff --git a/relayer/docker.go b/relayer/docker.go index 4ea984994..fef566e46 100644 --- a/relayer/docker.go +++ b/relayer/docker.go @@ -369,7 +369,7 @@ func (r *DockerRelayer) StartRelayer(ctx context.Context, rep ibc.RelayerExecRep r.containerLifecycle = dockerutil.NewContainerLifecycle(r.log, r.client, containerName) if err := r.containerLifecycle.CreateContainer( - ctx, r.testName, r.networkID, containerImage, nil, + ctx, r.testName, r.networkID, containerImage, nil, "", r.Bind(), nil, r.HostName(joinedPaths), cmd, nil, []string{}, ); err != nil { return err diff --git a/relayer/hermes/hermes_config.go b/relayer/hermes/hermes_config.go index 5c1d90771..a4b7fdd33 100644 --- a/relayer/hermes/hermes_config.go +++ b/relayer/hermes/hermes_config.go @@ -4,6 +4,8 @@ import ( "fmt" "strconv" "strings" + + "github.com/strangelove-ventures/interchaintest/v8/ibc" ) // NewConfig returns a hermes Config with an entry for each of the provided ChainConfigs. @@ -18,8 +20,22 @@ func NewConfig(chainConfigs ...ChainConfig) Config { panic(err) } + var chainType string + var accountPrefix string + var trustingPeriod string + switch chainCfg.Type { + case ibc.Namada: + chainType = Namada + accountPrefix = "" + trustingPeriod = "1day" + default: + chainType = Cosmos + accountPrefix = chainCfg.Bech32Prefix + trustingPeriod = "14days" + } chains = append(chains, Chain{ ID: chainCfg.ChainID, + Type: chainType, RPCAddr: hermesCfg.rpcAddr, CCVConsumerChain: false, GrpcAddr: fmt.Sprintf("http://%s", hermesCfg.grpcAddr), @@ -29,7 +45,7 @@ func NewConfig(chainConfigs ...ChainConfig) Config { BatchDelay: "200ms", }, RPCTimeout: "10s", - AccountPrefix: chainCfg.Bech32Prefix, + AccountPrefix: accountPrefix, KeyName: hermesCfg.keyName, AddressType: AddressType{ Derivation: "cosmos", @@ -46,7 +62,7 @@ func NewConfig(chainConfigs ...ChainConfig) Config { MaxTxSize: 2097152, ClockDrift: "5s", MaxBlockTime: "30s", - TrustingPeriod: "14days", + TrustingPeriod: trustingPeriod, TrustThreshold: TrustThreshold{ Numerator: "1", Denominator: "3", @@ -92,6 +108,12 @@ func NewConfig(chainConfigs ...ChainConfig) Config { } } +// Chain type for Hermes, currently only `CosmosSdk` or `Namada` is supported. +const ( + Cosmos = "CosmosSdk" + Namada = "Namada" +) + type Config struct { Global Global `toml:"global"` Mode Mode `toml:"mode"` @@ -173,6 +195,7 @@ type TrustThreshold struct { type Chain struct { ID string `toml:"id"` + Type string `toml:"type"` RPCAddr string `toml:"rpc_addr"` GrpcAddr string `toml:"grpc_addr"` EventSource EventSource `toml:"event_source"` diff --git a/relayer/hermes/hermes_relayer.go b/relayer/hermes/hermes_relayer.go index 19cab3cee..3ad28b814 100644 --- a/relayer/hermes/hermes_relayer.go +++ b/relayer/hermes/hermes_relayer.go @@ -276,12 +276,19 @@ func (r *Relayer) CreateClient(ctx context.Context, rep ibc.RelayerExecReporter, // to copy the contents of the mnemonic into a file on disk and then reference the newly created file. func (r *Relayer) RestoreKey(ctx context.Context, rep ibc.RelayerExecReporter, cfg ibc.ChainConfig, keyName, mnemonic string) error { chainID := cfg.ChainID - relativeMnemonicFilePath := fmt.Sprintf("%s/mnemonic.txt", chainID) - if err := r.WriteFileToHomeDir(ctx, relativeMnemonicFilePath, []byte(mnemonic)); err != nil { - return fmt.Errorf("failed to write mnemonic file: %w", err) - } + var cmd []string + switch cfg.Type { + case "namada": + relativeWalletFilePath := fmt.Sprintf("%s/wallet.toml", chainID) + cmd = []string{hermes, "keys", "add", "--chain", chainID, "--key-file", fmt.Sprintf("%s/%s", r.HomeDir(), relativeWalletFilePath), "--key-name", keyName} + default: + relativeMnemonicFilePath := fmt.Sprintf("%s/mnemonic.txt", chainID) + if err := r.WriteFileToHomeDir(ctx, relativeMnemonicFilePath, []byte(mnemonic)); err != nil { + return fmt.Errorf("failed to write mnemonic file: %w", err) + } - cmd := []string{hermes, "keys", "add", "--chain", chainID, "--mnemonic-file", fmt.Sprintf("%s/%s", r.HomeDir(), relativeMnemonicFilePath), "--key-name", keyName} + cmd = []string{hermes, "keys", "add", "--chain", chainID, "--mnemonic-file", fmt.Sprintf("%s/%s", r.HomeDir(), relativeMnemonicFilePath), "--key-name", keyName} + } // Restoring a key should be near-instantaneous, so add a 1-minute timeout // to detect if Docker has hung.