diff --git a/Makefile b/Makefile index 32eaca44..2e66d963 100644 --- a/Makefile +++ b/Makefile @@ -49,8 +49,7 @@ test: go test -race ./... test-e2e: - cd $(TOOLS_DIR); go install -trimpath $(BABYLON_PKG); - go test -race -mod=readonly -timeout=25m -v $(PACKAGES_E2E) -count=1 --tags=e2e + go test -race -mod=readonly --failfast -timeout=25m -v $(PACKAGES_E2E) -count=1 --tags=e2e build-docker: $(DOCKER) build --tag babylonlabs-io/vigilante -f Dockerfile \ diff --git a/e2etest/babylon_node_handler.go b/e2etest/babylon_node_handler.go deleted file mode 100644 index 4e8ce4e4..00000000 --- a/e2etest/babylon_node_handler.go +++ /dev/null @@ -1,214 +0,0 @@ -package e2etest - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - - bbn "github.com/babylonlabs-io/babylon/types" - "github.com/btcsuite/btcd/btcec/v2" -) - -var ( - // jury - _, juryPK = btcec.PrivKeyFromBytes( - []byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, - ) -) - -func baseDirBabylondir() (string, error) { - tempPath := os.TempDir() - - tempName, err := os.MkdirTemp(tempPath, "BabylonTestVigilante") - if err != nil { - return "", err - } - - err = os.Chmod(tempName, 0755) - - if err != nil { - return "", err - } - - return tempName, nil -} - -type babylonNode struct { - cmd *exec.Cmd - pidFile string - dataDir string -} - -func newBabylonNode(dataDir string, cmd *exec.Cmd) *babylonNode { - return &babylonNode{ - dataDir: dataDir, - cmd: cmd, - } -} - -func (n *babylonNode) start() error { - if err := n.cmd.Start(); err != nil { - return err - } - - pid, err := os.Create(filepath.Join(n.dataDir, - fmt.Sprintf("%s.pid", "config"))) - if err != nil { - return err - } - - n.pidFile = pid.Name() - if _, err = fmt.Fprintf(pid, "%d\n", n.cmd.Process.Pid); err != nil { - return err - } - - if err := pid.Close(); err != nil { - return err - } - - return nil -} - -func (n *babylonNode) stop() (err error) { - if n.cmd == nil || n.cmd.Process == nil { - // return if not properly initialized - // or error starting the process - return nil - } - - defer func() { - err = n.cmd.Wait() - }() - - if runtime.GOOS == "windows" { - return n.cmd.Process.Signal(os.Kill) - } - return n.cmd.Process.Signal(os.Interrupt) -} - -func (n *babylonNode) cleanup() error { - if n.pidFile != "" { - if err := os.Remove(n.pidFile); err != nil { - log.Errorf("unable to remove file %s: %v", n.pidFile, - err) - } - } - - dirs := []string{ - n.dataDir, - } - var err error - for _, dir := range dirs { - if err = os.RemoveAll(dir); err != nil { - log.Errorf("Cannot remove dir %s: %v", dir, err) - } - } - return nil -} - -func (n *babylonNode) shutdown() error { - if err := n.stop(); err != nil { - return err - } - if err := n.cleanup(); err != nil { - return err - } - return nil -} - -type BabylonNodeHandler struct { - babylonNode *babylonNode -} - -func NewBabylonNodeHandler(baseHeaderHex string, slashingPkScript string, epochInterval uint) (*BabylonNodeHandler, error) { - testDir, err := baseDirBabylondir() - if err != nil { - return nil, err - } - - initTestnetCmd := exec.Command( - "babylond", - "testnet", - "--v=1", - fmt.Sprintf("--output-dir=%s", testDir), - "--starting-ip-address=192.168.10.2", - "--keyring-backend=test", - "--chain-id=chain-test", - "--btc-finalization-timeout=4", - "--btc-confirmation-depth=2", - "--additional-sender-account", - "--btc-network=regtest", - "--min-staking-time-blocks=200", - "--min-staking-amount-sat=10000", - fmt.Sprintf("--epoch-interval=%d", epochInterval), - fmt.Sprintf("--slashing-pk-script=%s", slashingPkScript), - fmt.Sprintf("--btc-base-header=%s", baseHeaderHex), - "--covenant-quorum=1", - fmt.Sprintf("--covenant-pks=%s", bbn.NewBIP340PubKeyFromBTCPK(juryPK).MarshalHex()), - ) - - var stderr bytes.Buffer - initTestnetCmd.Stderr = &stderr - - err = initTestnetCmd.Run() - - if err != nil { - // remove the testDir if this fails - _ = os.RemoveAll(testDir) - fmt.Println(fmt.Sprint(err) + ": " + stderr.String()) - return nil, err - } - - nodeDataDir := filepath.Join(testDir, "node0", "babylond") - - f, err := os.Create(filepath.Join(testDir, "babylon.log")) - - if err != nil { - return nil, err - } - - startCmd := exec.Command( - "babylond", - "start", - fmt.Sprintf("--home=%s", nodeDataDir), - "--log_level=debug", - ) - - startCmd.Stdout = f - - log.Info("Successfully created Babylon node") - - return &BabylonNodeHandler{ - babylonNode: newBabylonNode(testDir, startCmd), - }, nil -} - -func (w *BabylonNodeHandler) Start() error { - if err := w.babylonNode.start(); err != nil { - // try to cleanup after start error, but return original error - _ = w.babylonNode.cleanup() - return err - } - - log.Info("Successfully started Babylon node") - - return nil -} - -func (w *BabylonNodeHandler) Stop() error { - if err := w.babylonNode.shutdown(); err != nil { - return err - } - - log.Info("Successfully stopped Babylon node") - - return nil -} - -func (w *BabylonNodeHandler) GetNodeDataDir() string { - dir := filepath.Join(w.babylonNode.dataDir, "node0", "babylond") - return dir -} diff --git a/e2etest/bitcoind_node_setup.go b/e2etest/bitcoind_node_setup.go index e6686eaf..7a7a0969 100644 --- a/e2etest/bitcoind_node_setup.go +++ b/e2etest/bitcoind_node_setup.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "github.com/babylonlabs-io/vigilante/e2etest/container" + "github.com/ory/dockertest/v3" "github.com/stretchr/testify/require" "os" "strconv" @@ -33,17 +34,14 @@ type BitcoindTestHandler struct { m *container.Manager } -func NewBitcoindHandler(t *testing.T) *BitcoindTestHandler { - manager, err := container.NewManager() - require.NoError(t, err) - +func NewBitcoindHandler(t *testing.T, manager *container.Manager) *BitcoindTestHandler { return &BitcoindTestHandler{ t: t, m: manager, } } -func (h *BitcoindTestHandler) Start() { +func (h *BitcoindTestHandler) Start(t *testing.T) *dockertest.Resource { tempPath, err := os.MkdirTemp("", "vigilante-test-*") require.NoError(h.t, err) @@ -51,7 +49,7 @@ func (h *BitcoindTestHandler) Start() { _ = os.RemoveAll(tempPath) }) - _, err = h.m.RunBitcoindResource(tempPath) + bitcoinResource, err := h.m.RunBitcoindResource(t, tempPath) require.NoError(h.t, err) h.t.Cleanup(func() { @@ -65,6 +63,8 @@ func (h *BitcoindTestHandler) Start() { } return err == nil }, startTimeout, 500*time.Millisecond, "bitcoind did not start") + + return bitcoinResource } // GetBlockCount retrieves the current number of blocks in the blockchain from the Bitcoind. @@ -115,3 +115,7 @@ func (h *BitcoindTestHandler) ImportDescriptors(descriptor string) { _, _, err := h.m.ExecBitcoindCliCmd(h.t, []string{"importdescriptors", descriptor}) require.NoError(h.t, err) } + +func (h *BitcoindTestHandler) Stop() { + _ = h.m.ClearResources() +} diff --git a/e2etest/container/config.go b/e2etest/container/config.go index 01b75746..ffc45aca 100644 --- a/e2etest/container/config.go +++ b/e2etest/container/config.go @@ -5,19 +5,24 @@ package container type ImageConfig struct { BitcoindRepository string BitcoindVersion string + BabylonRepository string + BabylonVersion string } //nolint:deadcode const ( dockerBitcoindRepository = "lncm/bitcoind" dockerBitcoindVersionTag = "v27.0" + dockerBabylondRepository = "babylonlabs/babylond" + dockerBabylondVersionTag = "8e0222804ed19b18d74d599b80baa18f05e87d8a" // this is built from commit b1e255a ) // NewImageConfig returns ImageConfig needed for running e2e test. func NewImageConfig() ImageConfig { - config := ImageConfig{ + return ImageConfig{ BitcoindRepository: dockerBitcoindRepository, BitcoindVersion: dockerBitcoindVersionTag, + BabylonRepository: dockerBabylondRepository, + BabylonVersion: dockerBabylondVersionTag, } - return config } diff --git a/e2etest/container/container.go b/e2etest/container/container.go index 7fc39e00..7dff1f55 100644 --- a/e2etest/container/container.go +++ b/e2etest/container/container.go @@ -4,7 +4,12 @@ import ( "bytes" "context" "fmt" + bbn "github.com/babylonlabs-io/babylon/types" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/cometbft/cometbft/libs/rand" + "net" "regexp" + "strconv" "testing" "time" @@ -14,7 +19,15 @@ import ( ) const ( - bitcoindContainerName = "bitcoind-test" + bitcoindContainerName = "bitcoind" + babylondContainerName = "babylond" +) + +var ( + // jury + _, juryPK = btcec.PrivKeyFromBytes( + []byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + ) ) var errRegex = regexp.MustCompile(`(E|e)rror`) @@ -38,6 +51,7 @@ func NewManager() (docker *Manager, err error) { if err != nil { return nil, err } + return docker, nil } @@ -121,15 +135,20 @@ func (m *Manager) ExecCmd(t *testing.T, containerName string, command []string) return outBuf, errBuf, nil } +// RunBitcoindResource starts a bitcoind docker container func (m *Manager) RunBitcoindResource( + t *testing.T, bitcoindCfgPath string, ) (*dockertest.Resource, error) { bitcoindResource, err := m.pool.RunWithOptions( &dockertest.RunOptions{ - Name: bitcoindContainerName, + Name: fmt.Sprintf("%s-%s", bitcoindContainerName, t.Name()), Repository: m.cfg.BitcoindRepository, Tag: m.cfg.BitcoindVersion, User: "root:root", + Labels: map[string]string{ + "e2e": "bitcoind", + }, Mounts: []string{ fmt.Sprintf("%s/:/data/.bitcoin", bitcoindCfgPath), }, @@ -141,14 +160,6 @@ func (m *Manager) RunBitcoindResource( "18443", "18444", }, - PortBindings: map[docker.Port][]docker.PortBinding{ - "8332/tcp": {{HostIP: "", HostPort: "8332"}}, - "8333/tcp": {{HostIP: "", HostPort: "8333"}}, - "28332/tcp": {{HostIP: "", HostPort: "28332"}}, - "28333/tcp": {{HostIP: "", HostPort: "28333"}}, - "18443/tcp": {{HostIP: "", HostPort: "18443"}}, - "18444/tcp": {{HostIP: "", HostPort: "18444"}}, - }, Cmd: []string{ "-regtest", "-txindex", @@ -160,6 +171,16 @@ func (m *Manager) RunBitcoindResource( "-fallbackfee=0.0002", }, }, + func(config *docker.HostConfig) { + config.PortBindings = map[docker.Port][]docker.PortBinding{ + "8332/tcp": {{HostIP: "", HostPort: strconv.Itoa(randomAvailablePort(t))}}, + "8333/tcp": {{HostIP: "", HostPort: strconv.Itoa(randomAvailablePort(t))}}, + "28332/tcp": {{HostIP: "", HostPort: strconv.Itoa(randomAvailablePort(t))}}, + "28333/tcp": {{HostIP: "", HostPort: strconv.Itoa(randomAvailablePort(t))}}, + "18443/tcp": {{HostIP: "", HostPort: strconv.Itoa(randomAvailablePort(t))}}, + "18444/tcp": {{HostIP: "", HostPort: strconv.Itoa(randomAvailablePort(t))}}, + } + }, noRestart, ) if err != nil { @@ -169,11 +190,71 @@ func (m *Manager) RunBitcoindResource( return bitcoindResource, nil } +// RunBabylondResource starts a babylond container +func (m *Manager) RunBabylondResource( + t *testing.T, + mounthPath string, + baseHeaderHex string, + slashingPkScript string, + epochInterval uint, +) (*dockertest.Resource, error) { + cmd := []string{ + "sh", "-c", fmt.Sprintf( + "babylond testnet --v=1 --output-dir=/home --starting-ip-address=192.168.10.2 "+ + "--keyring-backend=test --chain-id=chain-test --btc-finalization-timeout=4 "+ + "--btc-confirmation-depth=2 --additional-sender-account --btc-network=regtest "+ + "--min-staking-time-blocks=200 --min-staking-amount-sat=10000 "+ + "--epoch-interval=%d --slashing-pk-script=%s --btc-base-header=%s "+ + "--covenant-quorum=1 --covenant-pks=%s && babylond start --home=/home/node0/babylond --log_level=debug", + epochInterval, slashingPkScript, baseHeaderHex, bbn.NewBIP340PubKeyFromBTCPK(juryPK).MarshalHex()), + } + + resource, err := m.pool.RunWithOptions( + &dockertest.RunOptions{ + Name: fmt.Sprintf("%s-%s", babylondContainerName, t.Name()), + Repository: m.cfg.BabylonRepository, + Tag: m.cfg.BabylonVersion, + Labels: map[string]string{ + "e2e": "babylond", + }, + User: "root:root", + Mounts: []string{ + fmt.Sprintf("%s/:/home/", mounthPath), + }, + ExposedPorts: []string{ + "1317", + "2345", + "9090", + "26656", + "26657", + }, + Cmd: cmd, + }, + func(config *docker.HostConfig) { + config.PortBindings = map[docker.Port][]docker.PortBinding{ + "1317/tcp": {{HostIP: "", HostPort: strconv.Itoa(randomAvailablePort(t))}}, + "2345/tcp": {{HostIP: "", HostPort: strconv.Itoa(randomAvailablePort(t))}}, + "9090/tcp": {{HostIP: "", HostPort: strconv.Itoa(randomAvailablePort(t))}}, + "26656/tcp": {{HostIP: "", HostPort: strconv.Itoa(randomAvailablePort(t))}}, + "26657/tcp": {{HostIP: "", HostPort: strconv.Itoa(randomAvailablePort(t))}}, + } + }, + noRestart, + ) + if err != nil { + return nil, err + } + + m.resources[babylondContainerName] = resource + + return resource, nil +} + // ClearResources removes all outstanding Docker resources created by the Manager. func (m *Manager) ClearResources() error { for _, resource := range m.resources { if err := m.pool.Purge(resource); err != nil { - return err + continue } } @@ -181,8 +262,41 @@ func (m *Manager) ClearResources() error { } func noRestart(config *docker.HostConfig) { - // in this case we don't want the nodes to restart on failure + // in this case, we don't want the nodes to restart on failure config.RestartPolicy = docker.RestartPolicy{ Name: "no", } } + +// randomAvailablePort tries to find an available TCP port on the localhost +// by testing multiple random ports within a specified range. +func randomAvailablePort(t *testing.T) int { + randPort := func(base, spread int) int { + return base + rand.Intn(spread) + } + + // Base port and spread range for port selection + const ( + basePort = 20000 + portRange = 10000 + ) + + // Seed the random number generator to ensure randomness + rand.Seed(time.Now().UnixNano()) + + // Try up to 5 times to find an available port + for i := 0; i < 5; i++ { + port := randPort(basePort, portRange) + address := fmt.Sprintf("127.0.0.1:%d", port) + + listener, err := net.Listen("tcp", address) + if err == nil { + _ = listener.Close() + return port + } + } + + // If no available port was found, fail the test + t.Fatalf("failed to find an available port in range %d-%d", basePort, basePort+portRange) + return 0 +} diff --git a/e2etest/test_manager.go b/e2etest/test_manager.go index d8b41406..771f4aee 100644 --- a/e2etest/test_manager.go +++ b/e2etest/test_manager.go @@ -6,11 +6,15 @@ import ( "encoding/hex" "encoding/json" "fmt" - "github.com/btcsuite/btcd/txscript" - "go.uber.org/zap" + "os" + "os/exec" "testing" "time" + "github.com/babylonlabs-io/vigilante/e2etest/container" + "github.com/btcsuite/btcd/txscript" + "go.uber.org/zap" + pv "github.com/cosmos/relayer/v2/relayer/provider" bbnclient "github.com/babylonlabs-io/babylon/client/client" @@ -54,11 +58,11 @@ func defaultVigilanteConfig() *config.Config { type TestManager struct { TestRpcClient *rpcclient.Client BitcoindHandler *BitcoindTestHandler - BabylonHandler *BabylonNodeHandler BabylonClient *bbnclient.Client BTCClient *btcclient.Client Config *config.Config WalletPrivKey *btcec.PrivateKey + manger *container.Manager } func initBTCClientWithSubscriber(t *testing.T, cfg *config.Config) *btcclient.Client { @@ -82,13 +86,19 @@ func initBTCClientWithSubscriber(t *testing.T, cfg *config.Config) *btcclient.Cl // StartManager creates a test manager // NOTE: uses btc client with zmq func StartManager(t *testing.T, numMatureOutputsInWallet uint32, epochInterval uint) *TestManager { - btcHandler := NewBitcoindHandler(t) - btcHandler.Start() + manager, err := container.NewManager() + require.NoError(t, err) + + btcHandler := NewBitcoindHandler(t, manager) + bitcoind := btcHandler.Start(t) passphrase := "pass" _ = btcHandler.CreateWallet("default", passphrase) cfg := defaultVigilanteConfig() + cfg.BTC.Endpoint = fmt.Sprintf("127.0.0.1:%s", bitcoind.GetPort("18443/tcp")) + cfg.BTC.ZmqSeqEndpoint = fmt.Sprintf("tcp:// 127.0.0.1:%s", bitcoind.GetPort("28333/tcp")) + testRpcClient, err := rpcclient.New(&rpcclient.ConnConfig{ Host: cfg.BTC.Endpoint, User: cfg.BTC.Username, @@ -121,16 +131,25 @@ func StartManager(t *testing.T, numMatureOutputsInWallet uint32, epochInterval u require.NoError(t, err) // start Babylon node - bh, err := NewBabylonNodeHandler(baseHeaderHex, hex.EncodeToString(pkScript), epochInterval) + + tmpDir, err := tempDir(t) require.NoError(t, err) - err = bh.Start() + + babylond, err := manager.RunBabylondResource(t, tmpDir, baseHeaderHex, hex.EncodeToString(pkScript), epochInterval) require.NoError(t, err) // create Babylon client - cfg.Babylon.KeyDirectory = bh.GetNodeDataDir() + cfg.Babylon.KeyDirectory = "/home/node0/babylond" cfg.Babylon.Key = "test-spending-key" // keyring to bbn node cfg.Babylon.GasAdjustment = 3.0 + fmt.Printf("PATH ----- %s \n", cfg.Babylon.KeyDirectory) + fmt.Printf("%+v\n", cfg.Babylon) + + // update port with the dynamically allocated one from docker + cfg.Babylon.RPCAddr = fmt.Sprintf("http://localhost:%s", babylond.GetPort("26657/tcp")) + cfg.Babylon.GRPCAddr = fmt.Sprintf("https://localhost:%s", babylond.GetPort("9090/tcp")) + babylonClient, err := bbnclient.New(&cfg.Babylon, nil) require.NoError(t, err) @@ -144,21 +163,23 @@ func StartManager(t *testing.T, numMatureOutputsInWallet uint32, epochInterval u return true }, eventuallyWaitTimeOut, eventuallyPollTime) + cmd := exec.Command("ls", "-a", cfg.Babylon.KeyDirectory) + output, _ := cmd.Output() + require.NoError(t, err) + fmt.Printf("-------- %s", string(output)) + return &TestManager{ TestRpcClient: testRpcClient, - BabylonHandler: bh, BabylonClient: babylonClient, BitcoindHandler: btcHandler, BTCClient: btcClient, Config: cfg, WalletPrivKey: walletPrivKey, + manger: manager, } } func (tm *TestManager) Stop(t *testing.T) { - err := tm.BabylonHandler.Stop() - require.NoError(t, err) - if tm.BabylonClient.IsRunning() { err := tm.BabylonClient.Stop() require.NoError(t, err) @@ -262,3 +283,20 @@ func importPrivateKey(btcHandler *BitcoindTestHandler) (*btcec.PrivateKey, error return privKey, nil } + +func tempDir(t *testing.T) (string, error) { + tempPath, err := os.MkdirTemp(os.TempDir(), "babylon-test-*") + if err != nil { + return "", err + } + + if err = os.Chmod(tempPath, 0777); err != nil { + return "", err + } + + t.Cleanup(func() { + _ = os.RemoveAll(tempPath) + }) + + return tempPath, err +}