What changed, and why it matters
This commit is purely a testing-infrastructure change. It adds a new option to run LND's integration tests against a bitcoind miner backend in addition to the existing btcd miner backend. It does not change any production LND node code, wallet logic, or consensus rules, so it cannot directly affect real users or funds.
No security action required. Treat as a normal test-framework refactor. If reviewing further, verify that the new bitcoind miner backend correctly cleans up temporary data directories and processes on test failure, and that the fallback fee-rate handling in CreateTransaction does not cause flaky tests.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces a MinerBackend interface and two implementations (btcd_miner.go and bitcoind_miner.go) under lntest/miner. It refactors HarnessMiner to delegate RPC calls through the interface rather than directly to btcd’s rpctest.Harness. Existing tests are updated to use backend-agnostic helper methods (e.g., Miner().InvalidateBlock, Miner().GenerateBlocks). A few harness helpers are generalized (network params lookup, raw tx submission, funding shim output index). No production code paths are modified.
Changed components
lntest/minerlntest/harness.golntest/harness_miner.golntest/harness_setup.golntest/bitcoind_common.goitest/*.go integration testsInspect captured patch +1584 / −136
diff --git a/.gitignore b/.gitignore
index 11c67fe..95fe26e 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_coop_close_rbf_test.go b/itest/lnd_coop_close_rbf_test.go
index 13e10c9..24ef317 100644
--- a/itest/lnd_coop_close_rbf_test.go
+++ b/itest/lnd_coop_close_rbf_test.go
@@ -245,11 +245,11 @@ func testCoopCloseRBFWithReorg(ht *lntest.HarnessTest) {
// because we haven't reached full confirmation yet.
bestBlockHash := block2.Header.BlockHash()
require.NoError(
- ht, ht.Miner().Client.InvalidateBlock(&bestBlockHash),
+ ht, ht.Miner().InvalidateBlock(&bestBlockHash),
)
bestBlockHash = block1.Header.BlockHash()
require.NoError(
- ht, ht.Miner().Client.InvalidateBlock(&bestBlockHash),
+ ht, ht.Miner().InvalidateBlock(&bestBlockHash),
)
_, bestHeight = ht.GetBestBlock()
diff --git a/itest/lnd_misc_test.go b/itest/lnd_misc_test.go
index f625213..33b44ef 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_open_channel_test.go b/itest/lnd_open_channel_test.go
index 7319660..55b04e5 100644
--- a/itest/lnd_open_channel_test.go
+++ b/itest/lnd_open_channel_test.go
@@ -91,8 +91,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.
@@ -136,8 +135,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
@@ -1047,8 +1045,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.
@@ -1073,8 +1070,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 b975438..54d718f 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/lntest/bitcoind_common.go b/lntest/bitcoind_common.go
index 6d5cacc..cc78837 100644
--- a/lntest/bitcoind_common.go
+++ b/lntest/bitcoind_common.go
@@ -4,17 +4,20 @@
package lntest
import (
+ "encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
+ "strings"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/rpcclient"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntest/port"
+ "github.com/lightningnetwork/lnd/lntest/wait"
)
// logDirPattern is the pattern of the name of the temporary log directory.
@@ -67,12 +70,43 @@ func (b BitcoindBackendConfig) GenArgs() []string {
// ConnectMiner is called to establish a connection to the test miner.
func (b BitcoindBackendConfig) ConnectMiner() error {
- return b.rpcClient.AddNode(b.minerAddr, rpcclient.ANAdd)
+ err := b.rpcClient.AddNode(b.minerAddr, rpcclient.ANOneTry)
+ if err != nil {
+ return err
+ }
+
+ return wait.NoError(func() error {
+ peerInfo, err := b.rpcClient.GetPeerInfo()
+ if err != nil {
+ return err
+ }
+
+ for _, peer := range peerInfo {
+ if strings.HasPrefix(peer.Addr, b.minerAddr) {
+ return nil
+ }
+ }
+
+ return fmt.Errorf("peer %s not connected", b.minerAddr)
+ }, wait.DefaultTimeout)
}
// 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 +158,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 f3ffc92..66c85cc 100644
--- a/lntest/harness.go
+++ b/lntest/harness.go
@@ -1,6 +1,7 @@
package lntest
import (
+ "bytes"
"context"
"fmt"
"runtime/debug"
@@ -54,8 +55,11 @@ const (
// mining blocks.
maxBlocksAllowed = 100
- finalCltvDelta = routing.MinCLTVDelta // 24.
- thawHeightDelta = finalCltvDelta * 2 // 48.
+ // finalCltvDelta is the min CLTV delta used by the router.
+ finalCltvDelta = routing.MinCLTVDelta
+
+ // thawHeightDelta defines how far in the future we pick thaw heights.
+ thawHeightDelta = finalCltvDelta * 2
)
var (
@@ -98,7 +102,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 +511,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 +1337,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)
@@ -2285,10 +2290,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
}
@@ -2595,14 +2604,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.
@@ -2617,6 +2655,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 010c3f8..bc24b5e 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 166880b..bbea5c4 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,39 @@ 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.
+ m.SetBtcdConnectionRetryParams(
+ rpctest.DefaultMaxConnectionRetries*2,
+ 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 0000000..c390b36
--- /dev/null
+++ b/lntest/miner/bitcoind_miner.go
@@ -0,0 +1,842 @@
+package miner
+
+import (
+ "bytes"
+ "context"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "math"
+ "os"
+ "os/exec"
+ "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/rpcclient"
+ "github.com/btcsuite/btcd/wire"
+ "github.com/lightningnetwork/lnd/lntest/node"
+ "github.com/lightningnetwork/lnd/lntest/port"
+ "github.com/lightningnetwork/lnd/lntest/wait"
+)
+
+// 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
+ extraArgs []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"`
+}
+
+type blockchainInfoResp struct {
+ BestBlockHash string `json:"bestblockhash"`
+ Blocks int32 `json:"blocks"`
+}
+
+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",
+ extraArgs: append([]string(nil), config.ExtraArgs...),
+ }
+}
+
+// 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",
+ }
+
+ cmdArgs = append(cmdArgs, b.extraArgs...)
+
+ // 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
+ }
+
+ if i < maxRetries-1 {
+ time.Sleep(retryDelay)
+ }
+ }
+
+ 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 {
+ return nil
+ }
+
+ // 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) {
+ infoJSON, err := b.rpcClient.RawRequest("getblockchaininfo", nil)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ var info blockchainInfoResp
+ if err := json.Unmarshal(infoJSON, &info); err != nil {
+ return nil, 0, fmt.Errorf(
+ "parse getblockchaininfo resp: %w", err,
+ )
+ }
+
+ hash, err := chainhash.NewHashFromStr(info.BestBlockHash)
+ if err != nil {
+ return nil, 0, fmt.Errorf("invalid best block hash %q: %w",
+ info.BestBlockHash, err)
+ }
+
+ return hash, info.Blocks, 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)
+}
+
+// GetRawTransactionVerbose returns verbose information about the given txid.
+func (b *BitcoindMinerBackend) GetRawTransactionVerbose(
+ txid *chainhash.Hash) (*btcjson.TxRawResult, error) {
+
+ return b.rpcClient.GetRawTransactionVerbose(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) {
+
+ tx := wire.NewMsgTx(2)
+ for _, output := range outputs {
+ tx.AddTxOut(output)
+ }
+
+ var rawTx bytes.Buffer
+ if err := tx.Serialize(&rawTx); err != nil {
+ return nil, fmt.Errorf("serialize raw transaction: %w", err)
+ }
+
+ rawHex := hex.EncodeToString(rawTx.Bytes())
+
+ // 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 (fee_rate=%s, "+
+ "changePosition=%d): %w", feeRateOpt, len(outputs), err)
+ }
+
+ var funded fundRawTransactionResp
+ if err := json.Unmarshal(fundResp, &funded); err != nil {
+ return nil, fmt.Errorf("parse fundrawtransaction resp: %w",
+ err)
+ }
+
+ // 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"`),
+ },
+ )
+
+ if err != nil {
+ return err
+ }
+
+ return wait.NoError(func() error {
+ peerInfo, err := b.rpcClient.GetPeerInfo()
+ if err != nil {
+ return err
+ }
+
+ for _, peer := range peerInfo {
+ if strings.HasPrefix(peer.Addr, address) {
+ return nil
+ }
+ }
+
+ return fmt.Errorf("peer %s not connected", address)
+ }, wait.DefaultTimeout)
+}
+
+// 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 0000000..3c919cb
--- /dev/null
+++ b/lntest/miner/btcd_miner.go
@@ -0,0 +1,241 @@
+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)
+}
+
+// GetRawTransactionVerbose returns verbose information about the given txid.
+func (b *BtcdMinerBackend) GetRawTransactionVerbose(
+ txid *chainhash.Hash) (*btcjson.TxRawResult, error) {
+
+ return b.Client.GetRawTransactionVerbose(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 0000000..190d8d1
--- /dev/null
+++ b/lntest/miner/interface.go
@@ -0,0 +1,110 @@
+package miner
+
+import (
+ "time"
+
+ "github.com/btcsuite/btcd/btcjson"
+ "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.
+ //
+ // If setupChain is true, the backend should perform any initial chain
+ // setup needed for tests. numMatureOutputs specifies how many mature
+ // coinbase outputs should be available after startup.
+ 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)
+
+ // GetRawTransactionVerbose returns verbose information about the given
+ // transaction.
+ GetRawTransactionVerbose(
+ txid *chainhash.Hash) (*btcjson.TxRawResult, 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 0229d6a..92a9000 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
- *rpctest.Harness
+
+ // 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.
+ harness *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,179 @@ 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)
+}
+
+// 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()
- ctxt, cancel := context.WithCancel(ctxb)
+ // 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,
+ })
+
+ return &HarnessMiner{
+ T: t,
+ backend: bitcoindBackend,
+ // No btcd harness when using bitcoind.
+ harness: nil,
+ ActiveNet: HarnessNetParams,
+ runCtx: bitcoindBackend.runCtx,
+ cancel: bitcoindBackend.cancel,
+ }
+}
- logFile := fmt.Sprintf("%s/%s", path, file.Name())
- err := node.CopyFile(filepath.Clean(copyPath), logFile)
- require.NoError(h, err, "unable to copy file")
+// SetBtcdConnectionRetryParams updates the underlying btcd harness connection
+// retry parameters when this miner is backed by btcd.
+func (h *HarnessMiner) SetBtcdConnectionRetryParams(maxRetries int,
+ retryTimeout time.Duration) {
+
+ if h.harness == nil {
+ return
}
- err = os.RemoveAll(h.logPath)
- require.NoErrorf(h, err, "cannot remove dir %s", h.logPath)
+ h.harness.MaxConnRetries = maxRetries
+ h.harness.ConnectionRetryTimeout = retryTimeout
+}
+
+// Start starts the miner backend.
+func (h *HarnessMiner) Start(setupChain bool, numMatureOutputs uint32) error {
+ return h.backend.Start(setupChain, numMatureOutputs)
+}
+
+// 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 +267,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 +280,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 +289,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,17 +380,30 @@ 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 {
- tx, err := h.Client.GetRawTransactionVerbose(&txid)
+ tx, err := h.backend.GetRawTransactionVerbose(&txid)
require.NoErrorf(h, err, "failed to get raw tx verbose: %v", txid)
return tx
}
@@ -370,9 +493,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 +504,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 +515,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 +535,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 +565,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 +602,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 +613,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 +634,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 +653,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 +672,43 @@ 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")
+ }
// 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 +719,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 +744,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 +766,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)
+}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.