diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5f49e64b679..5daf71ac34a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -280,6 +280,8 @@ jobs: args: backend=btcd cover=1 - name: bitcoind args: backend=bitcoind cover=1 + - name: bitcoind-miner + args: backend=bitcoind minerbackend=bitcoind - name: bitcoind-notxindex args: backend="bitcoind notxindex" - name: neutrino diff --git a/.gitignore b/.gitignore index 11c67fe65c4..95fe26e5419 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ itest/btcd-itest itest/.logs-* itest/cover +# Local lntest miner logs (dev artifacts) +lntest/miner/*.log + cmd/cmd *.key *.hex diff --git a/itest/lnd_misc_test.go b/itest/lnd_misc_test.go index f6252132f97..33b44ef9e84 100644 --- a/itest/lnd_misc_test.go +++ b/itest/lnd_misc_test.go @@ -1580,7 +1580,7 @@ func testReorgNotifications(ht *lntest.HarnessTest) { // Reorg block1. blockHash1 := block1.Header.BlockHash() - require.NoError(ht, ht.Miner().Client.InvalidateBlock(&blockHash1)) + require.NoError(ht, ht.Miner().InvalidateBlock(&blockHash1)) // Mine empty blocks to evict block1 in bitcoin backend (e.g. bitcoind). ht.Miner().MineEmptyBlocks(2) diff --git a/itest/lnd_nonstd_sweep_test.go b/itest/lnd_nonstd_sweep_test.go index 1e20e2bfe76..47725f12b00 100644 --- a/itest/lnd_nonstd_sweep_test.go +++ b/itest/lnd_nonstd_sweep_test.go @@ -123,12 +123,13 @@ func testNonStdSweepInner(ht *lntest.HarnessTest, address string) { fee = inputVal - outputVal - // Fetch the vsize of the transaction so we can determine if the + // Calculate the vsize of the transaction so we can determine if the // transaction pays >= 1 sat/vbyte. - rawTx := ht.Miner().GetRawTransactionVerbose(txid) + weight := ht.CalculateTxWeight(msgTx) + vbytes := (int64(weight) + 3) / 4 // Require fee >= vbytes. - require.True(ht, fee >= int(rawTx.Vsize)) + require.True(ht, int64(fee) >= vbytes) // Mine a block to keep the mempool clean. ht.MineBlocksAndAssertNumTxes(1, 1) diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go index e6cdca4d81d..b6b8dbcb945 100644 --- a/itest/lnd_open_channel_test.go +++ b/itest/lnd_open_channel_test.go @@ -90,8 +90,7 @@ func testOpenChannelAfterReorg(ht *lntest.HarnessTest) { // open. block := ht.MineBlocksAndAssertNumTxes(10, 1)[0] ht.AssertTxInBlock(block, *fundingTxID) - _, err = tempMiner.Client.Generate(15) - require.NoError(ht, err, "unable to generate blocks") + tempMiner.GenerateBlocks(15) // Ensure the chain lengths are what we expect, with the temp miner // being 5 blocks ahead. @@ -135,8 +134,7 @@ func testOpenChannelAfterReorg(ht *lntest.HarnessTest) { // This should have caused a reorg, and Alice should sync to the longer // chain, where the funding transaction is not confirmed. - _, tempMinerHeight, err := tempMiner.Client.GetBestBlock() - require.NoError(ht, err, "unable to get current blockheight") + _, tempMinerHeight := tempMiner.GetBestBlock() ht.WaitForNodeBlockHeight(alice, tempMinerHeight) // Since the fundingtx was reorged out, Alice should now have no edges @@ -1046,8 +1044,7 @@ func testPendingChannelAfterReorg(ht *lntest.HarnessTest) { // We now cause a fork, by letting our original miner mine 1 blocks, // and our new miner mine 3. - _, err := tempMiner.Client.Generate(3) - require.NoError(ht, err, "unable to generate blocks on temp miner") + tempMiner.GenerateBlocks(3) // Ensure the chain lengths are what we expect, with the temp miner // being 2 blocks ahead. @@ -1072,8 +1069,7 @@ func testPendingChannelAfterReorg(ht *lntest.HarnessTest) { // This should have caused a reorg, and Alice should sync to the longer // chain, where the funding transaction is not confirmed. - _, tempMinerHeight, err := tempMiner.Client.GetBestBlock() - require.NoError(ht, err, "unable to get current blockheight") + _, tempMinerHeight := tempMiner.GetBestBlock() ht.WaitForNodeBlockHeight(alice, tempMinerHeight) // After the reorg, the funding transaction's confirmation is removed, diff --git a/itest/lnd_revocation_test.go b/itest/lnd_revocation_test.go index b975438f1be..54d718f2861 100644 --- a/itest/lnd_revocation_test.go +++ b/itest/lnd_revocation_test.go @@ -589,7 +589,7 @@ func revokedCloseRetributionRemoteHodlCase(ht *lntest.HarnessTest, // NOTE: We don't use `ht.GetRawTransaction` // which asserts a txid must be found as the HTLC // spending txes might be aggregated. - tx, err := ht.Miner().Client.GetRawTransaction(&txid) + tx, err := ht.Miner().GetRawTransactionNoAssert(txid) if err != nil { return nil, err } diff --git a/itest/lnd_test.go b/itest/lnd_test.go index 32c0f1c024e..7f3d81154ef 100644 --- a/itest/lnd_test.go +++ b/itest/lnd_test.go @@ -15,6 +15,7 @@ import ( "github.com/btcsuite/btcd/integration/rpctest" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/miner" "github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lntest/port" "github.com/lightningnetwork/lnd/lntest/wait" @@ -86,6 +87,12 @@ var ( lndExecutable = flag.String( "lndexec", itestLndBinary, "full path to lnd binary", ) + + // minerBackendFlag selects which miner backend to use. If not set, a + // default miner is used. + minerBackendFlag = flag.String( + "minerbackend", "", "miner backend (btcd, bitcoind)", + ) ) // TestLightningNetworkDaemon performs a series of integration tests amongst a @@ -104,8 +111,15 @@ func TestLightningNetworkDaemon(t *testing.T) { // Get the binary path and setup the harness test. binary := getLndBinary(t) - harnessTest := lntest.SetupHarness( - t, binary, *dbBackendFlag, *nativeSQLFlag, feeService, + var minerCfg *miner.MinerConfig + if minerBackendFlag != nil && *minerBackendFlag != "" { + minerCfg = &miner.MinerConfig{ + Backend: *minerBackendFlag, + } + } + + harnessTest := lntest.SetupHarnessWithMinerConfig( + t, binary, *dbBackendFlag, *nativeSQLFlag, feeService, minerCfg, ) defer harnessTest.Stop() diff --git a/lntest/bitcoind_common.go b/lntest/bitcoind_common.go index 6d5cacc43b8..9cbb3731085 100644 --- a/lntest/bitcoind_common.go +++ b/lntest/bitcoind_common.go @@ -4,6 +4,7 @@ package lntest import ( + "encoding/json" "errors" "fmt" "os" @@ -72,7 +73,20 @@ func (b BitcoindBackendConfig) ConnectMiner() error { // DisconnectMiner is called to disconnect the miner. func (b BitcoindBackendConfig) DisconnectMiner() error { - return b.rpcClient.AddNode(b.minerAddr, rpcclient.ANRemove) + // `addnode remove` removes from the addnode list, but doesn't reliably + // disconnect an existing connection. Use `disconnectnode` first. + _, err := b.rpcClient.RawRequest( + "disconnectnode", + []json.RawMessage{ + []byte(fmt.Sprintf("%q", b.minerAddr)), + }, + ) + if err != nil { + return err + } + + _ = b.rpcClient.AddNode(b.minerAddr, rpcclient.ANRemove) + return nil } // Credentials returns the rpc username, password and host for the backend. @@ -124,7 +138,8 @@ func newBackend(miner string, netParams *chaincfg.Params, extraArgs []string, cmdArgs := []string{ "-datadir=" + tempBitcoindDir, - "-whitelist=127.0.0.1", // whitelist localhost to speed up relay + // Whitelist localhost to speed up relay. + "-whitelist=127.0.0.1", "-rpcauth=weks:469e9bb14ab2360f8e226efed5ca6f" + "d$507c670e800a95284294edb5773b05544b" + "220110063096c221be9933c82d38e1", diff --git a/lntest/harness.go b/lntest/harness.go index 21c32a531fe..14d59e849d9 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -1,6 +1,7 @@ package lntest import ( + "bytes" "context" "fmt" "runtime/debug" @@ -54,8 +55,15 @@ const ( // mining blocks. maxBlocksAllowed = 100 - finalCltvDelta = routing.MinCLTVDelta // 18. - thawHeightDelta = finalCltvDelta * 2 // 36. + // finalCltvDelta is the min CLTV delta used by the router. + // + // At the time of writing, this is 18 blocks (routing.MinCLTVDelta). + finalCltvDelta = routing.MinCLTVDelta + + // thawHeightDelta defines how far in the future we pick thaw heights. + // + // At the time of writing, this is 36 blocks (finalCltvDelta * 2). + thawHeightDelta = finalCltvDelta * 2 ) var ( @@ -98,7 +106,8 @@ type HarnessTest struct { // runCtx is a context with cancel method. It's used to signal when the // node needs to quit, and used as the parent context when spawning // children contexts for RPC requests. - runCtx context.Context //nolint:containedctx + //nolint:containedctx + runCtx context.Context cancel context.CancelFunc // stopChainBackend points to the cleanup function returned by the @@ -506,8 +515,7 @@ func (h *HarnessTest) NewNode(name string, require.NoError(h, err, "failed to start node %s", node.Name()) // Get the miner's best block hash. - bestBlock, err := h.miner.Client.GetBestBlockHash() - require.NoError(h, err, "unable to get best block hash") + bestBlock, _ := h.miner.GetBestBlock() // Wait until the node's chain backend is synced to the miner's best // block. @@ -1333,7 +1341,8 @@ func (h *HarnessTest) CloseChannelAssertPending(hn *node.HarnessNode, return nil, nil } - pendingClose, ok := event.Update.(*lnrpc.CloseStatusUpdate_ClosePending) //nolint:ll + //nolint:ll + pendingClose, ok := event.Update.(*lnrpc.CloseStatusUpdate_ClosePending) require.Truef(h, ok, "expected channel close "+ "update, instead got %v", pendingClose) @@ -2265,10 +2274,14 @@ func (h *HarnessTest) GetOutputIndex(txid chainhash.Hash, addr string) int { p2trOutputIndex := -1 for i, txOut := range tx.MsgTx().TxOut { _, addrs, _, err := txscript.ExtractPkScriptAddrs( - txOut.PkScript, h.miner.ActiveNet, + txOut.PkScript, miner.HarnessNetParams, ) require.NoError(h, err) + if len(addrs) == 0 { + continue + } + if addrs[0].String() == addr { p2trOutputIndex = i } @@ -2575,14 +2588,43 @@ func (h *HarnessTest) DeriveFundingShim(alice, bob *node.HarnessNode, } var txid *chainhash.Hash + var outputIndex uint32 targetOutputs := []*wire.TxOut{fundingOutput} + + findFundingOutputIndex := func(tx *wire.MsgTx) uint32 { + for i, out := range tx.TxOut { + if out.Value != fundingOutput.Value { + continue + } + if !bytes.Equal(out.PkScript, fundingOutput.PkScript) { + continue + } + + return uint32(i) + } + + require.Failf( + h, "funding output not found", + "funding output not found in tx %v", txid, + ) + + return 0 + } + if publish { txid = h.SendOutputsWithoutChange(targetOutputs, 5) + + // If we published the funding transaction, then we need to + // look it up in the mempool to locate the actual output + // index. + tx := h.GetRawTransaction(*txid).MsgTx() + outputIndex = findFundingOutputIndex(tx) } else { tx := h.CreateTransaction(targetOutputs, 5) txHash := tx.TxHash() txid = &txHash + outputIndex = findFundingOutputIndex(tx) } // At this point, we can being our external channel funding workflow. @@ -2597,6 +2639,7 @@ func (h *HarnessTest) DeriveFundingShim(alice, bob *node.HarnessNode, FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ FundingTxidBytes: txid[:], }, + OutputIndex: outputIndex, } chanPointShim := &lnrpc.ChanPointShim{ Amt: int64(chanSize), diff --git a/lntest/harness_miner.go b/lntest/harness_miner.go index 010c3f8ca28..bc24b5e8453 100644 --- a/lntest/harness_miner.go +++ b/lntest/harness_miner.go @@ -321,7 +321,9 @@ func (h *HarnessTest) AssertMinerBlockHeightDelta( func (h *HarnessTest) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (chainhash.Hash, error) { - txid, err := h.miner.Client.SendRawTransaction(tx, allowHighFees) + // Use the miner's SendRawTransaction method which handles both + // btcd and bitcoind backends. + txid, err := h.miner.SendRawTransaction(tx, allowHighFees) require.NoError(h, err) return *txid, nil diff --git a/lntest/harness_setup.go b/lntest/harness_setup.go index 166880baece..c5c0a1d4666 100644 --- a/lntest/harness_setup.go +++ b/lntest/harness_setup.go @@ -22,6 +22,18 @@ import ( func SetupHarness(t *testing.T, binaryPath, dbBackendName string, nativeSQL bool, feeService WebFeeService) *HarnessTest { + return SetupHarnessWithMinerConfig( + t, binaryPath, dbBackendName, nativeSQL, feeService, nil, + ) +} + +// SetupHarnessWithMinerConfig is identical to SetupHarness, but allows callers +// to supply a miner configuration. This can be used to select alternative miner +// backends (e.g. bitcoind) without relying on environment variables. +func SetupHarnessWithMinerConfig(t *testing.T, binaryPath, + dbBackendName string, nativeSQL bool, feeService WebFeeService, + minerCfg *miner.MinerConfig) *HarnessTest { + t.Log("Setting up HarnessTest...") // Parse testing flags that influence our test execution. @@ -36,7 +48,7 @@ func SetupHarness(t *testing.T, binaryPath, dbBackendName string, // Init the miner. t.Log("Prepare the miner and mine blocks to activate segwit...") - miner := prepareMiner(ht.runCtx, ht.T) + miner := prepareMiner(ht.runCtx, ht.T, minerCfg) // Start a chain backend. chainBackend, cleanUp := prepareChainBackend(t, miner.P2PAddress()) @@ -59,27 +71,40 @@ func SetupHarness(t *testing.T, binaryPath, dbBackendName string, return ht } -// prepareMiner creates an instance of the btcd's rpctest.Harness that will act -// as the miner for all tests. This will be used to fund the wallets of the -// nodes within the test network and to drive blockchain related events within -// the network. Revert the default setting of accepting non-standard -// transactions on simnet to reject them. Transactions on the lightning network -// should always be standard to get better guarantees of getting included in to -// blocks. -func prepareMiner(ctxt context.Context, t *testing.T) *miner.HarnessMiner { - m := miner.NewMiner(ctxt, t) - - // Before we start anything, we want to overwrite some of the - // connection settings to make the tests more robust. We might need to - // restart the miner while there are already blocks present, which will - // take a bit longer than the 1 second the default settings amount to. - // Doubling both values will give us retries up to 4 seconds. - m.MaxConnRetries = rpctest.DefaultMaxConnectionRetries * 2 - m.ConnectionRetryTimeout = rpctest.DefaultConnectionRetryTimeout * 2 - - // Set up miner and connect chain backend to it. - require.NoError(t, m.SetUp(true, 50)) - require.NoError(t, m.Client.NotifyNewTransactions(false)) +// prepareMiner creates an instance of the miner that will act as the miner +// for all tests. This will be used to fund the wallets of the nodes within +// the test network and to drive blockchain related events within the network. +func prepareMiner(ctxt context.Context, t *testing.T, + minerCfg *miner.MinerConfig) *miner.HarnessMiner { + + var m *miner.HarnessMiner + switch { + case minerCfg != nil: + t.Logf("Using miner backend=%s", minerCfg.Backend) + m = miner.NewMinerWithConfig(ctxt, t, minerCfg) + + default: + // Default to btcd for backward compatibility. + t.Log("Using miner backend=btcd") + m = miner.NewMiner(ctxt, t) + } + + // For btcd, we can optimize connection settings. + // + // Before we start anything, we want to overwrite some of the connection + // settings to make the tests more robust. We might need to restart the + // miner while there are already blocks present, which will take a bit + // longer than the 1 second the default settings amount to. Doubling + // both values will give us retries up to 4 seconds. + if m.Harness != nil { + m.MaxConnRetries = rpctest.DefaultMaxConnectionRetries * 2 + m.ConnectionRetryTimeout = + rpctest.DefaultConnectionRetryTimeout * 2 + } + + // Start the miner. + require.NoError(t, m.Start(true, 50)) + require.NoError(t, m.NotifyNewTransactions(false)) // Next mine enough blocks in order for segwit and the CSV package // soft-fork to activate on SimNet. diff --git a/lntest/miner/bitcoind_miner.go b/lntest/miner/bitcoind_miner.go new file mode 100644 index 00000000000..c69028a4c6a --- /dev/null +++ b/lntest/miner/bitcoind_miner.go @@ -0,0 +1,839 @@ +package miner + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "math" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lntest/port" +) + +// BitcoindMinerBackend implements MinerBackend using bitcoind. +type BitcoindMinerBackend struct { + *testing.T + + // runCtx is a context with cancel method. + //nolint:containedctx + runCtx context.Context + cancel context.CancelFunc + + // bitcoind process and configuration. + cmd *exec.Cmd + dataDir string + rpcClient *rpcclient.Client + rpcHost string + rpcUser string + rpcPass string + p2pPort int + logPath string + logFilename string +} + +type fundRawTransactionResp struct { + Hex string `json:"hex"` +} + +type signRawTransactionResp struct { + Hex string `json:"hex"` + Complete bool `json:"complete"` +} + +type generateBlockResp struct { + Hash string `json:"hash"` +} + +type mempoolInfoResp struct { + MempoolMinFee float64 `json:"mempoolminfee"` +} + +type networkInfoResp struct { + RelayFee float64 `json:"relayfee"` +} + +func btcStringFromSats(sats int64) string { + sign := "" + if sats < 0 { + sign = "-" + sats = -sats + } + + whole := sats / 1e8 + frac := sats % 1e8 + + return fmt.Sprintf("%s%d.%08d", sign, whole, frac) +} + +// NewBitcoindMinerBackend creates a new bitcoind miner backend. +func NewBitcoindMinerBackend(ctxb context.Context, t *testing.T, + config *MinerConfig) *BitcoindMinerBackend { + + t.Helper() + + logDir := config.LogDir + if logDir == "" { + logDir = minerLogDir + } + + logFilename := config.LogFilename + if logFilename == "" { + logFilename = "output_bitcoind_miner.log" + } + + baseLogPath := fmt.Sprintf("%s/%s", node.GetLogDir(), logDir) + + ctxt, cancel := context.WithCancel(ctxb) + + return &BitcoindMinerBackend{ + T: t, + runCtx: ctxt, + cancel: cancel, + logPath: baseLogPath, + logFilename: logFilename, + rpcUser: "miner", + rpcPass: "minerpass", + } +} + +// Start starts the bitcoind miner backend. +func (b *BitcoindMinerBackend) Start(setupChain bool, + numMatureOutputs uint32) error { + // Create temporary directory for bitcoind data. + tempDir, err := os.MkdirTemp("", "bitcoind-miner") + if err != nil { + return fmt.Errorf("unable to create temp directory: %w", err) + } + b.dataDir = tempDir + + // Create log directory if it doesn't exist. + if err := os.MkdirAll(b.logPath, 0700); err != nil { + return fmt.Errorf("unable to create log directory: %w", err) + } + + logFile, err := filepath.Abs(b.logPath + "/bitcoind.log") + if err != nil { + return fmt.Errorf("unable to get absolute log path: %w", err) + } + + // Generate ports. + rpcPort := port.NextAvailablePort() + b.p2pPort = port.NextAvailablePort() + b.rpcHost = fmt.Sprintf("127.0.0.1:%d", rpcPort) + + // Build bitcoind command arguments. + cmdArgs := []string{ + "-datadir=" + b.dataDir, + "-regtest", + "-txindex", + // Whitelist localhost to speed up relay. + "-whitelist=127.0.0.1", + fmt.Sprintf("-rpcuser=%s", b.rpcUser), + fmt.Sprintf("-rpcpassword=%s", b.rpcPass), + fmt.Sprintf("-rpcport=%d", rpcPort), + fmt.Sprintf("-bind=127.0.0.1:%d", b.p2pPort), + "-rpcallowip=127.0.0.1", + "-server", + // Run in foreground for easier process management. + "-daemon=0", + // 0x20000002 signals SegWit activation (BIP141 bit 1). + "-blockversion=536870914", + "-debug", + "-debuglogfile=" + logFile, + // Set fallback fee for transaction creation. + "-fallbackfee=0.00001", + } + + // Start bitcoind process. + b.cmd = exec.Command("bitcoind", cmdArgs...) + + // Discard stdout and stderr to prevent output noise in tests. + // All debug output goes to the log file via -debuglogfile. + b.cmd.Stdout = nil + b.cmd.Stderr = nil + + if err := b.cmd.Start(); err != nil { + _ = b.cleanup() + return fmt.Errorf("couldn't start bitcoind: %w", err) + } + + // Create RPC client config. + rpcCfg := rpcclient.ConnConfig{ + Host: b.rpcHost, + User: b.rpcUser, + Pass: b.rpcPass, + DisableConnectOnNew: true, + DisableAutoReconnect: false, + DisableTLS: true, + HTTPPostMode: true, + } + + client, err := rpcclient.New(&rpcCfg, nil) + if err != nil { + _ = b.stopProcess() + _ = b.cleanup() + return fmt.Errorf("unable to create rpc client: %w", err) + } + b.rpcClient = client + + // Wait for bitcoind to be ready (with retries). Use GetBlockCount which + // is more universally supported. Bitcoind can take a while to start, + // especially on first run, so we give it up to 2 minutes. + maxRetries := 120 + retryDelay := 1 * time.Second + + for i := 0; i < maxRetries; i++ { + _, err = b.rpcClient.GetBlockCount() + if err == nil { + // Successfully connected! + break + } + + // Check if process is still running. + if b.cmd.Process != nil { + // Process is running, wait and retry. + time.Sleep(retryDelay) + continue + } + + // Process died. + _ = b.cleanup() + + return fmt.Errorf("bitcoind process died during startup") + } + + if err != nil { + _ = b.stopProcess() + _ = b.cleanup() + return fmt.Errorf("unable to connect to bitcoind after %d "+ + "retries: %w", maxRetries, err) + } + + // Create a default wallet for the miner using raw RPC. + _, err = b.rpcClient.RawRequest("createwallet", []json.RawMessage{ + []byte(`"miner"`), + }) + if err != nil { + _ = b.stopProcess() + _ = b.cleanup() + return fmt.Errorf("unable to create wallet: %w", err) + } + + if setupChain { + // Generate initial blocks to fund the wallet with mature + // coinbase outputs. + // + // Coinbase outputs mature after 100 confirmations. In order to + // have numMatureOutputs mature outputs available, we mine: + // 100 + numMatureOutputs + // blocks. + // + // Use legacy addresses to ensure compatibility with btcd before + // SegWit is fully activated. + // + // Params: label, address_type. + addrResult, err := b.rpcClient.RawRequest( + "getnewaddress", []json.RawMessage{ + []byte(`""`), + []byte(`"legacy"`), + }, + ) + if err != nil { + _ = b.stopProcess() + _ = b.cleanup() + return fmt.Errorf("unable to get new address: %w", err) + } + + var addrStr string + if err := json.Unmarshal(addrResult, &addrStr); err != nil { + _ = b.stopProcess() + _ = b.cleanup() + return fmt.Errorf("unable to parse address: %w", err) + } + + addr, err := btcutil.DecodeAddress(addrStr, HarnessNetParams) + if err != nil { + _ = b.stopProcess() + _ = b.cleanup() + return fmt.Errorf("unable to decode address: %w", err) + } + + initialBlocks := int64(100 + numMatureOutputs) + _, err = b.rpcClient.GenerateToAddress(initialBlocks, addr, nil) + if err != nil { + _ = b.stopProcess() + _ = b.cleanup() + return fmt.Errorf("unable to generate initial blocks: "+ + "%w", err) + } + } + + return nil +} + +func (b *BitcoindMinerBackend) minRelayFeeBTCPerKVb() float64 { + // If we fail to query the min relay fee, return 0 so callers can + // continue with their requested fee rate. + if b.rpcClient == nil { + return 0 + } + + var ( + relayFeeBTCPerKVb float64 + mempoolMinFeeBTCPerKVb float64 + ) + + networkInfoJSON, err := b.rpcClient.RawRequest("getnetworkinfo", nil) + if err == nil { + var ni networkInfoResp + if json.Unmarshal(networkInfoJSON, &ni) == nil { + relayFeeBTCPerKVb = ni.RelayFee + } + } + + mempoolInfoJSON, err := b.rpcClient.RawRequest("getmempoolinfo", nil) + if err == nil { + var mi mempoolInfoResp + if json.Unmarshal(mempoolInfoJSON, &mi) == nil { + mempoolMinFeeBTCPerKVb = mi.MempoolMinFee + } + } + + if relayFeeBTCPerKVb > mempoolMinFeeBTCPerKVb { + return relayFeeBTCPerKVb + } + + return mempoolMinFeeBTCPerKVb +} + +func txFromHex(hexStr string) (*wire.MsgTx, error) { + rawTxBytes, err := hex.DecodeString(hexStr) + if err != nil { + return nil, fmt.Errorf("decode tx hex: %w", err) + } + + tx := &wire.MsgTx{} + err = tx.Deserialize(bytes.NewReader(rawTxBytes)) + if err != nil { + return nil, fmt.Errorf("deserialize tx: %w", err) + } + + return tx, nil +} + +// Stop stops the bitcoind miner backend and performs cleanup. +func (b *BitcoindMinerBackend) Stop() error { + b.cancel() + + // Close RPC client. + if b.rpcClient != nil { + b.rpcClient.Disconnect() + } + + // Stop bitcoind process. + _ = b.stopProcess() + + // Copy logs and cleanup. + b.saveLogs() + + return b.cleanup() +} + +// stopProcess stops the bitcoind process gracefully or forcefully. +func (b *BitcoindMinerBackend) stopProcess() error { + if b.cmd == nil || b.cmd.Process == nil { + return nil + } + + // Try to stop bitcoind gracefully via RPC if client is available. + if b.rpcClient != nil { + _, _ = b.rpcClient.RawRequest("stop", nil) + // Give it a moment to shutdown gracefully. + time.Sleep(500 * time.Millisecond) + } + + // Kill the process if it's still running. + _ = b.cmd.Process.Kill() + + // Wait for the process to exit to ensure it releases file handles. + _ = b.cmd.Wait() + + return nil +} + +// cleanup removes temporary directories. +func (b *BitcoindMinerBackend) cleanup() error { + if b.dataDir != "" { + if err := os.RemoveAll(b.dataDir); err != nil { + return fmt.Errorf("cannot remove data dir %s: %w", + b.dataDir, err) + } + } + + return nil +} + +// saveLogs copies the bitcoind log file. +func (b *BitcoindMinerBackend) saveLogs() { + logFile := b.logPath + "/bitcoind.log" + logDestination := fmt.Sprintf("%s/../%s", b.logPath, b.logFilename) + + err := node.CopyFile(logDestination, logFile) + if err != nil { + // Log error but don't fail. + b.Logf("Unable to copy log file: %v", err) + } + + err = os.RemoveAll(b.logPath) + if err != nil { + // Log error but don't fail. + b.Logf("Cannot remove log dir %s: %v", b.logPath, err) + } +} + +// GetBestBlock returns the hash and height of the best block. +func (b *BitcoindMinerBackend) GetBestBlock() (*chainhash.Hash, int32, error) { + // GetBestBlock is a btcd-specific method. For bitcoind, we need to use + // GetBlockCount and GetBestBlockHash separately. + hash, err := b.rpcClient.GetBestBlockHash() + if err != nil { + return nil, 0, err + } + + height, err := b.rpcClient.GetBlockCount() + if err != nil { + return nil, 0, err + } + + return hash, int32(height), nil +} + +// GetRawMempool returns all transaction hashes in the mempool. +func (b *BitcoindMinerBackend) GetRawMempool() ([]*chainhash.Hash, error) { + return b.rpcClient.GetRawMempool() +} + +// Generate mines a specified number of blocks. +func (b *BitcoindMinerBackend) Generate(blocks uint32) ([]*chainhash.Hash, + error) { + + // First create an address to mine to. + addr, err := b.NewAddress() + if err != nil { + return nil, fmt.Errorf("unable to get new address: %w", err) + } + + return b.rpcClient.GenerateToAddress(int64(blocks), addr, nil) +} + +// GetBlock returns the block for the given block hash. +func (b *BitcoindMinerBackend) GetBlock(blockHash *chainhash.Hash) ( + *wire.MsgBlock, error) { + + return b.rpcClient.GetBlock(blockHash) +} + +// GetRawTransaction returns the raw transaction for the given txid. +func (b *BitcoindMinerBackend) GetRawTransaction(txid *chainhash.Hash) ( + *btcutil.Tx, error) { + + return b.rpcClient.GetRawTransaction(txid) +} + +// InvalidateBlock marks a block as invalid, triggering a reorg. +func (b *BitcoindMinerBackend) InvalidateBlock( + blockHash *chainhash.Hash) error { + + _, err := b.rpcClient.RawRequest( + "invalidateblock", + []json.RawMessage{ + []byte(fmt.Sprintf("%q", blockHash.String())), + }, + ) + + return err +} + +// NotifyNewTransactions registers for new transaction notifications. Bitcoind +// doesn't expose btcd-style tx notifications through the btcd rpcclient +// wrapper, so this is a no-op. +func (b *BitcoindMinerBackend) NotifyNewTransactions(_ bool) error { + return nil +} + +// SendOutputsWithoutChange creates and broadcasts a transaction with the given +// outputs using the specified fee rate. +func (b *BitcoindMinerBackend) SendOutputsWithoutChange(outputs []*wire.TxOut, + feeRate btcutil.Amount) (*chainhash.Hash, error) { + + tx, err := b.CreateTransaction(outputs, feeRate) + if err != nil { + return nil, err + } + + return b.SendRawTransaction(tx, true) +} + +// CreateTransaction creates a transaction with the given outputs. +func (b *BitcoindMinerBackend) CreateTransaction(outputs []*wire.TxOut, + feeRate btcutil.Amount) (*wire.MsgTx, error) { + + // Extract destinations (address -> amount) in BTC. + destinations := make(map[string]json.RawMessage, len(outputs)) + for _, output := range outputs { + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + output.PkScript, HarnessNetParams, + ) + if err != nil { + return nil, fmt.Errorf("extract address: %w", err) + } + if len(addrs) == 0 { + return nil, fmt.Errorf("no address in output") + } + + destinations[addrs[0].String()] = []byte( + btcStringFromSats(output.Value), + ) + } + + destinationsJSON, err := json.Marshal(destinations) + if err != nil { + return nil, fmt.Errorf("marshal destinations: %w", err) + } + + // 1) Create raw tx with no inputs. + createResp, err := b.rpcClient.RawRequest( + "createrawtransaction", + []json.RawMessage{ + []byte(`[]`), + destinationsJSON, + }, + ) + if err != nil { + return nil, fmt.Errorf("createrawtransaction: %w", err) + } + + var rawHex string + if err := json.Unmarshal(createResp, &rawHex); err != nil { + return nil, fmt.Errorf("parse createrawtransaction resp: %w", + err) + } + + // 2) Fund the tx using the miner wallet without broadcasting. + // + // The fee rate coming from lntest is in sat/kw. Bitcoind expects + // BTC/kvB. + // + // sat/kw -> sat/kvB: multiply by 4 (1000 weight units is 250 vbytes). + feeRateSatPerKVb := int64(feeRate) * 4 + minFeeRateBTCPerKVb := b.minRelayFeeBTCPerKVb() + minFeeSatPerKVb := int64(math.Ceil(minFeeRateBTCPerKVb * 1e8)) + if feeRateSatPerKVb < minFeeSatPerKVb { + feeRateSatPerKVb = minFeeSatPerKVb + } + + feeRateOpt := btcStringFromSats(feeRateSatPerKVb) + fundOpts, err := json.Marshal(map[string]interface{}{ + // Bitcoin Core supports two distinct fee rate options: + // - fee_rate: sat/vB + // - feeRate: BTC/kvB + // + // We use feeRate (BTC/kvB) because lntest uses btcutil.Amount + // and we already clamp against getnetworkinfo/getmempoolinfo + // which are expressed in BTC/kvB. + "feeRate": json.RawMessage(feeRateOpt), + "changePosition": len(outputs), + "lockUnspents": true, + }) + if err != nil { + return nil, fmt.Errorf("marshal fundrawtransaction opts: %w", + err) + } + + fundResp, err := b.rpcClient.RawRequest( + "fundrawtransaction", + []json.RawMessage{ + []byte(fmt.Sprintf("%q", rawHex)), fundOpts, + }, + ) + if err != nil { + return nil, fmt.Errorf("fundrawtransaction (outputs=%s, "+ + "fee_rate=%s, changePosition=%d): %w", + string(destinationsJSON), feeRateOpt, len(outputs), err) + } + + var funded fundRawTransactionResp + if err := json.Unmarshal(fundResp, &funded); err != nil { + return nil, fmt.Errorf("parse fundrawtransaction resp: %w", + err) + } + + // 3) Sign the funded tx using the miner wallet. + signResp, err := b.rpcClient.RawRequest( + "signrawtransactionwithwallet", + []json.RawMessage{ + []byte(fmt.Sprintf("%q", funded.Hex)), + }, + ) + if err != nil { + return nil, fmt.Errorf("signrawtransactionwithwallet: %w", err) + } + + var signed signRawTransactionResp + if err := json.Unmarshal(signResp, &signed); err != nil { + return nil, fmt.Errorf("parse signrawtransaction resp: %w", + err) + } + if !signed.Complete { + return nil, fmt.Errorf("signrawtransactionwithwallet " + + "incomplete") + } + + return txFromHex(signed.Hex) +} + +// SendOutputs creates and broadcasts a transaction with the given outputs. +func (b *BitcoindMinerBackend) SendOutputs(outputs []*wire.TxOut, + feeRate btcutil.Amount) (*chainhash.Hash, error) { + + return b.SendOutputsWithoutChange(outputs, feeRate) +} + +// GenerateAndSubmitBlock generates a block with the given transactions. +func (b *BitcoindMinerBackend) GenerateAndSubmitBlock(txes []*btcutil.Tx, + blockVersion int32, blockTime time.Time) (*btcutil.Block, error) { + + _ = blockVersion + _ = blockTime + + // Generate a block that includes only the specified transactions. + addr, err := b.NewAddress() + if err != nil { + return nil, fmt.Errorf("unable to get new address: %w", err) + } + + // `generateblock` is available on Bitcoin Core regtest, and lets us + // mine blocks without pulling in arbitrary mempool transactions. + // + // `generateblock` has existed in multiple forms across Bitcoin Core + // versions. We try the following strategies, in order: + // + // 1. Pass raw tx hex strings (doesn't require mempool acceptance, + // avoids policy issues like RBF replacement checks). + // 2. Pass txids after submitting to mempool. + // 3. Fallback to `generatetoaddress`. + var ( + resp json.RawMessage + generateErrHex error + generateErrID error + ) + + // Strategy 1: try `generateblock` with raw tx hex strings. + rawTxs := make([]string, 0, len(txes)) + for _, tx := range txes { + var buf bytes.Buffer + if err := tx.MsgTx().Serialize(&buf); err != nil { + return nil, fmt.Errorf("serialize tx %s: %w", + tx.Hash(), err) + } + rawTxs = append(rawTxs, hex.EncodeToString(buf.Bytes())) + } + + rawTxsJSON, err := json.Marshal(rawTxs) + if err != nil { + return nil, fmt.Errorf("marshal raw txs: %w", err) + } + + resp, generateErrHex = b.rpcClient.RawRequest( + "generateblock", + []json.RawMessage{ + []byte(fmt.Sprintf("%q", addr.EncodeAddress())), + rawTxsJSON, + }, + ) + + if generateErrHex != nil { + // Strategy 2: submit to mempool, then try `generateblock` with + // txids. + txids := make([]string, 0, len(txes)) + for _, tx := range txes { + txid := tx.Hash().String() + txids = append(txids, txid) + + _, err := b.rpcClient.SendRawTransaction( + tx.MsgTx(), true, + ) + if err != nil { + // Ignore already-in-mempool errors. + if !strings.Contains(err.Error(), "already") && + !strings.Contains( + err.Error(), + "txn-already-known", + ) { + + return nil, fmt.Errorf("unable to "+ + "send tx %s to mempool: %w", + txid, err) + } + } + } + + txidsJSON, err := json.Marshal(txids) + if err != nil { + return nil, fmt.Errorf("marshal txids: %w", err) + } + + resp, generateErrID = b.rpcClient.RawRequest( + "generateblock", + []json.RawMessage{ + []byte(fmt.Sprintf("%q", addr.EncodeAddress())), + txidsJSON, + }, + ) + } + + if generateErrHex != nil && generateErrID != nil { + // Fall back to `generatetoaddress` for older bitcoind versions. + // + // Note: this fallback may include additional mempool + // transactions. + blockHashes, genErr := b.rpcClient.GenerateToAddress( + 1, addr, nil, + ) + if genErr != nil { + return nil, fmt.Errorf("generateblock (hex): %v; "+ + "generateblock (txid): %v; fallback "+ + "generatetoaddress: %v", generateErrHex, + generateErrID, genErr) + } + if len(blockHashes) == 0 { + return nil, fmt.Errorf("no block generated") + } + + block, getErr := b.rpcClient.GetBlock(blockHashes[0]) + if getErr != nil { + return nil, fmt.Errorf("unable to get generated "+ + "block: %w", getErr) + } + + return btcutil.NewBlock(block), nil + } + + // `generateblock` returns either a hash string or an object with a + // `hash` field, depending on the version. + var blockHashStr string + if unmarshalErr := json.Unmarshal( + resp, &blockHashStr, + ); unmarshalErr != nil { + var respObj generateBlockResp + if unmarshalErr2 := json.Unmarshal( + resp, &respObj, + ); unmarshalErr2 != nil { + return nil, fmt.Errorf("parse generateblock resp: "+ + "%v; %v", unmarshalErr, unmarshalErr2) + } + blockHashStr = respObj.Hash + } + + blockHash, err := chainhash.NewHashFromStr(blockHashStr) + if err != nil { + return nil, fmt.Errorf("invalid generateblock hash %q: %w", + blockHashStr, err) + } + + block, err := b.rpcClient.GetBlock(blockHash) + if err != nil { + return nil, fmt.Errorf("unable to get generated block: %w", err) + } + + return btcutil.NewBlock(block), nil +} + +// NewAddress generates a new address. +func (b *BitcoindMinerBackend) NewAddress() (btcutil.Address, error) { + // Use legacy addresses for compatibility with btcd. + // + // Params: label, address_type. + addrResult, err := b.rpcClient.RawRequest( + "getnewaddress", []json.RawMessage{ + []byte(`""`), + []byte(`"legacy"`), + }, + ) + if err != nil { + return nil, fmt.Errorf("unable to get new address: %w", err) + } + + var addrStr string + if err := json.Unmarshal(addrResult, &addrStr); err != nil { + return nil, fmt.Errorf("unable to parse address: %w", err) + } + + return btcutil.DecodeAddress(addrStr, HarnessNetParams) +} + +// P2PAddress returns the P2P address of the miner. +func (b *BitcoindMinerBackend) P2PAddress() string { + return fmt.Sprintf("127.0.0.1:%d", b.p2pPort) +} + +// Name returns the name of the backend implementation. +func (b *BitcoindMinerBackend) Name() string { + return "bitcoind" +} + +// ConnectMiner connects this miner to another node. +func (b *BitcoindMinerBackend) ConnectMiner(address string) error { + // Use "onetry" so we don't persist peer connections across tests. + _, err := b.rpcClient.RawRequest( + "addnode", + []json.RawMessage{ + []byte(fmt.Sprintf("%q", address)), + []byte(`"onetry"`), + }, + ) + + return err +} + +// DisconnectMiner disconnects this miner from another node. +func (b *BitcoindMinerBackend) DisconnectMiner(address string) error { + // `addnode remove` removes from the addnode list, but doesn't reliably + // disconnect an existing connection. Use `disconnectnode` first. + _, err := b.rpcClient.RawRequest( + "disconnectnode", + []json.RawMessage{ + []byte(fmt.Sprintf("%q", address)), + }, + ) + if err != nil { + return err + } + + // Best-effort cleanup of any persistent addnode state. `ConnectMiner` + // uses "onetry", so `addnode remove` can return an error if the peer + // was never added to the addnode list. + _ = b.rpcClient.AddNode(address, rpcclient.ANRemove) + + return nil +} + +// SendRawTransaction sends a raw transaction to the network. +func (b *BitcoindMinerBackend) SendRawTransaction(tx *wire.MsgTx, + allowHighFees bool) (*chainhash.Hash, error) { + + return b.rpcClient.SendRawTransaction(tx, allowHighFees) +} diff --git a/lntest/miner/btcd_miner.go b/lntest/miner/btcd_miner.go new file mode 100644 index 00000000000..bdca6fe4679 --- /dev/null +++ b/lntest/miner/btcd_miner.go @@ -0,0 +1,234 @@ +package miner + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/integration/rpctest" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/lntest/node" + "github.com/stretchr/testify/require" +) + +// BtcdMinerBackend implements MinerBackend using btcd. +type BtcdMinerBackend struct { + *testing.T + *rpctest.Harness + + // runCtx is a context with cancel method. It's used to signal when the + // node needs to quit, and used as the parent context when spawning + // children contexts for RPC requests. + //nolint:containedctx + runCtx context.Context + cancel context.CancelFunc + + // logPath is the directory path of the miner's logs. + logPath string + + // logFilename is the saved log filename of the miner node. + logFilename string +} + +// NewBtcdMinerBackend creates a new btcd miner backend. +func NewBtcdMinerBackend(ctxb context.Context, t *testing.T, + config *MinerConfig) *BtcdMinerBackend { + + t.Helper() + + logDir := config.LogDir + if logDir == "" { + logDir = minerLogDir + } + + logFilename := config.LogFilename + if logFilename == "" { + logFilename = minerLogFilename + } + + handler := &rpcclient.NotificationHandlers{} + btcdBinary := node.GetBtcdBinary() + baseLogPath := fmt.Sprintf("%s/%s", node.GetLogDir(), logDir) + + args := []string{ + "--rejectnonstd", + "--txindex", + "--nowinservice", + "--nobanning", + "--debuglevel=debug", + "--logdir=" + baseLogPath, + "--trickleinterval=100ms", + // Don't disconnect if a reply takes too long. + "--nostalldetect", + } + + // Add any extra args from config. + if config.ExtraArgs != nil { + args = append(args, config.ExtraArgs...) + } + + miner, err := rpctest.New(HarnessNetParams, handler, args, btcdBinary) + require.NoError(t, err, "unable to create mining node") + + ctxt, cancel := context.WithCancel(ctxb) + + return &BtcdMinerBackend{ + T: t, + Harness: miner, + runCtx: ctxt, + cancel: cancel, + logPath: baseLogPath, + logFilename: logFilename, + } +} + +// Start starts the btcd miner backend. +func (b *BtcdMinerBackend) Start(setupChain bool, + numMatureOutputs uint32) error { + + return b.SetUp(setupChain, numMatureOutputs) +} + +// Stop stops the btcd miner backend and saves logs. +func (b *BtcdMinerBackend) Stop() error { + b.cancel() + if err := b.TearDown(); err != nil { + return fmt.Errorf("tear down miner got error: %w", err) + } + b.saveLogs() + + return nil +} + +// saveLogs copies the node logs and save it to the file specified by +// b.logFilename. +func (b *BtcdMinerBackend) saveLogs() { + // After shutting down the miner, we'll make a copy of the log files + // before deleting the temporary log dir. + path := fmt.Sprintf("%s/%s", b.logPath, HarnessNetParams.Name) + files, err := os.ReadDir(path) + require.NoError(b, err, "unable to read log directory") + + for _, file := range files { + newFilename := strings.Replace( + file.Name(), "btcd.log", b.logFilename, 1, + ) + copyPath := fmt.Sprintf("%s/../%s", b.logPath, newFilename) + + logFile := fmt.Sprintf("%s/%s", path, file.Name()) + err := node.CopyFile(filepath.Clean(copyPath), logFile) + require.NoError(b, err, "unable to copy file") + } + + err = os.RemoveAll(b.logPath) + require.NoErrorf(b, err, "cannot remove dir %s", b.logPath) +} + +// GetBestBlock returns the hash and height of the best block. +func (b *BtcdMinerBackend) GetBestBlock() (*chainhash.Hash, int32, error) { + return b.Client.GetBestBlock() +} + +// GetRawMempool returns all transaction hashes in the mempool. +func (b *BtcdMinerBackend) GetRawMempool() ([]*chainhash.Hash, error) { + return b.Client.GetRawMempool() +} + +// Generate mines a specified number of blocks. +func (b *BtcdMinerBackend) Generate(blocks uint32) ([]*chainhash.Hash, error) { + return b.Client.Generate(blocks) +} + +// GetBlock returns the block for the given block hash. +func (b *BtcdMinerBackend) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, + error) { + + return b.Client.GetBlock(blockHash) +} + +// GetRawTransaction returns the raw transaction for the given txid. +func (b *BtcdMinerBackend) GetRawTransaction(txid *chainhash.Hash) (*btcutil.Tx, + error) { + + return b.Client.GetRawTransaction(txid) +} + +// InvalidateBlock marks a block as invalid, triggering a reorg. +func (b *BtcdMinerBackend) InvalidateBlock(blockHash *chainhash.Hash) error { + return b.Client.InvalidateBlock(blockHash) +} + +// SendRawTransaction sends a raw transaction to the backend. +func (b *BtcdMinerBackend) SendRawTransaction(tx *wire.MsgTx, + allowHighFees bool) (*chainhash.Hash, error) { + + return b.Client.SendRawTransaction(tx, allowHighFees) +} + +// NotifyNewTransactions registers for new transaction notifications. +func (b *BtcdMinerBackend) NotifyNewTransactions(verbose bool) error { + return b.Client.NotifyNewTransactions(verbose) +} + +// SendOutputsWithoutChange creates and broadcasts a transaction with +// the given outputs using the specified fee rate. +func (b *BtcdMinerBackend) SendOutputsWithoutChange(outputs []*wire.TxOut, + feeRate btcutil.Amount) (*chainhash.Hash, error) { + + return b.Harness.SendOutputsWithoutChange(outputs, feeRate) +} + +// CreateTransaction creates a transaction with the given outputs. +func (b *BtcdMinerBackend) CreateTransaction(outputs []*wire.TxOut, + feeRate btcutil.Amount) (*wire.MsgTx, error) { + + return b.Harness.CreateTransaction(outputs, feeRate, false) +} + +// SendOutputs creates and broadcasts a transaction with the given outputs. +func (b *BtcdMinerBackend) SendOutputs(outputs []*wire.TxOut, + feeRate btcutil.Amount) (*chainhash.Hash, error) { + + return b.Harness.SendOutputs(outputs, feeRate) +} + +// GenerateAndSubmitBlock generates a block with the given transactions. +func (b *BtcdMinerBackend) GenerateAndSubmitBlock(txes []*btcutil.Tx, + blockVersion int32, + blockTime time.Time) (*btcutil.Block, error) { + + return b.Harness.GenerateAndSubmitBlock(txes, blockVersion, blockTime) +} + +// NewAddress generates a new address. +func (b *BtcdMinerBackend) NewAddress() (btcutil.Address, error) { + return b.Harness.NewAddress() +} + +// P2PAddress returns the P2P address of the miner. +func (b *BtcdMinerBackend) P2PAddress() string { + return b.Harness.P2PAddress() +} + +// Name returns the name of the backend implementation. +func (b *BtcdMinerBackend) Name() string { + return "btcd" +} + +// ConnectMiner connects this miner to another node using btcjson commands. +func (b *BtcdMinerBackend) ConnectMiner(address string) error { + return b.Client.Node(btcjson.NConnect, address, &Temp) +} + +// DisconnectMiner disconnects this miner from another node. +func (b *BtcdMinerBackend) DisconnectMiner(address string) error { + return b.Client.Node(btcjson.NDisconnect, address, &Temp) +} diff --git a/lntest/miner/interface.go b/lntest/miner/interface.go new file mode 100644 index 00000000000..bd51e824362 --- /dev/null +++ b/lntest/miner/interface.go @@ -0,0 +1,103 @@ +package miner + +import ( + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" +) + +// MinerBackend defines the interface for different miner backend +// implementations (btcd vs bitcoind). +// +// We keep a single interface to preserve the existing lntest APIs while +// supporting multiple backends. +// +//nolint:interfacebloat +type MinerBackend interface { + // Start starts the miner backend. + // + // For the btcd backend, these parameters map to rpctest.Harness.SetUp. + // For other backends, these parameters can be ignored. + Start(setupChain bool, numMatureOutputs uint32) error + + // Stop stops the miner backend and performs cleanup. + Stop() error + + // GetBestBlock returns the hash and height of the best block. + GetBestBlock() (*chainhash.Hash, int32, error) + + // GetRawMempool returns all transaction hashes in the mempool. + GetRawMempool() ([]*chainhash.Hash, error) + + // Generate mines a specified number of blocks. + Generate(blocks uint32) ([]*chainhash.Hash, error) + + // GetBlock returns the block for the given block hash. + GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) + + // GetRawTransaction returns the raw transaction for the given txid. + GetRawTransaction(txid *chainhash.Hash) (*btcutil.Tx, error) + + // InvalidateBlock marks a block as invalid, triggering a reorg. + InvalidateBlock(blockHash *chainhash.Hash) error + + // SendRawTransaction sends a raw transaction to the backend. + SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, + error) + + // NotifyNewTransactions registers for new transaction notifications. + // For backends that don't support this, it should be a no-op. + NotifyNewTransactions(verbose bool) error + + // SendOutputsWithoutChange creates and broadcasts a transaction with + // the given outputs using the specified fee rate. + SendOutputsWithoutChange(outputs []*wire.TxOut, + feeRate btcutil.Amount) (*chainhash.Hash, error) + + // CreateTransaction creates a transaction with the given outputs. + CreateTransaction(outputs []*wire.TxOut, + feeRate btcutil.Amount) (*wire.MsgTx, error) + + // SendOutputs creates and broadcasts a transaction with the given + // outputs. + SendOutputs(outputs []*wire.TxOut, + feeRate btcutil.Amount) (*chainhash.Hash, error) + + // GenerateAndSubmitBlock generates a block with the given transactions. + GenerateAndSubmitBlock(txes []*btcutil.Tx, blockVersion int32, + blockTime time.Time) (*btcutil.Block, error) + + // NewAddress generates a new address. + NewAddress() (btcutil.Address, error) + + // P2PAddress returns the P2P address of the miner. + P2PAddress() string + + // ConnectMiner connects this backend to a miner peer at the given + // address (host:port). + ConnectMiner(address string) error + + // DisconnectMiner disconnects this backend from a miner peer at the + // given address (host:port). + DisconnectMiner(address string) error + + // Name returns the name of the backend implementation. + Name() string +} + +// MinerConfig holds configuration for creating different miner backends. +type MinerConfig struct { + // Backend specifies which backend to use ("btcd" or "bitcoind"). + Backend string + + // LogDir specifies the directory for log files. + LogDir string + + // LogFilename specifies the log filename. + LogFilename string + + // ExtraArgs contains additional command-line arguments for the backend. + ExtraArgs []string +} diff --git a/lntest/miner/miner.go b/lntest/miner/miner.go index 0229d6a47f8..bbf7f9ca0bd 100644 --- a/lntest/miner/miner.go +++ b/lntest/miner/miner.go @@ -4,9 +4,6 @@ import ( "bytes" "context" "fmt" - "os" - "path/filepath" - "strings" "testing" "time" @@ -15,10 +12,8 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/integration/rpctest" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/fn/v2" - "github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/stretchr/testify/require" ) @@ -46,19 +41,32 @@ var ( type HarnessMiner struct { *testing.T + + // backend is the underlying miner backend implementation (btcd or + // bitcoind). This is always set. + backend MinerBackend + + // Harness is the btcd-specific harness. This is only set when using + // the btcd backend, and is nil when using bitcoind. *rpctest.Harness + // ActiveNet is the miner network parameters used by lntest. + // + // This field exists for backwards compatibility with existing itests + // that reference `ht.Miner().ActiveNet`. When using the bitcoind miner + // backend, the embedded btcd rpctest harness is nil, but callers still + // expect this value to be set. + ActiveNet *chaincfg.Params + // runCtx is a context with cancel method. It's used to signal when the // node needs to quit, and used as the parent context when spawning // children contexts for RPC requests. - runCtx context.Context //nolint:containedctx + // + // NOTE: The backend owns lifecycle management; these fields are kept + // for compatibility with existing patterns in lntest. + //nolint:containedctx + runCtx context.Context cancel context.CancelFunc - - // logPath is the directory path of the miner's logs. - logPath string - - // logFilename is the saved log filename of the miner node. - logFilename string } // NewMiner creates a new miner using btcd backend with the default log file @@ -78,77 +86,166 @@ func NewTempMiner(ctxt context.Context, t *testing.T, return newMiner(ctxt, t, tempDir, tempLogFilename) } +// NewBitcoindMiner creates a new miner using bitcoind backend with the +// default log file dir and name. +func NewBitcoindMiner(ctxt context.Context, t *testing.T) *HarnessMiner { + t.Helper() + return newBitcoindMiner( + ctxt, t, minerLogDir, "output_bitcoind_miner.log", + ) +} + +// NewBitcoindTempMiner creates a new miner using bitcoind backend with the +// specified log file dir and name. +func NewBitcoindTempMiner(ctxt context.Context, t *testing.T, + tempDir, tempLogFilename string) *HarnessMiner { + + t.Helper() + return newBitcoindMiner(ctxt, t, tempDir, tempLogFilename) +} + +// NewMinerWithConfig creates a new miner with the specified configuration. +// The Backend field in the config determines which backend to use ("btcd" or +// "bitcoind"). +func NewMinerWithConfig(ctxt context.Context, t *testing.T, + config *MinerConfig) *HarnessMiner { + + t.Helper() + + // Set defaults if not specified. + logDir := config.LogDir + if logDir == "" { + logDir = minerLogDir + } + + logFilename := config.LogFilename + if logFilename == "" { + if config.Backend == "bitcoind" { + logFilename = "output_bitcoind_miner.log" + } else { + logFilename = minerLogFilename + } + } + + // Choose backend based on config. + switch config.Backend { + case "bitcoind": + return newBitcoindMinerWithConfig(ctxt, t, config, logDir, + logFilename) + case "btcd", "": + // Default to btcd for backward compatibility. + return newBtcdMinerWithConfig(ctxt, t, config, logDir, + logFilename) + default: + require.Failf(t, "unknown backend", + "backend %s not supported", config.Backend) + return nil + } +} + // newMiner creates a new miner using btcd's rpctest. func newMiner(ctxb context.Context, t *testing.T, minerDirName, logFilename string) *HarnessMiner { t.Helper() - handler := &rpcclient.NotificationHandlers{} - btcdBinary := node.GetBtcdBinary() - baseLogPath := fmt.Sprintf("%s/%s", node.GetLogDir(), minerDirName) - - args := []string{ - "--rejectnonstd", - "--txindex", - "--nowinservice", - "--nobanning", - "--debuglevel=debug", - "--logdir=" + baseLogPath, - "--trickleinterval=100ms", - // Don't disconnect if a reply takes too long. - "--nostalldetect", + config := &MinerConfig{ + Backend: "btcd", + LogDir: minerDirName, + LogFilename: logFilename, + } + + return newBtcdMinerWithConfig(ctxb, t, config, minerDirName, + logFilename) +} + +// newBitcoindMiner creates a new miner using bitcoind. +func newBitcoindMiner(ctxb context.Context, t *testing.T, minerDirName, + logFilename string) *HarnessMiner { + + t.Helper() + + config := &MinerConfig{ + Backend: "bitcoind", + LogDir: minerDirName, + LogFilename: logFilename, } - miner, err := rpctest.New(HarnessNetParams, handler, args, btcdBinary) - require.NoError(t, err, "unable to create mining node") + return newBitcoindMinerWithConfig(ctxb, t, config, minerDirName, + logFilename) +} - ctxt, cancel := context.WithCancel(ctxb) +// newBtcdMinerWithConfig creates a new miner using btcd with the given config. +func newBtcdMinerWithConfig(ctxb context.Context, t *testing.T, + config *MinerConfig, minerDirName, logFilename string) *HarnessMiner { + + t.Helper() + + // Create the btcd backend wrapper. Note that we don't start the backend + // here, as callers (lntest harness) often tweak harness settings (e.g. + // connection retries) before calling SetUp/Start. + btcdBackend := NewBtcdMinerBackend(ctxb, t, &MinerConfig{ + Backend: "btcd", + LogDir: minerDirName, + LogFilename: logFilename, + ExtraArgs: config.ExtraArgs, + }) return &HarnessMiner{ - T: t, - Harness: miner, - runCtx: ctxt, - cancel: cancel, - logPath: baseLogPath, - logFilename: logFilename, + T: t, + backend: btcdBackend, + Harness: btcdBackend.Harness, + ActiveNet: HarnessNetParams, + runCtx: btcdBackend.runCtx, + cancel: btcdBackend.cancel, } } -// saveLogs copies the node logs and save it to the file specified by -// h.logFilename. -func (h *HarnessMiner) saveLogs() { - // After shutting down the miner, we'll make a copy of the log files - // before deleting the temporary log dir. - path := fmt.Sprintf("%s/%s", h.logPath, HarnessNetParams.Name) - files, err := os.ReadDir(path) - require.NoError(h, err, "unable to read log directory") +// newBitcoindMinerWithConfig creates a new miner using bitcoind with the +// given config. +func newBitcoindMinerWithConfig(ctxb context.Context, t *testing.T, + config *MinerConfig, minerDirName, logFilename string) *HarnessMiner { - for _, file := range files { - newFilename := strings.Replace( - file.Name(), "btcd.log", h.logFilename, 1, - ) - copyPath := fmt.Sprintf("%s/../%s", h.logPath, newFilename) + t.Helper() + + // Create the bitcoind backend. + bitcoindBackend := NewBitcoindMinerBackend(ctxb, t, &MinerConfig{ + Backend: "bitcoind", + LogDir: minerDirName, + LogFilename: logFilename, + ExtraArgs: config.ExtraArgs, + }) - logFile := fmt.Sprintf("%s/%s", path, file.Name()) - err := node.CopyFile(filepath.Clean(copyPath), logFile) - require.NoError(h, err, "unable to copy file") + return &HarnessMiner{ + T: t, + backend: bitcoindBackend, + // No btcd harness when using bitcoind. + Harness: nil, + ActiveNet: HarnessNetParams, + runCtx: bitcoindBackend.runCtx, + cancel: bitcoindBackend.cancel, } +} + +// Start starts the miner backend. +func (h *HarnessMiner) Start(setupChain bool, numMatureOutputs uint32) error { + return h.backend.Start(setupChain, numMatureOutputs) +} - err = os.RemoveAll(h.logPath) - require.NoErrorf(h, err, "cannot remove dir %s", h.logPath) +// NotifyNewTransactions registers for new transaction notifications. +func (h *HarnessMiner) NotifyNewTransactions(verbose bool) error { + return h.backend.NotifyNewTransactions(verbose) } // Stop shuts down the miner and saves its logs. func (h *HarnessMiner) Stop() { - h.cancel() - require.NoError(h, h.TearDown(), "tear down miner got error") - h.saveLogs() + err := h.backend.Stop() + require.NoError(h, err, "failed to stop miner backend") } // GetBestBlock makes a RPC request to miner and asserts. func (h *HarnessMiner) GetBestBlock() (*chainhash.Hash, int32) { - blockHash, height, err := h.Client.GetBestBlock() + blockHash, height, err := h.backend.GetBestBlock() require.NoError(h, err, "failed to GetBestBlock") return blockHash, height @@ -157,7 +254,7 @@ func (h *HarnessMiner) GetBestBlock() (*chainhash.Hash, int32) { // GetRawMempool makes a RPC call to the miner's GetRawMempool and // asserts. func (h *HarnessMiner) GetRawMempool() []chainhash.Hash { - mempool, err := h.Client.GetRawMempool() + mempool, err := h.backend.GetRawMempool() require.NoError(h, err, "unable to get mempool") txns := make([]chainhash.Hash, 0, len(mempool)) @@ -170,7 +267,7 @@ func (h *HarnessMiner) GetRawMempool() []chainhash.Hash { // GenerateBlocks mine 'num' of blocks and returns them. func (h *HarnessMiner) GenerateBlocks(num uint32) []*chainhash.Hash { - blockHashes, err := h.Client.Generate(num) + blockHashes, err := h.backend.Generate(num) require.NoError(h, err, "unable to generate blocks") require.Len(h, blockHashes, int(num), "wrong num of blocks generated") @@ -179,7 +276,7 @@ func (h *HarnessMiner) GenerateBlocks(num uint32) []*chainhash.Hash { // GetBlock gets a block using its block hash. func (h *HarnessMiner) GetBlock(blockHash *chainhash.Hash) *wire.MsgBlock { - block, err := h.Client.GetBlock(blockHash) + block, err := h.backend.GetBlock(blockHash) require.NoError(h, err, "unable to get block") return block @@ -270,16 +367,37 @@ func (h *HarnessMiner) MineBlocksAndAssertNumTxes(num uint32, // GetRawTransaction makes a RPC call to the miner's GetRawTransaction and // asserts. func (h *HarnessMiner) GetRawTransaction(txid chainhash.Hash) *btcutil.Tx { - tx, err := h.Client.GetRawTransaction(&txid) + tx, err := h.backend.GetRawTransaction(&txid) require.NoErrorf(h, err, "failed to get raw tx: %v", txid) return tx } +// GetRawTransactionNoAssert makes a RPC call to the miner's GetRawTransaction +// and returns the error to the caller. +func (h *HarnessMiner) GetRawTransactionNoAssert( + txid chainhash.Hash) (*btcutil.Tx, error) { + + return h.backend.GetRawTransaction(&txid) +} + +// InvalidateBlock marks a block as invalid, triggering a reorg. +func (h *HarnessMiner) InvalidateBlock(blockHash *chainhash.Hash) error { + return h.backend.InvalidateBlock(blockHash) +} + // GetRawTransactionVerbose makes a RPC call to the miner's // GetRawTransactionVerbose and asserts. func (h *HarnessMiner) GetRawTransactionVerbose( txid chainhash.Hash) *btcjson.TxRawResult { + // This method is only supported for btcd backend currently. + // For bitcoind, callers should use GetRawTransaction instead. + if h.Harness == nil { + require.Fail(h, "GetRawTransactionVerbose not supported for "+ + "this backend, use GetRawTransaction instead") + return nil + } + tx, err := h.Client.GetRawTransactionVerbose(&txid) require.NoErrorf(h, err, "failed to get raw tx verbose: %v", txid) return tx @@ -370,9 +488,7 @@ func (h *HarnessMiner) AssertTxNotInMempool(txid chainhash.Hash) { func (h *HarnessMiner) SendOutputsWithoutChange(outputs []*wire.TxOut, feeRate btcutil.Amount) *chainhash.Hash { - txid, err := h.Harness.SendOutputsWithoutChange( - outputs, feeRate, - ) + txid, err := h.backend.SendOutputsWithoutChange(outputs, feeRate) require.NoErrorf(h, err, "failed to send output") return txid @@ -383,7 +499,7 @@ func (h *HarnessMiner) SendOutputsWithoutChange(outputs []*wire.TxOut, func (h *HarnessMiner) CreateTransaction(outputs []*wire.TxOut, feeRate btcutil.Amount) *wire.MsgTx { - tx, err := h.Harness.CreateTransaction(outputs, feeRate, false) + tx, err := h.backend.CreateTransaction(outputs, feeRate) require.NoErrorf(h, err, "failed to create transaction") return tx @@ -394,7 +510,7 @@ func (h *HarnessMiner) CreateTransaction(outputs []*wire.TxOut, func (h *HarnessMiner) SendOutput(newOutput *wire.TxOut, feeRate btcutil.Amount) *chainhash.Hash { - hash, err := h.Harness.SendOutputs([]*wire.TxOut{newOutput}, feeRate) + hash, err := h.backend.SendOutputs([]*wire.TxOut{newOutput}, feeRate) require.NoErrorf(h, err, "failed to send outputs") return hash @@ -414,7 +530,7 @@ func (h *HarnessMiner) MineBlocksSlow(num uint32) []*wire.MsgBlock { } for i, blockHash := range blockHashes { - block, err := h.Client.GetBlock(blockHash) + block, err := h.backend.GetBlock(blockHash) require.NoError(h, err, "get blocks") blocks[i] = block @@ -444,7 +560,7 @@ func (h *HarnessMiner) AssertOutpointInMempool(op wire.OutPoint) *wire.MsgTx { // found. For instance, the aggregation logic used in // sweeping HTLC outputs will update the mempool by // replacing the HTLC spending txes with a single one. - tx, err := h.Client.GetRawTransaction(&txid) + tx, err := h.backend.GetRawTransaction(&txid) if err != nil { return err } @@ -481,7 +597,7 @@ func (h *HarnessMiner) GetNumTxsFromMempool(n int) []*wire.MsgTx { // NewMinerAddress creates a new address for the miner and asserts. func (h *HarnessMiner) NewMinerAddress() btcutil.Address { - addr, err := h.NewAddress() + addr, err := h.backend.NewAddress() require.NoError(h, err, "failed to create new miner address") return addr } @@ -492,10 +608,10 @@ func (h *HarnessMiner) MineBlockWithTxes(txes []*btcutil.Tx) *wire.MsgBlock { var emptyTime time.Time // Generate a block. - b, err := h.GenerateAndSubmitBlock(txes, -1, emptyTime) + b, err := h.backend.GenerateAndSubmitBlock(txes, -1, emptyTime) require.NoError(h, err, "unable to mine block") - block, err := h.Client.GetBlock(b.Hash()) + block, err := h.backend.GetBlock(b.Hash()) require.NoError(h, err, "unable to get block") // Make sure the mempool has been updated. @@ -513,10 +629,10 @@ func (h *HarnessMiner) MineBlockWithTx(tx *wire.MsgTx) *wire.MsgBlock { txes := []*btcutil.Tx{btcutil.NewTx(tx)} // Generate a block. - b, err := h.GenerateAndSubmitBlock(txes, -1, emptyTime) + b, err := h.backend.GenerateAndSubmitBlock(txes, -1, emptyTime) require.NoError(h, err, "unable to mine block") - block, err := h.Client.GetBlock(b.Hash()) + block, err := h.backend.GetBlock(b.Hash()) require.NoError(h, err, "unable to get block") // Make sure the mempool has been updated. @@ -532,7 +648,7 @@ func (h *HarnessMiner) MineEmptyBlocks(num int) []*wire.MsgBlock { blocks := make([]*wire.MsgBlock, num) for i := 0; i < num; i++ { // Generate an empty block. - b, err := h.GenerateAndSubmitBlock(nil, -1, emptyTime) + b, err := h.backend.GenerateAndSubmitBlock(nil, -1, emptyTime) require.NoError(h, err, "unable to mine empty block") block := h.GetBlock(b.Hash()) @@ -551,29 +667,45 @@ func (h *HarnessMiner) SpawnTempMiner() *HarnessMiner { // Setup a temp miner. tempLogDir := ".tempminerlogs" logFilename := "output-temp_miner.log" - tempMiner := NewTempMiner(h.runCtx, h.T, tempLogDir, logFilename) + var tempMiner *HarnessMiner + switch h.BackendName() { + case "bitcoind": + tempMiner = NewBitcoindTempMiner( + h.runCtx, h.T, tempLogDir, logFilename, + ) + case "btcd": + tempMiner = NewTempMiner(h.runCtx, h.T, tempLogDir, logFilename) + default: + require.Failf("unknown miner backend", + "backend %s not supported", h.BackendName()) + return nil + } // Make sure to clean the miner when the test ends. h.T.Cleanup(tempMiner.Stop) - // Setup the miner. - require.NoError(tempMiner.SetUp(false, 0), "unable to setup miner") + // Start the miner. + require.NoError(tempMiner.Start(false, 0), "unable to start miner") // Connect the temp miner to the original miner. - err := h.Client.Node(btcjson.NConnect, tempMiner.P2PAddress(), &Temp) + err := h.backend.ConnectMiner(tempMiner.P2PAddress()) require.NoError(err, "unable to connect node") // Sync the blocks. - nodeSlice := []*rpctest.Harness{h.Harness, tempMiner.Harness} - err = rpctest.JoinNodes(nodeSlice, rpctest.Blocks) - require.NoError(err, "unable to join node on blocks") + if h.Harness != nil && tempMiner.Harness != nil { + nodeSlice := []*rpctest.Harness{h.Harness, tempMiner.Harness} + err = rpctest.JoinNodes(nodeSlice, rpctest.Blocks) + require.NoError(err, "unable to join node on blocks") + } else { + h.AssertMinerBlockHeightDelta(tempMiner, 0) + } // The two miners should be on the same block height. h.AssertMinerBlockHeightDelta(tempMiner, 0) // Once synced, we now disconnect the temp miner so it'll be // independent from the original miner. - err = h.Client.Node(btcjson.NDisconnect, tempMiner.P2PAddress(), &Temp) + err = h.backend.DisconnectMiner(tempMiner.P2PAddress()) require.NoError(err, "unable to disconnect miners") return tempMiner @@ -584,17 +716,21 @@ func (h *HarnessMiner) ConnectMiner(tempMiner *HarnessMiner) { require := require.New(h.T) // Connect the current miner to the temporary miner. - err := h.Client.Node(btcjson.NConnect, tempMiner.P2PAddress(), &Temp) + err := h.backend.ConnectMiner(tempMiner.P2PAddress()) require.NoError(err, "unable to connect temp miner") - nodes := []*rpctest.Harness{tempMiner.Harness, h.Harness} - err = rpctest.JoinNodes(nodes, rpctest.Blocks) - require.NoError(err, "unable to join node on blocks") + if h.Harness != nil && tempMiner.Harness != nil { + nodes := []*rpctest.Harness{tempMiner.Harness, h.Harness} + err = rpctest.JoinNodes(nodes, rpctest.Blocks) + require.NoError(err, "unable to join node on blocks") + } else { + h.AssertMinerBlockHeightDelta(tempMiner, 0) + } } // DisconnectMiner disconnects the miner from the temp miner. func (h *HarnessMiner) DisconnectMiner(tempMiner *HarnessMiner) { - err := h.Client.Node(btcjson.NDisconnect, tempMiner.P2PAddress(), &Temp) + err := h.backend.DisconnectMiner(tempMiner.P2PAddress()) require.NoError(h.T, err, "unable to disconnect temp miner") } @@ -605,13 +741,13 @@ func (h *HarnessMiner) AssertMinerBlockHeightDelta(tempMiner *HarnessMiner, // Ensure the chain lengths are what we expect. err := wait.NoError(func() error { - _, tempMinerHeight, err := tempMiner.Client.GetBestBlock() + _, tempMinerHeight, err := tempMiner.backend.GetBestBlock() if err != nil { return fmt.Errorf("unable to get current "+ "blockheight %v", err) } - _, minerHeight, err := h.Client.GetBestBlock() + _, minerHeight, err := h.backend.GetBestBlock() if err != nil { return fmt.Errorf("unable to get current "+ "blockheight %v", err) @@ -627,3 +763,20 @@ func (h *HarnessMiner) AssertMinerBlockHeightDelta(tempMiner *HarnessMiner, }, wait.DefaultTimeout) require.NoError(h.T, err, "failed to assert block height delta") } + +// P2PAddress returns the P2P address of the miner. +func (h *HarnessMiner) P2PAddress() string { + return h.backend.P2PAddress() +} + +// BackendName returns the name of the backend implementation. +func (h *HarnessMiner) BackendName() string { + return h.backend.Name() +} + +// SendRawTransaction sends a raw transaction with optional high fee allowance. +func (h *HarnessMiner) SendRawTransaction(tx *wire.MsgTx, + allowHighFees bool) (*chainhash.Hash, error) { + + return h.backend.SendRawTransaction(tx, allowHighFees) +} diff --git a/make/testing_flags.mk b/make/testing_flags.mk index 95a493930eb..18b2e857681 100644 --- a/make/testing_flags.mk +++ b/make/testing_flags.mk @@ -69,6 +69,11 @@ ifneq ($(dbbackend),) ITEST_FLAGS += -dbbackend=$(dbbackend) endif +# Select miner backend independently from chain backend. +ifneq ($(minerbackend),) +ITEST_FLAGS += -minerbackend=$(minerbackend) +endif + ifeq ($(dbbackend),etcd) DEV_TAGS += kvdb_etcd endif