From 044ae3f314fcd81571019c996fb40e3c04b3d029 Mon Sep 17 00:00:00 2001 From: allocz Date: Fri, 7 Aug 2026 19:10:06 +0000 Subject: [PATCH 01/15] rpctest: use option structs to create, setup and teardown harness The usage of option structs allows cleaner calls when the default behavior is needed and also allows future extensions without breaking the API. --- integration/rpctest/rpc_harness.go | 153 ++++++++++++++++++++++------- 1 file changed, 118 insertions(+), 35 deletions(-) diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index 9c9cb85262..45102a8c27 100644 --- a/integration/rpctest/rpc_harness.go +++ b/integration/rpctest/rpc_harness.go @@ -127,30 +127,52 @@ type Harness struct { sync.Mutex } -// New creates and initializes new instance of the rpc test harness. -// Optionally, websocket handlers and a specified configuration may be passed. -// In the case that a nil config is passed, a default configuration will be -// used. If a custom btcd executable is specified, it will be used to start the -// harness node. Otherwise a new binary is built on demand. +// HarnessOpts are option that can be passed to New2 when initializing the +// harness instance. +type HarnessOpts struct { + // Params are the parameters of the network to be used, if nil, SimNet + // will be used. + Params *chaincfg.Params + + // Handlers are the RPC client notification handlers than can optionally + // be passed in. + Handlers *rpcclient.NotificationHandlers + + // ExtraArgs are extra arguments to be passed to the btcd instance. + ExtraArgs []string + + // CustomExePath sets the path of the btcd executable, if empty an + // executable is built on demand. + CustomExePath string +} + +// New2 creates and initializes a new instance of the rpctest Harness. // // NOTE: This function is safe for concurrent access. -func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, - extraArgs []string, customExePath string) (*Harness, error) { - +func New2(opts *HarnessOpts) (*Harness, error) { harnessStateMtx.Lock() defer harnessStateMtx.Unlock() + if opts == nil { + opts = &HarnessOpts{} + } + + // By default we run on SimNet. + if opts.Params == nil { + opts.Params = &chaincfg.SimNetParams + } + // Add a flag for the appropriate network type based on the provided // chain params. - switch activeNet.Net { + switch opts.Params.Net { case wire.MainNet: // No extra flags since mainnet is the default case wire.TestNet3: - extraArgs = append(extraArgs, "--testnet") + opts.ExtraArgs = append(opts.ExtraArgs, "--testnet") case wire.TestNet: - extraArgs = append(extraArgs, "--regtest") + opts.ExtraArgs = append(opts.ExtraArgs, "--regtest") case wire.SimNet: - extraArgs = append(extraArgs, "--simnet") + opts.ExtraArgs = append(opts.ExtraArgs, "--simnet") default: return nil, fmt.Errorf("rpctest.New must be called with one " + "of the supported chain networks") @@ -172,17 +194,16 @@ func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, return nil, err } - wallet, err := newMemWallet(activeNet, uint32(numTestInstances)) + wallet, err := newMemWallet(opts.Params, uint32(numTestInstances)) if err != nil { return nil, err } miningAddr := fmt.Sprintf("--miningaddr=%s", wallet.coinbaseAddr) - extraArgs = append(extraArgs, miningAddr) + opts.ExtraArgs = append(opts.ExtraArgs, miningAddr) - config, err := newConfig( - nodeTestData, certFile, keyFile, extraArgs, customExePath, - ) + config, err := newConfig(nodeTestData, certFile, keyFile, + opts.ExtraArgs, opts.CustomExePath) if err != nil { return nil, err } @@ -199,41 +220,45 @@ func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, nodeNum := numTestInstances numTestInstances++ - if handlers == nil { - handlers = &rpcclient.NotificationHandlers{} + if opts.Handlers == nil { + opts.Handlers = &rpcclient.NotificationHandlers{} } // If a handler for the OnFilteredBlock{Connected,Disconnected} callback // callback has already been set, then create a wrapper callback which // executes both the currently registered callback and the mem wallet's // callback. - if handlers.OnFilteredBlockConnected != nil { - obc := handlers.OnFilteredBlockConnected - handlers.OnFilteredBlockConnected = func(height int32, header *wire.BlockHeader, filteredTxns []*btcutil.Tx) { + if opts.Handlers.OnFilteredBlockConnected != nil { + obc := opts.Handlers.OnFilteredBlockConnected + opts.Handlers.OnFilteredBlockConnected = func(height int32, + header *wire.BlockHeader, filteredTxns []*btcutil.Tx) { + wallet.IngestBlock(height, header, filteredTxns) obc(height, header, filteredTxns) } } else { // Otherwise, we can claim the callback ourselves. - handlers.OnFilteredBlockConnected = wallet.IngestBlock + opts.Handlers.OnFilteredBlockConnected = wallet.IngestBlock } - if handlers.OnFilteredBlockDisconnected != nil { - obd := handlers.OnFilteredBlockDisconnected - handlers.OnFilteredBlockDisconnected = func(height int32, header *wire.BlockHeader) { + if opts.Handlers.OnFilteredBlockDisconnected != nil { + obd := opts.Handlers.OnFilteredBlockDisconnected + opts.Handlers.OnFilteredBlockDisconnected = func(height int32, + header *wire.BlockHeader) { + wallet.UnwindBlock(height, header) obd(height, header) } } else { - handlers.OnFilteredBlockDisconnected = wallet.UnwindBlock + opts.Handlers.OnFilteredBlockDisconnected = wallet.UnwindBlock } h := &Harness{ - handlers: handlers, + handlers: opts.Handlers, node: node, MaxConnRetries: DefaultMaxConnectionRetries, ConnectionRetryTimeout: DefaultConnectionRetryTimeout, testNodeDir: nodeTestData, - ActiveNet: activeNet, + ActiveNet: opts.Params, nodeNum: nodeNum, wallet: wallet, } @@ -245,14 +270,46 @@ func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, return h, nil } -// SetUp initializes the rpc test state. Initialization includes: starting up a +// New creates and initializes new instance of the rpc test harness. +// Optionally, websocket handlers and a specified configuration may be passed. +// In the case that a nil config is passed, a default configuration will be +// used. If a custom btcd executable is specified, it will be used to start the +// harness node. Otherwise a new binary is built on demand. +// +// NOTE: This function is safe for concurrent access. +func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, + extraArgs []string, customExePath string) (*Harness, error) { + + return New2(&HarnessOpts{ + Params: activeNet, + Handlers: handlers, + ExtraArgs: extraArgs, + CustomExePath: customExePath, + }) +} + +// SetUpOpts are options that can be passed to SetUp2 when starting the harness +// instance. +type SetUpOpts struct { + // CreateTestChain tells the harness to generate blocks. + CreateTestChain bool + + // NumMatureOutputs is the count of mature outputs to be generated. + NumMatureOutputs uint32 +} + +// SetUp2 initializes the rpc test state. Initialization includes: starting up a // simnet node, creating a websockets client and connecting to the started // node, and finally: optionally generating and submitting a testchain with a // configurable number of mature coinbase outputs coinbase outputs. // // NOTE: This method and TearDown should always be called from the same // goroutine as they are not concurrent safe. -func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error { +func (h *Harness) SetUp2(opts *SetUpOpts) error { + if opts == nil { + opts = &SetUpOpts{} + } + // Start the btcd node itself. This spawns a new process which will be // managed if err := h.node.start(); err != nil { @@ -279,9 +336,9 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error { // Create a test chain with the desired number of mature coinbase // outputs. - if createTestChain && numMatureOutputs != 0 { + if opts.CreateTestChain && opts.NumMatureOutputs != 0 { coinbaseMaturity := uint32(h.ActiveNet.CoinbaseMaturity) - numToGenerate := coinbaseMaturity + numMatureOutputs + numToGenerate := coinbaseMaturity + opts.NumMatureOutputs _, err := h.Client.Generate(numToGenerate) if err != nil { return err @@ -306,6 +363,20 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error { return nil } +// SetUp initializes the rpc test state. Initialization includes: starting up a +// simnet node, creating a websockets client and connecting to the started +// node, and finally: optionally generating and submitting a testchain with a +// configurable number of mature coinbase outputs coinbase outputs. +// +// NOTE: This method and TearDown should always be called from the same +// goroutine as they are not concurrent safe. +func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error { + return h.SetUp2(&SetUpOpts{ + CreateTestChain: createTestChain, + NumMatureOutputs: numMatureOutputs, + }) +} + // tearDown stops the running rpc test instance. All created processes are // killed, and temporary directories removed. // @@ -334,18 +405,30 @@ func (h *Harness) tearDown() error { return nil } -// TearDown stops the running rpc test instance. All created processes are +// TearDownOpts are options that can be passed to TearDown2. +type TearDownOpts struct{} + +// TearDown2 stops the running rpc test instance. All created processes are // killed, and temporary directories removed. // // NOTE: This method and SetUp should always be called from the same goroutine // as they are not concurrent safe. -func (h *Harness) TearDown() error { +func (h *Harness) TearDown2(opts *TearDownOpts) error { harnessStateMtx.Lock() defer harnessStateMtx.Unlock() return h.tearDown() } +// TearDown stops the running rpc test instance. All created processes are +// killed, and temporary directories removed. +// +// NOTE: This method and SetUp should always be called from the same goroutine +// as they are not concurrent safe. +func (h *Harness) TearDown() error { + return h.TearDown2(nil) +} + // connectRPCClient attempts to establish an RPC connection to the created btcd // process belonging to this Harness instance. If the initial connection // attempt fails, this function will retry h.maxConnRetries times, backing off From f6dcea1cd08015a972b8b8de4e133d877d563011 Mon Sep 17 00:00:00 2001 From: allocz Date: Fri, 7 Aug 2026 19:56:58 +0000 Subject: [PATCH 02/15] integration,rpctest: replace rpctest.New by rpctest.New2 rpctest.New was removed and rpctest.New2 renamed to rpctest.New, fixed broken tests due to the API change. --- integration/bip0009_test.go | 5 ++-- integration/chain_test.go | 7 +++--- integration/csv_fork_test.go | 9 ++++--- integration/getchaintips_test.go | 3 ++- .../invalidate_reconsider_block_test.go | 3 ++- integration/p2a_test.go | 7 ++---- integration/prune_test.go | 5 ++-- integration/rawtx_test.go | 7 +++--- integration/reorg_test.go | 5 ++-- integration/rpcserver_test.go | 7 ++---- integration/rpctest/rpc_harness.go | 24 +++---------------- integration/rpctest/rpc_harness_test.go | 13 +++++----- integration/sync_race_test.go | 12 ++++------ 13 files changed, 41 insertions(+), 66 deletions(-) diff --git a/integration/bip0009_test.go b/integration/bip0009_test.go index 28801beff9..c603ffe186 100644 --- a/integration/bip0009_test.go +++ b/integration/bip0009_test.go @@ -130,7 +130,8 @@ func assertSoftForkStatus(r *rpctest.Harness, t *testing.T, forkKey string, stat // specific soft fork deployment to test. func testBIP0009(t *testing.T, forkKey string, deploymentID uint32) { // Initialize the primary mining node with only the genesis block. - r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, nil, "") + opts := &rpctest.HarnessOpts{Params: &chaincfg.RegressionNetParams} + r, err := rpctest.New(opts) if err != nil { t.Fatalf("unable to create primary harness: %v", err) } @@ -383,7 +384,7 @@ func TestBIP0009Mining(t *testing.T) { t.Parallel() // Initialize the primary mining node with only the genesis block. - r, err := rpctest.New(&chaincfg.SimNetParams, nil, nil, "") + r, err := rpctest.New(nil) if err != nil { t.Fatalf("unable to create primary harness: %v", err) } diff --git a/integration/chain_test.go b/integration/chain_test.go index cfcd07cdcc..f3931781fe 100644 --- a/integration/chain_test.go +++ b/integration/chain_test.go @@ -8,7 +8,6 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil/v2" - "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcd/integration/rpctest" "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript/v2" @@ -26,8 +25,10 @@ func TestGetTxSpendingPrevOut(t *testing.T) { t.Parallel() // Boilerplate codetestDir to make a pruned node. - btcdCfg := []string{"--rejectnonstd", "--debuglevel=debug"} - r, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg, "") + opts := &rpctest.HarnessOpts{ + ExtraArgs: []string{"--rejectnonstd", "--debuglevel=debug"}, + } + r, err := rpctest.New(opts) require.NoError(t, err) // Setup the node. diff --git a/integration/csv_fork_test.go b/integration/csv_fork_test.go index 656f32cbc4..06a20cce5b 100644 --- a/integration/csv_fork_test.go +++ b/integration/csv_fork_test.go @@ -19,7 +19,6 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil/v2" - "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/integration/rpctest" "github.com/btcsuite/btcd/txscript/v2" @@ -111,8 +110,8 @@ func makeTestOutput(r *rpctest.Harness, t *testing.T, func TestBIP0113Activation(t *testing.T) { t.Parallel() - btcdCfg := []string{"--rejectnonstd"} - r, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg, "") + opts := &rpctest.HarnessOpts{ExtraArgs: []string{"--rejectnonstd"}} + r, err := rpctest.New(opts) if err != nil { t.Fatal("unable to create primary harness: ", err) } @@ -408,8 +407,8 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) { // (sequence locks) and BIP 112 rule-sets which add input-age based // relative lock times. - btcdCfg := []string{"--rejectnonstd"} - r, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg, "") + opts := &rpctest.HarnessOpts{ExtraArgs: []string{"--rejectnonstd"}} + r, err := rpctest.New(opts) if err != nil { t.Fatal("unable to create primary harness: ", err) } diff --git a/integration/getchaintips_test.go b/integration/getchaintips_test.go index 45750dd4d5..29ab2975f3 100644 --- a/integration/getchaintips_test.go +++ b/integration/getchaintips_test.go @@ -145,7 +145,8 @@ func TestGetChainTips(t *testing.T) { "0000000000000000000000000000000000000000000000000000" // Set up regtest chain. - r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, nil, "") + opts := &rpctest.HarnessOpts{Params: &chaincfg.RegressionNetParams} + r, err := rpctest.New(opts) if err != nil { t.Fatal("TestGetChainTips fail. Unable to create primary harness: ", err) } diff --git a/integration/invalidate_reconsider_block_test.go b/integration/invalidate_reconsider_block_test.go index 88d836eb89..2fc377860c 100644 --- a/integration/invalidate_reconsider_block_test.go +++ b/integration/invalidate_reconsider_block_test.go @@ -9,7 +9,8 @@ import ( func TestInvalidateAndReconsiderBlock(t *testing.T) { // Set up regtest chain. - r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, nil, "") + opts := &rpctest.HarnessOpts{Params: &chaincfg.RegressionNetParams} + r, err := rpctest.New(opts) if err != nil { t.Fatalf("TestInvalidateAndReconsiderBlock fail."+ "Unable to create primary harness: %v", err) diff --git a/integration/p2a_test.go b/integration/p2a_test.go index e986db23d0..87d2ff4a80 100644 --- a/integration/p2a_test.go +++ b/integration/p2a_test.go @@ -29,10 +29,8 @@ func TestPayToAnchorSimple(t *testing.T) { // default, but the sub-dust and non-empty-witness cases below rely on // standardness checks running, so we start the node with // --rejectnonstd. - btcdCfg := []string{"--rejectnonstd"} - harness, err := rpctest.New( - &chaincfg.SimNetParams, nil, btcdCfg, "", - ) + opts := &rpctest.HarnessOpts{ExtraArgs: []string{"--rejectnonstd"}} + harness, err := rpctest.New(opts) if err != nil { t.Fatalf("unable to create test harness: %v", err) } @@ -197,4 +195,3 @@ func TestPayToAnchorSimple(t *testing.T) { } }) } - diff --git a/integration/prune_test.go b/integration/prune_test.go index ef69916fd6..ecd4a03cd9 100644 --- a/integration/prune_test.go +++ b/integration/prune_test.go @@ -11,7 +11,6 @@ package integration import ( "testing" - "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcd/integration/rpctest" "github.com/stretchr/testify/require" ) @@ -20,8 +19,8 @@ func TestPrune(t *testing.T) { t.Parallel() // Boilerplate code to make a pruned node. - btcdCfg := []string{"--prune=1536"} - r, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg, "") + opts := &rpctest.HarnessOpts{ExtraArgs: []string{"--prune=1536"}} + r, err := rpctest.New(opts) require.NoError(t, err) if err := r.SetUp(false, 0); err != nil { diff --git a/integration/rawtx_test.go b/integration/rawtx_test.go index a211d7d2d8..0d4f00b875 100644 --- a/integration/rawtx_test.go +++ b/integration/rawtx_test.go @@ -9,7 +9,6 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil/v2" - "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcd/integration/rpctest" "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript/v2" @@ -27,8 +26,10 @@ func TestTestMempoolAccept(t *testing.T) { t.Parallel() // Boilerplate codetestDir to make a pruned node. - btcdCfg := []string{"--rejectnonstd", "--debuglevel=debug"} - r, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg, "") + opts := &rpctest.HarnessOpts{ + ExtraArgs: []string{"--rejectnonstd", "--debuglevel=debug"}, + } + r, err := rpctest.New(opts) require.NoError(t, err) // Setup the node. diff --git a/integration/reorg_test.go b/integration/reorg_test.go index bfdbe95c20..6c6ef3ea5f 100644 --- a/integration/reorg_test.go +++ b/integration/reorg_test.go @@ -5,7 +5,6 @@ import ( "time" "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcd/integration/rpctest" "github.com/btcsuite/btcd/rpcclient" "github.com/stretchr/testify/require" @@ -33,12 +32,12 @@ func TestReorgFromForkPoint(t *testing.T) { forkBranchLen = int32(shorterBlocks) ) - longer, err := rpctest.New(&chaincfg.SimNetParams, nil, []string{}, "") + longer, err := rpctest.New(nil) require.NoError(t, err) require.NoError(t, longer.SetUp(false, 0)) t.Cleanup(func() { require.NoError(t, longer.TearDown()) }) - shorter, err := rpctest.New(&chaincfg.SimNetParams, nil, []string{}, "") + shorter, err := rpctest.New(nil) require.NoError(t, err) require.NoError(t, shorter.SetUp(false, 0)) t.Cleanup(func() { require.NoError(t, shorter.TearDown()) }) diff --git a/integration/rpcserver_test.go b/integration/rpcserver_test.go index 0649644682..d86895a289 100644 --- a/integration/rpcserver_test.go +++ b/integration/rpcserver_test.go @@ -17,7 +17,6 @@ import ( "time" "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/integration/rpctest" "github.com/btcsuite/btcd/rpcclient" @@ -307,10 +306,8 @@ func TestMain(m *testing.M) { // In order to properly test scenarios on as if we were on mainnet, // ensure that non-standard transactions aren't accepted into the // mempool or relayed. - btcdCfg := []string{"--rejectnonstd"} - primaryHarness, err = rpctest.New( - &chaincfg.SimNetParams, nil, btcdCfg, "", - ) + opts := &rpctest.HarnessOpts{ExtraArgs: []string{"--rejectnonstd"}} + primaryHarness, err = rpctest.New(opts) if err != nil { fmt.Println("unable to create primary harness: ", err) os.Exit(1) diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index 45102a8c27..0f49a0b889 100644 --- a/integration/rpctest/rpc_harness.go +++ b/integration/rpctest/rpc_harness.go @@ -127,7 +127,7 @@ type Harness struct { sync.Mutex } -// HarnessOpts are option that can be passed to New2 when initializing the +// HarnessOpts are options that can be passed to New when initializing the // harness instance. type HarnessOpts struct { // Params are the parameters of the network to be used, if nil, SimNet @@ -146,10 +146,10 @@ type HarnessOpts struct { CustomExePath string } -// New2 creates and initializes a new instance of the rpctest Harness. +// New creates and initializes a new instance of the rpctest Harness. // // NOTE: This function is safe for concurrent access. -func New2(opts *HarnessOpts) (*Harness, error) { +func New(opts *HarnessOpts) (*Harness, error) { harnessStateMtx.Lock() defer harnessStateMtx.Unlock() @@ -270,24 +270,6 @@ func New2(opts *HarnessOpts) (*Harness, error) { return h, nil } -// New creates and initializes new instance of the rpc test harness. -// Optionally, websocket handlers and a specified configuration may be passed. -// In the case that a nil config is passed, a default configuration will be -// used. If a custom btcd executable is specified, it will be used to start the -// harness node. Otherwise a new binary is built on demand. -// -// NOTE: This function is safe for concurrent access. -func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, - extraArgs []string, customExePath string) (*Harness, error) { - - return New2(&HarnessOpts{ - Params: activeNet, - Handlers: handlers, - ExtraArgs: extraArgs, - CustomExePath: customExePath, - }) -} - // SetUpOpts are options that can be passed to SetUp2 when starting the harness // instance. type SetUpOpts struct { diff --git a/integration/rpctest/rpc_harness_test.go b/integration/rpctest/rpc_harness_test.go index 3fa8da2ba1..e8a49ab7b0 100644 --- a/integration/rpctest/rpc_harness_test.go +++ b/integration/rpctest/rpc_harness_test.go @@ -15,7 +15,6 @@ import ( "time" "github.com/btcsuite/btcd/btcutil/v2" - "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/txscript/v2" "github.com/btcsuite/btcd/wire/v2" @@ -106,7 +105,7 @@ func assertConnectedTo(t *testing.T, nodeA *Harness, nodeB *Harness) { func testConnectNode(r *Harness, t *testing.T) { // Create a fresh test harness. - harness, err := New(&chaincfg.SimNetParams, nil, nil, "") + harness, err := New(nil) if err != nil { t.Fatal(err) } @@ -154,7 +153,7 @@ func testActiveHarnesses(r *Harness, t *testing.T) { numInitialHarnesses := len(ActiveHarnesses()) // Create a single test harness. - harness1, err := New(&chaincfg.SimNetParams, nil, nil, "") + harness1, err := New(nil) if err != nil { t.Fatal(err) } @@ -182,7 +181,7 @@ func testJoinMempools(r *Harness, t *testing.T) { // Create a local test harness with only the genesis block. The nodes // will be synced below so the same transaction can be sent to both // nodes without it being an orphan. - harness, err := New(&chaincfg.SimNetParams, nil, nil, "") + harness, err := New(nil) if err != nil { t.Fatal(err) } @@ -282,7 +281,7 @@ func testJoinMempools(r *Harness, t *testing.T) { func testJoinBlocks(r *Harness, t *testing.T) { // Create a second harness with only the genesis block so it is behind // the main harness. - harness, err := New(&chaincfg.SimNetParams, nil, nil, "") + harness, err := New(nil) if err != nil { t.Fatal(err) } @@ -470,7 +469,7 @@ func testGenerateAndSubmitBlockWithCustomCoinbaseOutputs(r *Harness, func testMemWalletReorg(r *Harness, t *testing.T) { // Create a fresh harness, we'll be using the main harness to force a // re-org on this local harness. - harness, err := New(&chaincfg.SimNetParams, nil, nil, "") + harness, err := New(nil) if err != nil { t.Fatal(err) } @@ -567,7 +566,7 @@ const ( func TestMain(m *testing.M) { var err error - mainHarness, err = New(&chaincfg.SimNetParams, nil, nil, "") + mainHarness, err = New(nil) if err != nil { fmt.Println("unable to create main harness: ", err) os.Exit(1) diff --git a/integration/sync_race_test.go b/integration/sync_race_test.go index b678fa1809..fc7d88a4f4 100644 --- a/integration/sync_race_test.go +++ b/integration/sync_race_test.go @@ -11,7 +11,6 @@ import ( "testing" "time" - "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcd/integration/rpctest" "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire/v2" @@ -299,9 +298,8 @@ func TestSyncManagerRaceCorruption(t *testing.T) { // This test deliberately opens more concurrent peers than the default // inbound limit. Raise the harness limit so admission does not dilute the // sync-manager lifecycle stress this test is intended to apply. - stressedHarness, err := rpctest.New( - &chaincfg.SimNetParams, nil, []string{"--maxpeers=400"}, "", - ) + opts := &rpctest.HarnessOpts{ExtraArgs: []string{"--maxpeers=400"}} + stressedHarness, err := rpctest.New(opts) require.NoError(t, err) require.NoError(t, stressedHarness.SetUp(true, 0)) t.Cleanup(func() { @@ -326,7 +324,7 @@ func TestSyncManagerRaceCorruption(t *testing.T) { // Prove corruption: connect a live node and generate blocks. If // the stressed node was corrupted (dead sync peer, 0 connected // peers), it will not sync from the new one. - newHarness, err := rpctest.New(&chaincfg.SimNetParams, nil, nil, "") + newHarness, err := rpctest.New(nil) require.NoError(t, err) require.NoError(t, newHarness.SetUp(true, 0)) defer func() { _ = newHarness.TearDown() }() @@ -420,7 +418,7 @@ func dialPreVerackPeer(nodeAddr string) (net.Conn, error) { // produced (no peerAdd), since peerLifecycleHandler only sends // peerAdd when verAckCh is closed. The node must remain healthy. func TestPreVerackDisconnect(t *testing.T) { - harness, err := rpctest.New(&chaincfg.SimNetParams, nil, nil, "") + harness, err := rpctest.New(nil) require.NoError(t, err) require.NoError(t, harness.SetUp(true, 0)) t.Cleanup(func() { _ = harness.TearDown() }) @@ -463,7 +461,7 @@ func TestPreVerackDisconnect(t *testing.T) { // Verify the node is still healthy: connect a real peer, generate // blocks, and confirm the harness syncs them. - helper, err := rpctest.New(&chaincfg.SimNetParams, nil, nil, "") + helper, err := rpctest.New(nil) require.NoError(t, err) require.NoError(t, helper.SetUp(true, 0)) defer func() { _ = helper.TearDown() }() From 72f5420d8623878066e5b4ede7ac5ef15433ce50 Mon Sep 17 00:00:00 2001 From: allocz Date: Mon, 10 Aug 2026 18:14:08 +0000 Subject: [PATCH 03/15] rpctest,integration: replace harness SetUp by SetUp2 --- integration/bip0009_test.go | 4 ++-- integration/chain_test.go | 6 +++++- integration/csv_fork_test.go | 12 +++++++++-- integration/getchaintips_test.go | 2 +- .../invalidate_reconsider_block_test.go | 2 +- integration/p2a_test.go | 6 +++++- integration/prune_test.go | 2 +- integration/rawtx_test.go | 6 +++++- integration/reorg_test.go | 4 ++-- integration/rpcserver_test.go | 6 +++++- integration/rpctest/rpc_harness.go | 20 +++---------------- integration/rpctest/rpc_harness_test.go | 18 ++++++++++++----- integration/sync_race_test.go | 8 ++++---- 13 files changed, 57 insertions(+), 39 deletions(-) diff --git a/integration/bip0009_test.go b/integration/bip0009_test.go index c603ffe186..b8d3a1124a 100644 --- a/integration/bip0009_test.go +++ b/integration/bip0009_test.go @@ -135,7 +135,7 @@ func testBIP0009(t *testing.T, forkKey string, deploymentID uint32) { if err != nil { t.Fatalf("unable to create primary harness: %v", err) } - if err := r.SetUp(false, 0); err != nil { + if err := r.SetUp(nil); err != nil { t.Fatalf("unable to setup test chain: %v", err) } defer r.TearDown() @@ -388,7 +388,7 @@ func TestBIP0009Mining(t *testing.T) { if err != nil { t.Fatalf("unable to create primary harness: %v", err) } - if err := r.SetUp(true, 0); err != nil { + if err := r.SetUp(nil); err != nil { t.Fatalf("unable to setup test chain: %v", err) } defer r.TearDown() diff --git a/integration/chain_test.go b/integration/chain_test.go index f3931781fe..d8a67175cd 100644 --- a/integration/chain_test.go +++ b/integration/chain_test.go @@ -32,7 +32,11 @@ func TestGetTxSpendingPrevOut(t *testing.T) { require.NoError(t, err) // Setup the node. - require.NoError(t, r.SetUp(true, 100)) + sOpts := &rpctest.SetUpOpts{ + CreateTestChain: true, + NumMatureOutputs: 100, + } + require.NoError(t, r.SetUp(sOpts)) t.Cleanup(func() { require.NoError(t, r.TearDown()) }) diff --git a/integration/csv_fork_test.go b/integration/csv_fork_test.go index 06a20cce5b..232ff8c138 100644 --- a/integration/csv_fork_test.go +++ b/integration/csv_fork_test.go @@ -115,7 +115,11 @@ func TestBIP0113Activation(t *testing.T) { if err != nil { t.Fatal("unable to create primary harness: ", err) } - if err := r.SetUp(true, 1); err != nil { + sOpts := &rpctest.SetUpOpts{ + CreateTestChain: true, + NumMatureOutputs: 1, + } + if err := r.SetUp(sOpts); err != nil { t.Fatalf("unable to setup test chain: %v", err) } defer r.TearDown() @@ -412,7 +416,11 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) { if err != nil { t.Fatal("unable to create primary harness: ", err) } - if err := r.SetUp(true, 1); err != nil { + sOpts := &rpctest.SetUpOpts{ + CreateTestChain: true, + NumMatureOutputs: 1, + } + if err := r.SetUp(sOpts); err != nil { t.Fatalf("unable to setup test chain: %v", err) } defer r.TearDown() diff --git a/integration/getchaintips_test.go b/integration/getchaintips_test.go index 29ab2975f3..bc4e3a7fb3 100644 --- a/integration/getchaintips_test.go +++ b/integration/getchaintips_test.go @@ -150,7 +150,7 @@ func TestGetChainTips(t *testing.T) { if err != nil { t.Fatal("TestGetChainTips fail. Unable to create primary harness: ", err) } - if err := r.SetUp(true, 0); err != nil { + if err := r.SetUp(nil); err != nil { t.Fatalf("TestGetChainTips fail. Unable to setup test chain: %v", err) } defer r.TearDown() diff --git a/integration/invalidate_reconsider_block_test.go b/integration/invalidate_reconsider_block_test.go index 2fc377860c..f7390ab072 100644 --- a/integration/invalidate_reconsider_block_test.go +++ b/integration/invalidate_reconsider_block_test.go @@ -15,7 +15,7 @@ func TestInvalidateAndReconsiderBlock(t *testing.T) { t.Fatalf("TestInvalidateAndReconsiderBlock fail."+ "Unable to create primary harness: %v", err) } - if err := r.SetUp(true, 0); err != nil { + if err := r.SetUp(nil); err != nil { t.Fatalf("TestInvalidateAndReconsiderBlock fail. "+ "Unable to setup test chain: %v", err) } diff --git a/integration/p2a_test.go b/integration/p2a_test.go index 87d2ff4a80..1862256c43 100644 --- a/integration/p2a_test.go +++ b/integration/p2a_test.go @@ -38,7 +38,11 @@ func TestPayToAnchorSimple(t *testing.T) { // Initialize the test harness with mining enabled to confirm // transactions. - err = harness.SetUp(true, 25) + sOpts := &rpctest.SetUpOpts{ + CreateTestChain: true, + NumMatureOutputs: 25, + } + err = harness.SetUp(sOpts) if err != nil { t.Fatalf("unable to setup test harness: %v", err) } diff --git a/integration/prune_test.go b/integration/prune_test.go index ecd4a03cd9..4cfcd839ef 100644 --- a/integration/prune_test.go +++ b/integration/prune_test.go @@ -23,7 +23,7 @@ func TestPrune(t *testing.T) { r, err := rpctest.New(opts) require.NoError(t, err) - if err := r.SetUp(false, 0); err != nil { + if err := r.SetUp(nil); err != nil { require.NoError(t, err) } t.Cleanup(func() { r.TearDown() }) diff --git a/integration/rawtx_test.go b/integration/rawtx_test.go index 0d4f00b875..98e38421e0 100644 --- a/integration/rawtx_test.go +++ b/integration/rawtx_test.go @@ -33,7 +33,11 @@ func TestTestMempoolAccept(t *testing.T) { require.NoError(t, err) // Setup the node. - require.NoError(t, r.SetUp(true, 100)) + sOpts := &rpctest.SetUpOpts{ + CreateTestChain: true, + NumMatureOutputs: 100, + } + require.NoError(t, r.SetUp(sOpts)) t.Cleanup(func() { require.NoError(t, r.TearDown()) }) diff --git a/integration/reorg_test.go b/integration/reorg_test.go index 6c6ef3ea5f..a45fb1ca7d 100644 --- a/integration/reorg_test.go +++ b/integration/reorg_test.go @@ -34,12 +34,12 @@ func TestReorgFromForkPoint(t *testing.T) { longer, err := rpctest.New(nil) require.NoError(t, err) - require.NoError(t, longer.SetUp(false, 0)) + require.NoError(t, longer.SetUp(nil)) t.Cleanup(func() { require.NoError(t, longer.TearDown()) }) shorter, err := rpctest.New(nil) require.NoError(t, err) - require.NoError(t, shorter.SetUp(false, 0)) + require.NoError(t, shorter.SetUp(nil)) t.Cleanup(func() { require.NoError(t, shorter.TearDown()) }) // Mine the shared chain on the longer node before connecting so that diff --git a/integration/rpcserver_test.go b/integration/rpcserver_test.go index d86895a289..cb91de50c5 100644 --- a/integration/rpcserver_test.go +++ b/integration/rpcserver_test.go @@ -316,7 +316,11 @@ func TestMain(m *testing.M) { // Initialize the primary mining node with a chain of length 125, // providing 25 mature coinbases to allow spending from for testing // purposes. - if err := primaryHarness.SetUp(true, 25); err != nil { + sOpts := &rpctest.SetUpOpts{ + CreateTestChain: true, + NumMatureOutputs: 25, + } + if err := primaryHarness.SetUp(sOpts); err != nil { fmt.Println("unable to setup test chain: ", err) // Even though the harness was not fully setup, it still needs diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index 0f49a0b889..188338dc1a 100644 --- a/integration/rpctest/rpc_harness.go +++ b/integration/rpctest/rpc_harness.go @@ -270,7 +270,7 @@ func New(opts *HarnessOpts) (*Harness, error) { return h, nil } -// SetUpOpts are options that can be passed to SetUp2 when starting the harness +// SetUpOpts are options that can be passed to SetUp when starting the harness // instance. type SetUpOpts struct { // CreateTestChain tells the harness to generate blocks. @@ -280,14 +280,14 @@ type SetUpOpts struct { NumMatureOutputs uint32 } -// SetUp2 initializes the rpc test state. Initialization includes: starting up a +// SetUp initializes the rpc test state. Initialization includes: starting up a // simnet node, creating a websockets client and connecting to the started // node, and finally: optionally generating and submitting a testchain with a // configurable number of mature coinbase outputs coinbase outputs. // // NOTE: This method and TearDown should always be called from the same // goroutine as they are not concurrent safe. -func (h *Harness) SetUp2(opts *SetUpOpts) error { +func (h *Harness) SetUp(opts *SetUpOpts) error { if opts == nil { opts = &SetUpOpts{} } @@ -345,20 +345,6 @@ func (h *Harness) SetUp2(opts *SetUpOpts) error { return nil } -// SetUp initializes the rpc test state. Initialization includes: starting up a -// simnet node, creating a websockets client and connecting to the started -// node, and finally: optionally generating and submitting a testchain with a -// configurable number of mature coinbase outputs coinbase outputs. -// -// NOTE: This method and TearDown should always be called from the same -// goroutine as they are not concurrent safe. -func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error { - return h.SetUp2(&SetUpOpts{ - CreateTestChain: createTestChain, - NumMatureOutputs: numMatureOutputs, - }) -} - // tearDown stops the running rpc test instance. All created processes are // killed, and temporary directories removed. // diff --git a/integration/rpctest/rpc_harness_test.go b/integration/rpctest/rpc_harness_test.go index e8a49ab7b0..67d5b5d094 100644 --- a/integration/rpctest/rpc_harness_test.go +++ b/integration/rpctest/rpc_harness_test.go @@ -109,7 +109,7 @@ func testConnectNode(r *Harness, t *testing.T) { if err != nil { t.Fatal(err) } - if err := harness.SetUp(false, 0); err != nil { + if err := harness.SetUp(nil); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } defer harness.TearDown() @@ -185,7 +185,7 @@ func testJoinMempools(r *Harness, t *testing.T) { if err != nil { t.Fatal(err) } - if err := harness.SetUp(false, 0); err != nil { + if err := harness.SetUp(nil); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } defer harness.TearDown() @@ -285,7 +285,7 @@ func testJoinBlocks(r *Harness, t *testing.T) { if err != nil { t.Fatal(err) } - if err := harness.SetUp(false, 0); err != nil { + if err := harness.SetUp(nil); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } defer harness.TearDown() @@ -473,7 +473,11 @@ func testMemWalletReorg(r *Harness, t *testing.T) { if err != nil { t.Fatal(err) } - if err := harness.SetUp(true, 5); err != nil { + sOpts := &SetUpOpts{ + CreateTestChain: true, + NumMatureOutputs: 5, + } + if err := harness.SetUp(sOpts); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } defer harness.TearDown() @@ -575,7 +579,11 @@ func TestMain(m *testing.M) { // Initialize the main mining node with a chain of length 125, // providing 25 mature coinbases to allow spending from for testing // purposes. - if err = mainHarness.SetUp(true, numMatureOutputs); err != nil { + sOpts := &SetUpOpts{ + CreateTestChain: true, + NumMatureOutputs: numMatureOutputs, + } + if err = mainHarness.SetUp(sOpts); err != nil { fmt.Println("unable to setup test chain: ", err) // Even though the harness was not fully setup, it still needs diff --git a/integration/sync_race_test.go b/integration/sync_race_test.go index fc7d88a4f4..98287fe328 100644 --- a/integration/sync_race_test.go +++ b/integration/sync_race_test.go @@ -301,7 +301,7 @@ func TestSyncManagerRaceCorruption(t *testing.T) { opts := &rpctest.HarnessOpts{ExtraArgs: []string{"--maxpeers=400"}} stressedHarness, err := rpctest.New(opts) require.NoError(t, err) - require.NoError(t, stressedHarness.SetUp(true, 0)) + require.NoError(t, stressedHarness.SetUp(nil)) t.Cleanup(func() { require.NoError(t, stressedHarness.TearDown()) }) @@ -326,7 +326,7 @@ func TestSyncManagerRaceCorruption(t *testing.T) { // peers), it will not sync from the new one. newHarness, err := rpctest.New(nil) require.NoError(t, err) - require.NoError(t, newHarness.SetUp(true, 0)) + require.NoError(t, newHarness.SetUp(nil)) defer func() { _ = newHarness.TearDown() }() require.NoError(t, rpctest.ConnectNode(stressedHarness, newHarness), @@ -420,7 +420,7 @@ func dialPreVerackPeer(nodeAddr string) (net.Conn, error) { func TestPreVerackDisconnect(t *testing.T) { harness, err := rpctest.New(nil) require.NoError(t, err) - require.NoError(t, harness.SetUp(true, 0)) + require.NoError(t, harness.SetUp(nil)) t.Cleanup(func() { _ = harness.TearDown() }) nodeAddr := harness.P2PAddress() @@ -463,7 +463,7 @@ func TestPreVerackDisconnect(t *testing.T) { // blocks, and confirm the harness syncs them. helper, err := rpctest.New(nil) require.NoError(t, err) - require.NoError(t, helper.SetUp(true, 0)) + require.NoError(t, helper.SetUp(nil)) defer func() { _ = helper.TearDown() }() require.NoError(t, rpctest.ConnectNode(harness, helper)) From 3f4f1b723cc7ba649200be788d79a50069c80db2 Mon Sep 17 00:00:00 2001 From: allocz Date: Mon, 10 Aug 2026 18:25:55 +0000 Subject: [PATCH 04/15] rpctest,integration: replace harness teardown by teardown2 --- integration/bip0009_test.go | 4 ++-- integration/chain_test.go | 2 +- integration/csv_fork_test.go | 4 ++-- integration/getchaintips_test.go | 2 +- integration/invalidate_reconsider_block_test.go | 2 +- integration/p2a_test.go | 2 +- integration/prune_test.go | 2 +- integration/rawtx_test.go | 2 +- integration/reorg_test.go | 4 ++-- integration/rpcserver_test.go | 2 +- integration/rpctest/rpc_harness.go | 15 +++------------ integration/rpctest/rpc_harness_test.go | 12 ++++++------ integration/sync_race_test.go | 8 ++++---- 13 files changed, 26 insertions(+), 35 deletions(-) diff --git a/integration/bip0009_test.go b/integration/bip0009_test.go index b8d3a1124a..9f01ba9b92 100644 --- a/integration/bip0009_test.go +++ b/integration/bip0009_test.go @@ -138,7 +138,7 @@ func testBIP0009(t *testing.T, forkKey string, deploymentID uint32) { if err := r.SetUp(nil); err != nil { t.Fatalf("unable to setup test chain: %v", err) } - defer r.TearDown() + defer r.TearDown(nil) // Short-circuit deployments that are configured as always active. if deploymentID < uint32(len(r.ActiveNet.Deployments)) { @@ -391,7 +391,7 @@ func TestBIP0009Mining(t *testing.T) { if err := r.SetUp(nil); err != nil { t.Fatalf("unable to setup test chain: %v", err) } - defer r.TearDown() + defer r.TearDown(nil) // Assert the chain only consists of the genesis block. assertChainHeight(r, t, 0) diff --git a/integration/chain_test.go b/integration/chain_test.go index d8a67175cd..360be9effa 100644 --- a/integration/chain_test.go +++ b/integration/chain_test.go @@ -38,7 +38,7 @@ func TestGetTxSpendingPrevOut(t *testing.T) { } require.NoError(t, r.SetUp(sOpts)) t.Cleanup(func() { - require.NoError(t, r.TearDown()) + require.NoError(t, r.TearDown(nil)) }) // Create a tx and testing outpoints. diff --git a/integration/csv_fork_test.go b/integration/csv_fork_test.go index 232ff8c138..beaba12e66 100644 --- a/integration/csv_fork_test.go +++ b/integration/csv_fork_test.go @@ -122,7 +122,7 @@ func TestBIP0113Activation(t *testing.T) { if err := r.SetUp(sOpts); err != nil { t.Fatalf("unable to setup test chain: %v", err) } - defer r.TearDown() + defer r.TearDown(nil) // Create a fresh output for usage within the test below. const outputValue = btcutil.SatoshiPerBitcoin @@ -423,7 +423,7 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) { if err := r.SetUp(sOpts); err != nil { t.Fatalf("unable to setup test chain: %v", err) } - defer r.TearDown() + defer r.TearDown(nil) assertSoftForkStatus(r, t, csvKey, blockchain.ThresholdStarted) diff --git a/integration/getchaintips_test.go b/integration/getchaintips_test.go index bc4e3a7fb3..64f91d5fea 100644 --- a/integration/getchaintips_test.go +++ b/integration/getchaintips_test.go @@ -153,7 +153,7 @@ func TestGetChainTips(t *testing.T) { if err := r.SetUp(nil); err != nil { t.Fatalf("TestGetChainTips fail. Unable to setup test chain: %v", err) } - defer r.TearDown() + defer r.TearDown(nil) // Immediately call getchaintips after setting up regtest. gotChainTips, err := r.Client.GetChainTips() diff --git a/integration/invalidate_reconsider_block_test.go b/integration/invalidate_reconsider_block_test.go index f7390ab072..a3315b9189 100644 --- a/integration/invalidate_reconsider_block_test.go +++ b/integration/invalidate_reconsider_block_test.go @@ -19,7 +19,7 @@ func TestInvalidateAndReconsiderBlock(t *testing.T) { t.Fatalf("TestInvalidateAndReconsiderBlock fail. "+ "Unable to setup test chain: %v", err) } - defer r.TearDown() + defer r.TearDown(nil) // Generate 4 blocks. // diff --git a/integration/p2a_test.go b/integration/p2a_test.go index 1862256c43..a912c6b270 100644 --- a/integration/p2a_test.go +++ b/integration/p2a_test.go @@ -34,7 +34,7 @@ func TestPayToAnchorSimple(t *testing.T) { if err != nil { t.Fatalf("unable to create test harness: %v", err) } - defer harness.TearDown() + defer harness.TearDown(nil) // Initialize the test harness with mining enabled to confirm // transactions. diff --git a/integration/prune_test.go b/integration/prune_test.go index 4cfcd839ef..6a67be1502 100644 --- a/integration/prune_test.go +++ b/integration/prune_test.go @@ -26,7 +26,7 @@ func TestPrune(t *testing.T) { if err := r.SetUp(nil); err != nil { require.NoError(t, err) } - t.Cleanup(func() { r.TearDown() }) + t.Cleanup(func() { r.TearDown(nil) }) // Check that the rpc call for block chain info comes back correctly. chainInfo, err := r.Client.GetBlockChainInfo() diff --git a/integration/rawtx_test.go b/integration/rawtx_test.go index 98e38421e0..fa1bba1492 100644 --- a/integration/rawtx_test.go +++ b/integration/rawtx_test.go @@ -39,7 +39,7 @@ func TestTestMempoolAccept(t *testing.T) { } require.NoError(t, r.SetUp(sOpts)) t.Cleanup(func() { - require.NoError(t, r.TearDown()) + require.NoError(t, r.TearDown(nil)) }) // Create testing txns. diff --git a/integration/reorg_test.go b/integration/reorg_test.go index a45fb1ca7d..34b731eaa1 100644 --- a/integration/reorg_test.go +++ b/integration/reorg_test.go @@ -35,12 +35,12 @@ func TestReorgFromForkPoint(t *testing.T) { longer, err := rpctest.New(nil) require.NoError(t, err) require.NoError(t, longer.SetUp(nil)) - t.Cleanup(func() { require.NoError(t, longer.TearDown()) }) + t.Cleanup(func() { require.NoError(t, longer.TearDown(nil)) }) shorter, err := rpctest.New(nil) require.NoError(t, err) require.NoError(t, shorter.SetUp(nil)) - t.Cleanup(func() { require.NoError(t, shorter.TearDown()) }) + t.Cleanup(func() { require.NoError(t, shorter.TearDown(nil)) }) // Mine the shared chain on the longer node before connecting so that // it is "current" and can serve headers/blocks to the shorter node. diff --git a/integration/rpcserver_test.go b/integration/rpcserver_test.go index cb91de50c5..afb0e255c5 100644 --- a/integration/rpcserver_test.go +++ b/integration/rpcserver_test.go @@ -328,7 +328,7 @@ func TestMain(m *testing.M) { // directories are cleaned up. The error is intentionally // ignored since this is already an error path and nothing else // could be done about it anyways. - _ = primaryHarness.TearDown() + _ = primaryHarness.TearDown(nil) os.Exit(1) } diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index 188338dc1a..49561ca736 100644 --- a/integration/rpctest/rpc_harness.go +++ b/integration/rpctest/rpc_harness.go @@ -373,30 +373,21 @@ func (h *Harness) tearDown() error { return nil } -// TearDownOpts are options that can be passed to TearDown2. +// TearDownOpts are options that can be passed to TearDown. type TearDownOpts struct{} -// TearDown2 stops the running rpc test instance. All created processes are +// TearDown stops the running rpc test instance. All created processes are // killed, and temporary directories removed. // // NOTE: This method and SetUp should always be called from the same goroutine // as they are not concurrent safe. -func (h *Harness) TearDown2(opts *TearDownOpts) error { +func (h *Harness) TearDown(opts *TearDownOpts) error { harnessStateMtx.Lock() defer harnessStateMtx.Unlock() return h.tearDown() } -// TearDown stops the running rpc test instance. All created processes are -// killed, and temporary directories removed. -// -// NOTE: This method and SetUp should always be called from the same goroutine -// as they are not concurrent safe. -func (h *Harness) TearDown() error { - return h.TearDown2(nil) -} - // connectRPCClient attempts to establish an RPC connection to the created btcd // process belonging to this Harness instance. If the initial connection // attempt fails, this function will retry h.maxConnRetries times, backing off diff --git a/integration/rpctest/rpc_harness_test.go b/integration/rpctest/rpc_harness_test.go index 67d5b5d094..bc35152278 100644 --- a/integration/rpctest/rpc_harness_test.go +++ b/integration/rpctest/rpc_harness_test.go @@ -112,7 +112,7 @@ func testConnectNode(r *Harness, t *testing.T) { if err := harness.SetUp(nil); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } - defer harness.TearDown() + defer harness.TearDown(nil) // Establish a p2p connection from our new local harness to the main // harness. @@ -157,7 +157,7 @@ func testActiveHarnesses(r *Harness, t *testing.T) { if err != nil { t.Fatal(err) } - defer harness1.TearDown() + defer harness1.TearDown(nil) // With the harness created above, a single harness should be detected // as active. @@ -188,7 +188,7 @@ func testJoinMempools(r *Harness, t *testing.T) { if err := harness.SetUp(nil); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } - defer harness.TearDown() + defer harness.TearDown(nil) nodeSlice := []*Harness{r, harness} @@ -288,7 +288,7 @@ func testJoinBlocks(r *Harness, t *testing.T) { if err := harness.SetUp(nil); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } - defer harness.TearDown() + defer harness.TearDown(nil) nodeSlice := []*Harness{r, harness} blocksSynced := make(chan struct{}) @@ -480,7 +480,7 @@ func testMemWalletReorg(r *Harness, t *testing.T) { if err := harness.SetUp(sOpts); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } - defer harness.TearDown() + defer harness.TearDown(nil) // The internal wallet of this harness should now have 250 BTC. expectedBalance := btcutil.Amount(250 * btcutil.SatoshiPerBitcoin) @@ -591,7 +591,7 @@ func TestMain(m *testing.M) { // directories are cleaned up. The error is intentionally // ignored since this is already an error path and nothing else // could be done about it anyways. - _ = mainHarness.TearDown() + _ = mainHarness.TearDown(nil) os.Exit(1) } diff --git a/integration/sync_race_test.go b/integration/sync_race_test.go index 98287fe328..476ecdf027 100644 --- a/integration/sync_race_test.go +++ b/integration/sync_race_test.go @@ -303,7 +303,7 @@ func TestSyncManagerRaceCorruption(t *testing.T) { require.NoError(t, err) require.NoError(t, stressedHarness.SetUp(nil)) t.Cleanup(func() { - require.NoError(t, stressedHarness.TearDown()) + require.NoError(t, stressedHarness.TearDown(nil)) }) nodeAddr := stressedHarness.P2PAddress() @@ -327,7 +327,7 @@ func TestSyncManagerRaceCorruption(t *testing.T) { newHarness, err := rpctest.New(nil) require.NoError(t, err) require.NoError(t, newHarness.SetUp(nil)) - defer func() { _ = newHarness.TearDown() }() + defer func() { _ = newHarness.TearDown(nil) }() require.NoError(t, rpctest.ConnectNode(stressedHarness, newHarness), "stressed node must connect to the new node") @@ -421,7 +421,7 @@ func TestPreVerackDisconnect(t *testing.T) { harness, err := rpctest.New(nil) require.NoError(t, err) require.NoError(t, harness.SetUp(nil)) - t.Cleanup(func() { _ = harness.TearDown() }) + t.Cleanup(func() { _ = harness.TearDown(nil) }) nodeAddr := harness.P2PAddress() @@ -464,7 +464,7 @@ func TestPreVerackDisconnect(t *testing.T) { helper, err := rpctest.New(nil) require.NoError(t, err) require.NoError(t, helper.SetUp(nil)) - defer func() { _ = helper.TearDown() }() + defer func() { _ = helper.TearDown(nil) }() require.NoError(t, rpctest.ConnectNode(harness, helper)) From f54a84c37c79816fe85f28cf9cb7fcdc2865df85 Mon Sep 17 00:00:00 2001 From: allocz Date: Tue, 11 Aug 2026 10:09:04 +0000 Subject: [PATCH 05/15] rpctest: do not ignore command error on node stop --- integration/rpctest/node.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/integration/rpctest/node.go b/integration/rpctest/node.go index b397bb00bc..7a28e86feb 100644 --- a/integration/rpctest/node.go +++ b/integration/rpctest/node.go @@ -5,6 +5,7 @@ package rpctest import ( + "errors" "fmt" "log" "os" @@ -215,11 +216,17 @@ func (n *node) stop() error { // or error starting the process return nil } - defer n.cmd.Wait() + + var signalErr error if runtime.GOOS == "windows" { - return n.cmd.Process.Signal(os.Kill) + signalErr = n.cmd.Process.Signal(os.Kill) + } else { + signalErr = n.cmd.Process.Signal(os.Interrupt) } - return n.cmd.Process.Signal(os.Interrupt) + + waitErr := n.cmd.Wait() + + return errors.Join(signalErr, waitErr) } // cleanup cleanups process and args files. The file housing the pid of the From ef0e48239f4eed263e94d5031cb689aaab026ba8 Mon Sep 17 00:00:00 2001 From: allocz Date: Tue, 11 Aug 2026 10:42:10 +0000 Subject: [PATCH 06/15] rpctest: add signal option to node shutdown With the signal option, we make it possible to skip sending the shutdown signal to the node, allowing the test of cases where we expect the node shutting down by itself. --- integration/rpctest/node.go | 21 +++++++++++++++------ integration/rpctest/rpc_harness.go | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/integration/rpctest/node.go b/integration/rpctest/node.go index 7a28e86feb..f3825ba8a1 100644 --- a/integration/rpctest/node.go +++ b/integration/rpctest/node.go @@ -209,8 +209,11 @@ func (n *node) start() error { // stop interrupts the running btcd process process, and waits until it exits // properly. On windows, interrupt is not supported, so a kill signal is used -// instead -func (n *node) stop() error { +// instead. +// +// When signal is false, we skip sending the signal and wait for the node +// process to stop by itself. +func (n *node) stop(signal bool) error { if n.cmd == nil || n.cmd.Process == nil { // return if not properly initialized // or error starting the process @@ -218,9 +221,11 @@ func (n *node) stop() error { } var signalErr error - if runtime.GOOS == "windows" { + switch { + case signal && runtime.GOOS == "windows": signalErr = n.cmd.Process.Signal(os.Kill) - } else { + + case signal: signalErr = n.cmd.Process.Signal(os.Interrupt) } @@ -248,8 +253,12 @@ func (n *node) cleanup() error { // shutdown terminates the running btcd process, and cleans up all // file/directories created by node. -func (n *node) shutdown() error { - if err := n.stop(); err != nil { +// +// signal being false means that we will wait until node shutdowns itself, we +// won't send a signal. This way we can test cases where the expected behavior +// is node shutdown. +func (n *node) shutdown(signal bool) error { + if err := n.stop(signal); err != nil { return err } if err := n.cleanup(); err != nil { diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index 49561ca736..e3ca0b9e6b 100644 --- a/integration/rpctest/rpc_harness.go +++ b/integration/rpctest/rpc_harness.go @@ -360,7 +360,7 @@ func (h *Harness) tearDown() error { h.BatchClient.WaitForShutdown() } - if err := h.node.shutdown(); err != nil { + if err := h.node.shutdown(true); err != nil { return err } From 26d999d153dfbed6c64cfaee7fe6abbe12792dd8 Mon Sep 17 00:00:00 2001 From: allocz Date: Tue, 11 Aug 2026 10:47:54 +0000 Subject: [PATCH 07/15] rpctest: do not skip cleanup when node stops with error --- integration/rpctest/node.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/integration/rpctest/node.go b/integration/rpctest/node.go index f3825ba8a1..7d2c4a29fd 100644 --- a/integration/rpctest/node.go +++ b/integration/rpctest/node.go @@ -258,13 +258,10 @@ func (n *node) cleanup() error { // won't send a signal. This way we can test cases where the expected behavior // is node shutdown. func (n *node) shutdown(signal bool) error { - if err := n.stop(signal); err != nil { - return err - } - if err := n.cleanup(); err != nil { - return err - } - return nil + stopErr := n.stop(signal) + cleanupErr := n.cleanup() + + return errors.Join(stopErr, cleanupErr) } // genCertPair generates a key/cert pair to the paths provided. From 609f6d0369c9d881bca074ea45d65c41cc135982 Mon Sep 17 00:00:00 2001 From: allocz Date: Tue, 11 Aug 2026 13:49:06 +0000 Subject: [PATCH 08/15] rpctest: allow node restart Node restart support was implemented by creating the node instance on harness SetUp instead of New, this way we can also run the same node several times with a different set of arguments, allowing more test cases to happen. An option `NoRPCClientAndWallet` was added to simplify test cases where for some reason the RPC Client won't connect to the instance. Options `SkipCleanup` and `NoSignal` added to tear down procedure, enabling the node to be restarted while the state is kept and also allowing the test of cases where the node should shutdown by itself instead of being shutdown upon receiving a signal. Tests covering node restart and exit error code implemented and added to the harness test table. --- integration/rpctest/rpc_harness.go | 79 +++++++++++++++++++------ integration/rpctest/rpc_harness_test.go | 45 ++++++++++++++ integration/rpctest/utils.go | 2 +- 3 files changed, 106 insertions(+), 20 deletions(-) diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index e3ca0b9e6b..0b907a0ed6 100644 --- a/integration/rpctest/rpc_harness.go +++ b/integration/rpctest/rpc_harness.go @@ -5,6 +5,7 @@ package rpctest import ( + "errors" "fmt" "math/rand/v2" "net" @@ -116,6 +117,7 @@ type Harness struct { Client *rpcclient.Client BatchClient *rpcclient.Client + nodeConfig *nodeConfig node *node handlers *rpcclient.NotificationHandlers @@ -211,12 +213,6 @@ func New(opts *HarnessOpts) (*Harness, error) { // Generate p2p+rpc listening addresses. config.listen, config.rpcListen = ListenAddressGenerator() - // Create the testing node bounded to the simnet. - node, err := newNode(config, nodeTestData) - if err != nil { - return nil, err - } - nodeNum := numTestInstances numTestInstances++ @@ -254,7 +250,7 @@ func New(opts *HarnessOpts) (*Harness, error) { h := &Harness{ handlers: opts.Handlers, - node: node, + nodeConfig: config, MaxConnRetries: DefaultMaxConnectionRetries, ConnectionRetryTimeout: DefaultConnectionRetryTimeout, testNodeDir: nodeTestData, @@ -278,6 +274,16 @@ type SetUpOpts struct { // NumMatureOutputs is the count of mature outputs to be generated. NumMatureOutputs uint32 + + // StartArgs are arguments passed to the btcd instance on start. This + // option enables restarting the node with different arguments, which + // is needed in some test cases. + StartArgs []string + + // NoRPCClientAndWallet skips the initialization of the RPC client and + // wallet. Note that enabling this option also disables generation of + // mature inputs and test chain. + NoRPCClientAndWallet bool } // SetUp initializes the rpc test state. Initialization includes: starting up a @@ -288,15 +294,30 @@ type SetUpOpts struct { // NOTE: This method and TearDown should always be called from the same // goroutine as they are not concurrent safe. func (h *Harness) SetUp(opts *SetUpOpts) error { + var err error if opts == nil { opts = &SetUpOpts{} } - // Start the btcd node itself. This spawns a new process which will be - // managed + // Create and start the btcd node bounded to the selected network. This + // spawns a new process which will be managed. + if h.node != nil { + return fmt.Errorf("node process already exists") + } + nodeConfig := *h.nodeConfig + nodeConfig.extra = append(nodeConfig.extra, opts.StartArgs...) + h.node, err = newNode(&nodeConfig, h.testNodeDir) + if err != nil { + return err + } if err := h.node.start(); err != nil { return fmt.Errorf("error starting node: %w", err) } + + if opts.NoRPCClientAndWallet { + return nil + } + if err := h.connectRPCClient(); err != nil { return fmt.Errorf("error connecting RPC client: %w", err) } @@ -345,11 +366,27 @@ func (h *Harness) SetUp(opts *SetUpOpts) error { return nil } +// TearDownOpts are options that can be passed to TearDown. +type TearDownOpts struct { + // SkipCleanup allows the harness to shutdown without cleaning up data, + // which is needed to test node restart. + SkipCleanup bool + + // NoSignal being true waits until node shutdowns itself instead of + // sending a termination signal. Needed for cases where we want to test + // node shutdown. + NoSignal bool +} + // tearDown stops the running rpc test instance. All created processes are // killed, and temporary directories removed. // // This function MUST be called with the harness state mutex held (for writes). -func (h *Harness) tearDown() error { +func (h *Harness) tearDown(opts *TearDownOpts) error { + if opts == nil { + opts = &TearDownOpts{} + } + if h.Client != nil { h.Client.Shutdown() h.Client.WaitForShutdown() @@ -360,22 +397,26 @@ func (h *Harness) tearDown() error { h.BatchClient.WaitForShutdown() } - if err := h.node.shutdown(true); err != nil { - return err + // Shutdown node only if it exists. + var shutdownErr error + if h.node != nil { + signal := !opts.NoSignal + shutdownErr = h.node.shutdown(signal) + h.node = nil } - if err := os.RemoveAll(h.testNodeDir); err != nil { - return err + // Perform cleanup only if not requested to skip it. + cleanup := !opts.SkipCleanup + var cleanupErr error + if cleanup { + cleanupErr = os.RemoveAll(h.testNodeDir) } delete(testInstances, h.testNodeDir) - return nil + return errors.Join(shutdownErr, cleanupErr) } -// TearDownOpts are options that can be passed to TearDown. -type TearDownOpts struct{} - // TearDown stops the running rpc test instance. All created processes are // killed, and temporary directories removed. // @@ -385,7 +426,7 @@ func (h *Harness) TearDown(opts *TearDownOpts) error { harnessStateMtx.Lock() defer harnessStateMtx.Unlock() - return h.tearDown() + return h.tearDown(opts) } // connectRPCClient attempts to establish an RPC connection to the created btcd diff --git a/integration/rpctest/rpc_harness_test.go b/integration/rpctest/rpc_harness_test.go index bc35152278..763140cc4b 100644 --- a/integration/rpctest/rpc_harness_test.go +++ b/integration/rpctest/rpc_harness_test.go @@ -9,8 +9,10 @@ package rpctest import ( + "errors" "fmt" "os" + "os/exec" "testing" "time" @@ -18,6 +20,7 @@ import ( "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/txscript/v2" "github.com/btcsuite/btcd/wire/v2" + "github.com/stretchr/testify/require" ) func testSendOutputs(r *Harness, t *testing.T) { @@ -550,6 +553,46 @@ func testMemWalletLockedOutputs(r *Harness, t *testing.T) { } } +func testNodeRestart(_ *Harness, t *testing.T) { + // Start the node and mine some blocks. + h, err := New(nil) + require.NoError(t, err) + err = h.SetUp(&SetUpOpts{CreateTestChain: true, NumMatureOutputs: 1}) + require.NoError(t, err) + count, err := h.Client.GetBlockCount() + require.NoError(t, err) + require.Equal(t, int64(101), count) + err = h.TearDown(&TearDownOpts{SkipCleanup: true}) + require.NoError(t, err) + + // Start the node again and assert the state was kept by checking that + // the block count remains. + err = h.SetUp(nil) + require.NoError(t, err) + count, err = h.Client.GetBlockCount() + require.NoError(t, err) + require.Equal(t, int64(101), count) + err = h.TearDown(nil) + require.NoError(t, err) + + // Confirm that the state was cleaned up by asserting that the node + // directory was deleted. + _, err = os.Stat(h.testNodeDir) + require.Equal(t, true, os.IsNotExist(err)) +} + +func testNodeExitError(_ *Harness, t *testing.T) { + // Start with invalid args, causing the node process to stop with error + // status code. + h, err := New(&HarnessOpts{ExtraArgs: []string{"--invalidflag=0"}}) + require.NoError(t, err) + err = h.SetUp(&SetUpOpts{NoRPCClientAndWallet: true}) + require.NoError(t, err) + err = h.TearDown(nil) + var exitErr *exec.ExitError + require.True(t, errors.As(err, &exitErr)) +} + var harnessTestCases = []HarnessTestCase{ testSendOutputs, testConnectNode, @@ -560,6 +603,8 @@ var harnessTestCases = []HarnessTestCase{ testGenerateAndSubmitBlockWithCustomCoinbaseOutputs, testMemWalletReorg, testMemWalletLockedOutputs, + testNodeRestart, + testNodeExitError, } var mainHarness *Harness diff --git a/integration/rpctest/utils.go b/integration/rpctest/utils.go index 43771a8e44..8a6f11126f 100644 --- a/integration/rpctest/utils.go +++ b/integration/rpctest/utils.go @@ -140,7 +140,7 @@ func TearDownAll() error { defer harnessStateMtx.Unlock() for _, harness := range testInstances { - if err := harness.tearDown(); err != nil { + if err := harness.tearDown(nil); err != nil { return err } } From ae7e8887a9bd62093174f1da1f716ced8db0684f Mon Sep 17 00:00:00 2001 From: allocz Date: Tue, 11 Aug 2026 14:06:13 +0000 Subject: [PATCH 09/15] rpctest: allow skip wallet wait There are cases where the RPC will work, but the wallet would not be able to finish sync up to best height, or we don't have reasons to wait for the sync to finish. Setting `NoWalletWait` to true allow us to skip waiting the wallet and speed up tests, or even avoid blocking forever. --- integration/rpctest/rpc_harness.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index 0b907a0ed6..c35c90a454 100644 --- a/integration/rpctest/rpc_harness.go +++ b/integration/rpctest/rpc_harness.go @@ -284,6 +284,11 @@ type SetUpOpts struct { // wallet. Note that enabling this option also disables generation of // mature inputs and test chain. NoRPCClientAndWallet bool + + // NoWalletWait can be set to never block waiting for wallet to catch up + // best node height. This is useful for tests where we need the RPC but + // don't want to wait for the wallet to sync. + NoWalletWait bool } // SetUp initializes the rpc test state. Initialization includes: starting up a @@ -348,6 +353,10 @@ func (h *Harness) SetUp(opts *SetUpOpts) error { } } + if opts.NoWalletWait { + return nil + } + // Block until the wallet has fully synced up to the tip of the main // chain. _, height, err := h.Client.GetBestBlock() From e4c37ef6ee785351b1708a15868accb01aae019c Mon Sep 17 00:00:00 2001 From: allocz Date: Tue, 11 Aug 2026 14:27:38 +0000 Subject: [PATCH 10/15] rpctest: fix memwallet goroutine leak Previously, each time harness setup was executed, a new goroutine was launched to handle the wallet updates, but the channel keeping the goroutine alive was never closed, causing goroutine leakage. The memwallet implementation was changed and now the memwallet goroutine is stopped when harness teardown is called. A test was added to assert that there's no goroutine leaks between harness setup and teardown. --- integration/rpctest/memwallet.go | 158 +++++++++++++++++++----- integration/rpctest/rpc_harness.go | 4 + integration/rpctest/rpc_harness_test.go | 16 +++ 3 files changed, 146 insertions(+), 32 deletions(-) diff --git a/integration/rpctest/memwallet.go b/integration/rpctest/memwallet.go index fd645f0201..2578991a6d 100644 --- a/integration/rpctest/memwallet.go +++ b/integration/rpctest/memwallet.go @@ -108,6 +108,9 @@ type memWallet struct { rpc *rpcclient.Client sync.RWMutex + + stop chan struct{} + done chan struct{} } // newMemWallet creates and returns a fully initialized instance of the @@ -146,6 +149,11 @@ func newMemWallet(net *chaincfg.Params, harnessID uint32) (*memWallet, error) { addrs := make(map[uint32]address.Address) addrs[0] = coinbaseAddr + stop := make(chan struct{}) + close(stop) + done := make(chan struct{}) + close(done) + return &memWallet{ net: net, coinbaseKey: coinbaseKey, @@ -154,14 +162,41 @@ func newMemWallet(net *chaincfg.Params, harnessID uint32) (*memWallet, error) { hdRoot: hdRoot, addrs: addrs, utxos: make(map[wire.OutPoint]*utxo), - chainUpdateSignal: make(chan struct{}), + chainUpdateSignal: make(chan struct{}, 1), reorgJournal: make(map[int32]*undoEntry), + stop: stop, + done: done, }, nil } // Start launches all goroutines required for the wallet to function properly. func (m *memWallet) Start() { - go m.chainSyncer() + select { + case <-m.done: + m.done = make(chan struct{}) + default: + return + } + + m.stop = make(chan struct{}) + go func() { + m.chainSyncer() + close(m.done) + }() +} + +// Stop sends a stop request to the wallet and wait until shutdown. +func (m *memWallet) Stop() { + select { + case <-m.stop: + return + default: + } + + close(m.stop) + <-m.done + + m.chainUpdates = nil } // SyncedHeight returns the height the wallet is known to be synced to. @@ -182,7 +217,15 @@ func (m *memWallet) SetRPCClient(rpcClient *rpcclient.Client) { // IngestBlock is a call-back which is to be triggered each time a new block is // connected to the main chain. It queues the update for the chain syncer, // calling the private version in sequential order. -func (m *memWallet) IngestBlock(height int32, header *wire.BlockHeader, filteredTxns []*btcutil.Tx) { +func (m *memWallet) IngestBlock(height int32, header *wire.BlockHeader, + filteredTxns []*btcutil.Tx) { + + select { + case <-m.stop: + return + default: + } + // Append this new chain update to the end of the queue of new chain // updates. m.chainMtx.Lock() @@ -190,12 +233,13 @@ func (m *memWallet) IngestBlock(height int32, header *wire.BlockHeader, filtered filteredTxns, true}) m.chainMtx.Unlock() - // Launch a goroutine to signal the chainSyncer that a new update is - // available. We do this in a new goroutine in order to avoid blocking - // the main loop of the rpc client. - go func() { - m.chainUpdateSignal <- struct{}{} - }() + // Signal the chainSyncer that a new update is available. We do this + // with a buffered channel in order to avoid blocking the main loop of + // the rpc client. + select { + case m.chainUpdateSignal <- struct{}{}: + default: + } } // ingestBlock updates the wallet's internal utxo state based on the outputs @@ -222,29 +266,72 @@ func (m *memWallet) ingestBlock(update *chainUpdate) { m.reorgJournal[update.blockHeight] = undo } +func (m *memWallet) processChainUpdate() { + // A new update is available, so pop the new chain + // update from the front of the update queue. + m.chainMtx.Lock() + update := m.chainUpdates[0] + // Set to nil to prevent GC leak. + m.chainUpdates[0] = nil + m.chainUpdates = m.chainUpdates[1:] + m.chainMtx.Unlock() + + m.Lock() + if update.isConnect { + m.ingestBlock(update) + } else { + m.unwindBlock(update) + } + m.Unlock() +} + +func (m *memWallet) processChainUpdates() { + m.chainMtx.Lock() + updatesAvailable := len(m.chainUpdates) + m.chainMtx.Unlock() + + // If we have multiple updates coming at the same time, the + // chainUpdateSignal may drop some signals, so every time a signal is + // received we iterate over all the chainUpdates received since the last + // signal. + // + // There's some possibilities here. + // + // 1. Received a single signal and a single chain update, in this + // case, the loop will be iterated once. + // + // 2. Lost some signals in a race and multiple updates will be processed + // now. + // + // 3. Processed all the updates but there was a signal left in the + // buffered channel that came from an update which was already + // processed, so no update will be processed. + // + // 4. While executing the loop below, new updates came, so there's a + // signal in the buffered channel and this procedure will be called + // again. + // + // In any case, there's no risk of not processing updates, nor + // panicking by trying to acess an update from a zero length + // chainUpdates slice. + for range updatesAvailable { + m.processChainUpdate() + } +} + // chainSyncer is a goroutine dedicated to processing new blocks in order to // keep the wallet's utxo state up to date. // // NOTE: This MUST be run as a goroutine. func (m *memWallet) chainSyncer() { - var update *chainUpdate - - for range m.chainUpdateSignal { - // A new update is available, so pop the new chain update from - // the front of the update queue. - m.chainMtx.Lock() - update = m.chainUpdates[0] - m.chainUpdates[0] = nil // Set to nil to prevent GC leak. - m.chainUpdates = m.chainUpdates[1:] - m.chainMtx.Unlock() - - m.Lock() - if update.isConnect { - m.ingestBlock(update) - } else { - m.unwindBlock(update) + for { + select { + case <-m.chainUpdateSignal: + m.processChainUpdates() + + case <-m.stop: + return } - m.Unlock() } } @@ -303,6 +390,12 @@ func (m *memWallet) evalInputs(inputs []*wire.TxIn, undo *undoEntry) { // disconnected from the main chain. It queues the update for the chain syncer, // calling the private version in sequential order. func (m *memWallet) UnwindBlock(height int32, header *wire.BlockHeader) { + select { + case <-m.stop: + return + default: + } + // Append this new chain update to the end of the queue of new chain // updates. m.chainMtx.Lock() @@ -310,12 +403,13 @@ func (m *memWallet) UnwindBlock(height int32, header *wire.BlockHeader) { nil, false}) m.chainMtx.Unlock() - // Launch a goroutine to signal the chainSyncer that a new update is - // available. We do this in a new goroutine in order to avoid blocking - // the main loop of the rpc client. - go func() { - m.chainUpdateSignal <- struct{}{} - }() + // Signal the chainSyncer that a new update is available. We use select + // with a buffered channel to avoid blocking the main loop of the rpc + // client. + select { + case m.chainUpdateSignal <- struct{}{}: + default: + } } // unwindBlock undoes the effect that a particular block had on the wallet's diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index c35c90a454..2abf193989 100644 --- a/integration/rpctest/rpc_harness.go +++ b/integration/rpctest/rpc_harness.go @@ -406,6 +406,10 @@ func (h *Harness) tearDown(opts *TearDownOpts) error { h.BatchClient.WaitForShutdown() } + if h.wallet != nil { + h.wallet.Stop() + } + // Shutdown node only if it exists. var shutdownErr error if h.node != nil { diff --git a/integration/rpctest/rpc_harness_test.go b/integration/rpctest/rpc_harness_test.go index 763140cc4b..e38eaf8dbf 100644 --- a/integration/rpctest/rpc_harness_test.go +++ b/integration/rpctest/rpc_harness_test.go @@ -13,6 +13,7 @@ import ( "fmt" "os" "os/exec" + "runtime" "testing" "time" @@ -593,6 +594,20 @@ func testNodeExitError(_ *Harness, t *testing.T) { require.True(t, errors.As(err, &exitErr)) } +func testNoGoroutineLeak(_ *Harness, t *testing.T) { + gStart := runtime.NumGoroutine() + defer func() { + time.Sleep(time.Millisecond * 50) + runtime.GC() + require.Equal(t, int(0), runtime.NumGoroutine()-gStart) + }() + + h, err := New(nil) + require.NoError(t, err) + require.NoError(t, h.SetUp(nil)) + require.NoError(t, h.TearDown(nil)) +} + var harnessTestCases = []HarnessTestCase{ testSendOutputs, testConnectNode, @@ -605,6 +620,7 @@ var harnessTestCases = []HarnessTestCase{ testMemWalletLockedOutputs, testNodeRestart, testNodeExitError, + testNoGoroutineLeak, } var mainHarness *Harness From 0edb830d8eb760c87e5921f6c1ced6a889ccf4e5 Mon Sep 17 00:00:00 2001 From: allocz Date: Tue, 11 Aug 2026 14:44:04 +0000 Subject: [PATCH 11/15] debugstream: implemented server and client The debug stream allows the register of debug events which are broadcasted to debug clients. The `debug` tag switches between the real and the nop implementation, so that there's no cost of sending debug events in production. --- debugstream/client.go | 113 ++++++++++++++++ debugstream/debug.go | 11 ++ debugstream/event.go | 61 +++++++++ debugstream/log.go | 23 ++++ debugstream/nop.go | 11 ++ debugstream/stream.go | 256 +++++++++++++++++++++++++++++++++++++ debugstream/stream_nop.go | 20 +++ debugstream/stream_test.go | 216 +++++++++++++++++++++++++++++++ 8 files changed, 711 insertions(+) create mode 100644 debugstream/client.go create mode 100644 debugstream/debug.go create mode 100644 debugstream/event.go create mode 100644 debugstream/log.go create mode 100644 debugstream/nop.go create mode 100644 debugstream/stream.go create mode 100644 debugstream/stream_nop.go create mode 100644 debugstream/stream_test.go diff --git a/debugstream/client.go b/debugstream/client.go new file mode 100644 index 0000000000..db64b9d9b0 --- /dev/null +++ b/debugstream/client.go @@ -0,0 +1,113 @@ +package debugstream + +import ( + "fmt" + "net" + "time" +) + +// Client calls handler with events coming from the [StreamServer]. +type Client struct { + addr string + + conn *net.TCPConn + handler func(e Event) + + stop chan struct{} + done chan struct{} +} + +// NewClient initializes a [Client] instance. +func NewClient(streamAddr string, handler func(ev Event)) *Client { + done := make(chan struct{}) + close(done) + stop := make(chan struct{}) + close(stop) + return &Client{ + addr: streamAddr, + handler: handler, + done: done, + stop: stop, + } +} + +func (c *Client) connect() (*net.TCPConn, error) { + const maxConnAttempts = 7 + var ( + conn net.Conn + err error + ) + + for i := range maxConnAttempts { + if i > 0 { + time.Sleep((time.Millisecond * 100) << (i - 1)) + } + conn, err = net.Dial("tcp", c.addr) + if err != nil { + continue + } + break + } + if err != nil { + return nil, err + } + + return conn.(*net.TCPConn), nil +} + +func (c *Client) loop() { + for { + var ev Event + err := ev.read(c.conn) + if err == nil { + c.handler(ev) + continue + } + select { + case <-c.stop: + return + default: + cliLog.Errorf("client loop %s", err) + return + } + } +} + +func (c *Client) Start() error { + select { + case <-c.done: + c.done = make(chan struct{}) + default: + return fmt.Errorf("client running") + } + + conn, err := c.connect() + if err != nil { + close(c.done) + return err + } + + c.conn = conn + c.stop = make(chan struct{}) + + go func() { + c.loop() + close(c.done) + }() + + return nil +} + +func (c *Client) Stop() { + select { + case <-c.stop: + return + default: + } + + close(c.stop) + c.conn.Close() + <-c.done + + c.conn = nil +} diff --git a/debugstream/debug.go b/debugstream/debug.go new file mode 100644 index 0000000000..727cedf9df --- /dev/null +++ b/debugstream/debug.go @@ -0,0 +1,11 @@ +//go:build debug + +package debugstream + +// Stream is the real implementation of the debug stream, [StreamServer] when +// compiling with debug tag. +type Stream = StreamServer + +func New() *Stream { + return NewStreamServer() +} diff --git a/debugstream/event.go b/debugstream/event.go new file mode 100644 index 0000000000..67d098410e --- /dev/null +++ b/debugstream/event.go @@ -0,0 +1,61 @@ +package debugstream + +import ( + "encoding/binary" + "fmt" + "io" +) + +// The following are guard event codes, that can be used to assert debug events +// in tests. +const ( + DEStart = iota + 1 + DEShutdown +) + +// Event is the message that is sent from the Stream to the Client. +type Event struct { + Code uint64 + Data []byte +} + +func (e *Event) write(w io.Writer) error { + err := binary.Write(w, binary.BigEndian, e.Code) + if err != nil { + return err + } + dataLen := uint64(len(e.Data)) + err = binary.Write(w, binary.BigEndian, dataLen) + if err != nil { + return err + } + n, err := w.Write(e.Data) + if err != nil { + return err + } + if n != int(dataLen) { + return fmt.Errorf("nWrite != dataLen") + } + return nil +} + +func (e *Event) read(r io.Reader) error { + err := binary.Read(r, binary.BigEndian, &e.Code) + if err != nil { + return err + } + var dataLen uint64 + err = binary.Read(r, binary.BigEndian, &dataLen) + if err != nil { + return err + } + e.Data = make([]byte, dataLen) + n, err := io.ReadFull(r, e.Data) + if err != nil { + return err + } + if n != int(dataLen) { + return fmt.Errorf("nRead != dataLen") + } + return nil +} diff --git a/debugstream/log.go b/debugstream/log.go new file mode 100644 index 0000000000..9c23b1aa7b --- /dev/null +++ b/debugstream/log.go @@ -0,0 +1,23 @@ +package debugstream + +import "github.com/btcsuite/btclog" + +var strLog btclog.Logger +var cliLog btclog.Logger + +func init() { + DisableLog() +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until either UseLogger or SetLogWriter are called. +func DisableLog() { + strLog, cliLog = btclog.Disabled, btclog.Disabled +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLoggers(strLogger, cliLogger btclog.Logger) { + strLog, cliLog = strLogger, cliLogger +} diff --git a/debugstream/nop.go b/debugstream/nop.go new file mode 100644 index 0000000000..73aee4f846 --- /dev/null +++ b/debugstream/nop.go @@ -0,0 +1,11 @@ +//go:build !debug + +package debugstream + +// Stream is a [StreamNOP] when debug flag is not set, effectively doing nothing +// because all its procedures are also NOP. +type Stream = StreamNOP + +func New() *Stream { + return NewStreamNOP() +} diff --git a/debugstream/stream.go b/debugstream/stream.go new file mode 100644 index 0000000000..68c744ac0a --- /dev/null +++ b/debugstream/stream.go @@ -0,0 +1,256 @@ +package debugstream + +import ( + "bytes" + "errors" + "fmt" + "net" + "slices" + "sync" + "time" +) + +// S is a global stream variable used to allow the broadcast of debug events. +var S *Stream + +type clientState struct { + conn *net.TCPConn + shouldClose bool + offset int +} + +// StreamServer broadcasts debug events to connected clients. +type StreamServer struct { + broadcastCh chan struct{} + events [][]byte + + mu sync.Mutex + clients []clientState + + stop chan struct{} + done chan struct{} + wg sync.WaitGroup +} + +// NewStreamServer returns a new stream instance. +func NewStreamServer() *StreamServer { + done := make(chan struct{}) + close(done) + stop := make(chan struct{}) + close(stop) + return &StreamServer{ + broadcastCh: make(chan struct{}, 1), + stop: stop, + done: done, + } +} + +// Broadcast sends the event to all connected event listeners. +func (s *StreamServer) Broadcast(e Event) { + select { + case <-s.stop: + return + default: + } + + var buf bytes.Buffer + _ = e.write(&buf) + s.mu.Lock() + s.events = append(s.events, buf.Bytes()) + s.mu.Unlock() + + select { + case s.broadcastCh <- struct{}{}: + default: + } +} + +func (s *StreamServer) sendNewEventsToClient(client *clientState) { + const maxWait = time.Second + + if client.offset >= len(s.events) { + // client up to date + return + } + + for ; client.offset < len(s.events)-1; client.offset++ { + client.conn.SetWriteDeadline(time.Now().Add(maxWait)) + _, err := client.conn.Write(s.events[client.offset+1]) + if err != nil { + strLog.Errorf("send to peer %s: %s", + client.conn.RemoteAddr(), err) + client.shouldClose = true + return + } + } +} + +func (s *StreamServer) broadcastEvents() { + const maxConcurrent = 10 + + sem := make(chan struct{}, maxConcurrent) + var wg sync.WaitGroup + for i := range s.clients { + peer := &s.clients[i] + sem <- struct{}{} + wg.Go(func() { + s.sendNewEventsToClient(peer) + <-sem + }) + } + wg.Wait() + + df := func(peer clientState) bool { + if !peer.shouldClose { + return false + } + peer.conn.Close() + return true + } + s.clients = slices.DeleteFunc(s.clients, df) +} + +func (s *StreamServer) broadcastLoop() { + for { + select { + case <-s.broadcastCh: + s.mu.Lock() + s.broadcastEvents() + s.mu.Unlock() + case <-s.stop: + return + } + } +} + +type acceptResult struct { + conn *net.TCPConn + err error +} + +func (s *StreamServer) connAccepter(connCh chan<- acceptResult, + lst *net.TCPListener) { + + for { + conn, err := lst.AcceptTCP() + if err != nil && errors.Is(err, net.ErrClosed) { + return + } + if err != nil { + connCh <- acceptResult{nil, err} + continue + } + connCh <- acceptResult{conn, nil} + } +} + +func (s *StreamServer) loop(listener *net.TCPListener) { + broadcastLoopDone := make(chan struct{}) + s.wg.Go(func() { + s.broadcastLoop() + close(broadcastLoopDone) + }) + + connCh := make(chan acceptResult) + connAccepterDone := make(chan struct{}) + s.wg.Go(func() { + s.connAccepter(connCh, listener) + close(connAccepterDone) + }) + +out: + for { + var conn *net.TCPConn + + select { + case <-s.stop: + listener.Close() + break out + case connRes := <-connCh: + if connRes.err != nil { + strLog.Error("accept err: %s", connRes.err) + continue + } + conn = connRes.conn + } + + s.mu.Lock() + s.clients = append(s.clients, clientState{ + conn: conn, + offset: -1, + }) + s.mu.Unlock() + + select { + case s.broadcastCh <- struct{}{}: + default: + } + } + +out2: + for { + select { + case <-connAccepterDone: + break out2 + case res := <-connCh: + if res.err == nil { + res.conn.Close() + } + } + } + + <-broadcastLoopDone +out3: + for { + select { + case <-s.broadcastCh: + default: + break out3 + } + } + + s.mu.Lock() + for _, p := range s.clients { + p.conn.Close() + } + s.clients = nil + s.events = nil + s.mu.Unlock() +} + +// Listen starts the stream listening at addr. +func (s *StreamServer) Listen(addr string) error { + select { + case <-s.done: + s.done = make(chan struct{}) + default: + return fmt.Errorf("server is not stopped") + } + + listener, err := net.Listen("tcp", addr) + if err != nil { + close(s.done) + return err + } + + s.stop = make(chan struct{}) + + s.wg.Go(func() { + s.loop(listener.(*net.TCPListener)) + close(s.done) + }) + + return nil +} + +// Shutdown stops the server. +func (s *StreamServer) Shutdown() { + select { + case <-s.stop: + return + default: + } + + close(s.stop) + s.wg.Wait() +} diff --git a/debugstream/stream_nop.go b/debugstream/stream_nop.go new file mode 100644 index 0000000000..fcd429d0f7 --- /dev/null +++ b/debugstream/stream_nop.go @@ -0,0 +1,20 @@ +package debugstream + +// StreamNOP is a NOP stream, can be used in production because all its methods +// are also NOP and therefore the compiler can optimize them out. +type StreamNOP struct { +} + +func NewStreamNOP() *StreamNOP { + return &StreamNOP{} +} + +func (s *StreamNOP) Broadcast(_ Event) { +} + +func (s *StreamNOP) Listen(_ string) error { + return nil +} + +func (s *StreamNOP) Shutdown() { +} diff --git a/debugstream/stream_test.go b/debugstream/stream_test.go new file mode 100644 index 0000000000..222e25b44a --- /dev/null +++ b/debugstream/stream_test.go @@ -0,0 +1,216 @@ +package debugstream + +import ( + "bytes" + "context" + "fmt" + "net" + "runtime" + "strings" + "testing" + "time" +) + +func genTCPListenAddr(t *testing.T) string { + conn, err := net.Listen("tcp4", "") + if err != nil { + t.Fatal(err) + } + addrPort := strings.Split(conn.Addr().String(), ":") + conn.Close() + return fmt.Sprintf("127.0.0.1:%s", addrPort[1]) +} + +// TestStreamProcedureSignatures verifies that the same [Stream] public +// interface is used with and without the debug build tag. +func TestStreamProcedureSignatures(t *testing.T) { + t.Parallel() + + stream := New() + err := stream.Listen(genTCPListenAddr(t)) + if err != nil { + t.Fatal(err) + } + stream.Broadcast(Event{Code: 0, Data: nil}) + stream.Shutdown() +} + +func testStreamServerAndClient(t *testing.T) { + listenAddr := genTCPListenAddr(t) + stream := NewStreamServer() + err := stream.Listen(listenAddr) + if err != nil { + t.Fatal(err) + } + defer stream.Shutdown() + + tests := []Event{ + {Code: 1, Data: []byte("event 1")}, + {Code: 2, Data: []byte("")}, + {Code: 3, Data: []byte("event 3")}, + } + + const ( + stStart uint64 = iota + st1 + st2 + stEnd + ) + var state uint64 + okCh := make(chan struct{}) + h := func(e Event) { + switch { + case state == stStart && e.Code == 1 && + bytes.Equal(tests[0].Data, e.Data): + + state = st1 + + case state == st1 && e.Code == 2 && + bytes.Equal(tests[1].Data, e.Data): + + state = st2 + + case state == st2 && e.Code == 3 && + bytes.Equal(tests[2].Data, e.Data): + + state = stEnd + okCh <- struct{}{} + } + } + + client := NewClient(listenAddr, h) + err = client.Start() + if err != nil { + t.Fatal(err) + } + defer client.Stop() + + // broadcast the debug events + for _, e := range tests { + stream.Broadcast(e) + } + + select { + case <-okCh: + case <-time.After(time.Second * 10): + t.Fatal("timeout") + } + + if state != stEnd { + t.Fatal("unexpected end state", state) + } +} + +func maxGoroutineLeak(max int) func(t *testing.T) { + gstart := runtime.NumGoroutine() + return func(t *testing.T) { + time.Sleep(time.Millisecond * 50) + gend := runtime.NumGoroutine() + if gend-gstart <= max { + return + } + t.Fatalf("%d goroutine leaks", gend-gstart) + } +} + +func TestStreamServerAndClient(t *testing.T) { + t.Parallel() + defer maxGoroutineLeak(0)(t) + + const count = 3 + for range count { + testStreamServerAndClient(t) + } +} + +func TestStreamServerAndClientRestart(t *testing.T) { + t.Parallel() + defer maxGoroutineLeak(0)(t) + + listenAddr := genTCPListenAddr(t) + stream := NewStreamServer() + + ctx, cancel := context.WithTimeout(t.Context(), time.Second*10) + defer cancel() + okCh := make(chan struct{}, 1) + hf := func(e Event) { + if e.Code != 1 { + return + } + okCh <- struct{}{} + } + client := NewClient(listenAddr, hf) + + matchF := func(t *testing.T) { + err := stream.Listen(listenAddr) + if err != nil { + t.Fatal(err) + } + + // broadcast the debug event + stream.Broadcast(Event{Code: 1}) + + err = client.Start() + if err != nil { + t.Fatal(err) + } + + select { + case <-okCh: + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + client.Stop() + stream.Shutdown() + } + + const count = 3 + for range count { + matchF(t) + } +} + +func setupStream(t *testing.T) (string, func()) { + listenAddr := genTCPListenAddr(t) + stream := NewStreamServer() + err := stream.Listen(listenAddr) + if err != nil { + t.Fatal(err) + } + + return listenAddr, stream.Shutdown +} + +func TestClientStartStop(t *testing.T) { + t.Parallel() + defer maxGoroutineLeak(0)(t) + + listenAddr, cleanup := setupStream(t) + defer cleanup() + + client := NewClient(listenAddr, func(_ Event) {}) + + // Must not return error when started successfully. + if err := client.Start(); err != nil { + t.Fatal(err) + } + + // Must not be started again while running. + if err := client.Start(); err == nil { + t.Fatal(err) + } + + // Must be started successfully after disconnecting from the server. + if err := client.conn.Close(); err != nil { + t.Fatal(err) + } + <-client.done + if err := client.Start(); err != nil { + t.Fatal(err) + } + + // Stop can be called multiple times. + client.Stop() + client.Stop() + client.Stop() +} From 0f188dba38773768e109f99b7a657da98ca41551 Mon Sep 17 00:00:00 2001 From: allocz Date: Tue, 11 Aug 2026 17:28:54 +0000 Subject: [PATCH 12/15] rpctest, btcd: integrated debugstream with btcd and test harness Now, when btcd is compiled with the `debug` tag and the flag --debugstream= is passed to btcd, the debug stream server will be started, allowing btcd to broadcast debug events. When the harness option `DebugHandler` callback is set, the harness will connect with the debug stream, and the debug events can be handled by the callback. --- btcd.go | 19 +++++++++++++++ config.go | 1 + integration/rpctest/btcd.go | 5 ++-- integration/rpctest/rpc_harness.go | 29 +++++++++++++++++++++++ integration/rpctest/rpc_harness_test.go | 31 +++++++++++++++++++++++++ log.go | 6 +++++ 6 files changed, 88 insertions(+), 3 deletions(-) diff --git a/btcd.go b/btcd.go index 1d9a1e5f6b..d13fccf0f0 100644 --- a/btcd.go +++ b/btcd.go @@ -18,6 +18,7 @@ import ( "github.com/btcsuite/btcd/blockchain/indexers" "github.com/btcsuite/btcd/database" + "github.com/btcsuite/btcd/debugstream" "github.com/btcsuite/btcd/limits" "github.com/btcsuite/btcd/ossec" ) @@ -56,6 +57,24 @@ func btcdMain(serverChan chan<- *server) error { } }() + // DebugStream is enabled only if btcd is compiled with the debug tag. + // Otherwise a nop implementation is used. + debugstream.S = debugstream.New() + if dsListen := cfg.DebugStream; dsListen != "" { + err := debugstream.S.Listen(dsListen) + if err != nil { + return fmt.Errorf("error starting debug stream: %v", + err) + } + debugstream.S.Broadcast(debugstream.Event{ + Code: debugstream.DEStart, + }) + defer debugstream.S.Shutdown() + defer debugstream.S.Broadcast(debugstream.Event{ + Code: debugstream.DEShutdown, + }) + } + // Get a channel that will be closed when a shutdown signal has been // triggered either from an OS signal such as SIGINT (Ctrl+C) or from // another subsystem such as the RPC server. diff --git a/config.go b/config.go index c33a533bb5..3674645e4c 100644 --- a/config.go +++ b/config.go @@ -129,6 +129,7 @@ type config struct { DataDir string `short:"b" long:"datadir" description:"Directory to store data"` DbType string `long:"dbtype" description:"Database backend to use for the Block Chain"` DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify =,=,... to set the log level for individual subsystems -- Use show to list available subsystems"` + DebugStream string `long:"debugstream" hidden:"true" description:"TCP listen address of the debug stream. To use this feature btcd must also be compiled with \"debug\" tag."` DropAddrIndex bool `long:"dropaddrindex" description:"Deletes the address-based transaction index from the database on start up and then exits."` DropCfIndex bool `long:"dropcfindex" description:"Deletes the index used for committed filtering (CF) support from the database on start up and then exits."` DropTxIndex bool `long:"droptxindex" description:"Deletes the hash-based transaction index from the database on start up and then exits."` diff --git a/integration/rpctest/btcd.go b/integration/rpctest/btcd.go index 22717a5c94..9b4d9ed9cd 100644 --- a/integration/rpctest/btcd.go +++ b/integration/rpctest/btcd.go @@ -53,9 +53,8 @@ func btcdExecutablePath() (string, error) { if runtime.GOOS == "windows" { outputPath += ".exe" } - cmd := exec.Command( - "go", "build", "-o", outputPath, "github.com/btcsuite/btcd", - ) + cmd := exec.Command("go", "build", "-tags=debug", "-o", outputPath, + "github.com/btcsuite/btcd") err = cmd.Run() if err != nil { return "", fmt.Errorf("Failed to build btcd: %v", err) diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index 2abf193989..78e03a97f5 100644 --- a/integration/rpctest/rpc_harness.go +++ b/integration/rpctest/rpc_harness.go @@ -21,6 +21,7 @@ import ( "github.com/btcsuite/btcd/btcutil/v2" "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/debugstream" "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire/v2" ) @@ -123,6 +124,8 @@ type Harness struct { wallet *memWallet + debugClient *debugstream.Client + testNodeDir string nodeNum int @@ -146,6 +149,10 @@ type HarnessOpts struct { // CustomExePath sets the path of the btcd executable, if empty an // executable is built on demand. CustomExePath string + + // DebugHandler is an optional callback that can be used to receive + // debug events. + DebugHandler func(e debugstream.Event) } // New creates and initializes a new instance of the rpctest Harness. @@ -204,6 +211,14 @@ func New(opts *HarnessOpts) (*Harness, error) { miningAddr := fmt.Sprintf("--miningaddr=%s", wallet.coinbaseAddr) opts.ExtraArgs = append(opts.ExtraArgs, miningAddr) + var debugClient *debugstream.Client + if opts.DebugHandler != nil { + port := NextAvailablePort() + addr := fmt.Sprintf("127.0.0.1:%d", port) + debugClient = debugstream.NewClient(addr, opts.DebugHandler) + opts.ExtraArgs = append(opts.ExtraArgs, "--debugstream="+addr) + } + config, err := newConfig(nodeTestData, certFile, keyFile, opts.ExtraArgs, opts.CustomExePath) if err != nil { @@ -257,6 +272,7 @@ func New(opts *HarnessOpts) (*Harness, error) { ActiveNet: opts.Params, nodeNum: nodeNum, wallet: wallet, + debugClient: debugClient, } // Track this newly created test instance within the package level @@ -319,6 +335,13 @@ func (h *Harness) SetUp(opts *SetUpOpts) error { return fmt.Errorf("error starting node: %w", err) } + if h.debugClient != nil { + err := h.debugClient.Start() + if err != nil { + return err + } + } + if opts.NoRPCClientAndWallet { return nil } @@ -427,6 +450,12 @@ func (h *Harness) tearDown(opts *TearDownOpts) error { delete(testInstances, h.testNodeDir) + // Stop debugClient after stopping the node in order to be able to + // process events sent by the node shutdown logic. + if h.debugClient != nil { + h.debugClient.Stop() + } + return errors.Join(shutdownErr, cleanupErr) } diff --git a/integration/rpctest/rpc_harness_test.go b/integration/rpctest/rpc_harness_test.go index e38eaf8dbf..4de7e822af 100644 --- a/integration/rpctest/rpc_harness_test.go +++ b/integration/rpctest/rpc_harness_test.go @@ -19,6 +19,7 @@ import ( "github.com/btcsuite/btcd/btcutil/v2" "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/debugstream" "github.com/btcsuite/btcd/txscript/v2" "github.com/btcsuite/btcd/wire/v2" "github.com/stretchr/testify/require" @@ -608,6 +609,35 @@ func testNoGoroutineLeak(_ *Harness, t *testing.T) { require.NoError(t, h.TearDown(nil)) } +func testDebugStream(_ *Harness, t *testing.T) { + type dhState byte + const ( + stateBegin dhState = iota + stateStarted + stateShutdown + ) + var state dhState + done := make(chan struct{}) + debugHandler := func(e debugstream.Event) { + switch { + case state == stateBegin && e.Code == debugstream.DEStart: + state = stateStarted + + case state == stateStarted && e.Code == debugstream.DEShutdown: + state = stateShutdown + close(done) + } + } + + h, err := New(&HarnessOpts{DebugHandler: debugHandler}) + require.NoError(t, err) + require.NoError(t, h.SetUp(nil)) + require.NoError(t, h.TearDown(nil)) + + <-done + require.Equal(t, stateShutdown, state) +} + var harnessTestCases = []HarnessTestCase{ testSendOutputs, testConnectNode, @@ -621,6 +651,7 @@ var harnessTestCases = []HarnessTestCase{ testNodeRestart, testNodeExitError, testNoGoroutineLeak, + testDebugStream, } var mainHarness *Harness diff --git a/log.go b/log.go index 54dcb9f110..d6afa762ec 100644 --- a/log.go +++ b/log.go @@ -15,6 +15,7 @@ import ( "github.com/btcsuite/btcd/blockchain/indexers" "github.com/btcsuite/btcd/connmgr" "github.com/btcsuite/btcd/database" + "github.com/btcsuite/btcd/debugstream" "github.com/btcsuite/btcd/internal/inbound" "github.com/btcsuite/btcd/mempool" "github.com/btcsuite/btcd/mining" @@ -62,6 +63,8 @@ var ( bcdbLog = backendLog.Logger("BCDB") btcdLog = backendLog.Logger("BTCD") chanLog = backendLog.Logger("CHAN") + debsLog = backendLog.Logger("DEBS") + debcLog = backendLog.Logger("DEBC") discLog = backendLog.Logger("DISC") indxLog = backendLog.Logger("INDX") minrLog = backendLog.Logger("MINR") @@ -81,6 +84,7 @@ func init() { database.UseLogger(bcdbLog) inbound.UseLogger(srvrLog) blockchain.UseLogger(chanLog) + debugstream.UseLoggers(debsLog, debcLog) indexers.UseLogger(indxLog) mining.UseLogger(minrLog) cpuminer.UseLogger(minrLog) @@ -99,6 +103,8 @@ var subsystemLoggers = map[string]btclog.Logger{ "BCDB": bcdbLog, "BTCD": btcdLog, "CHAN": chanLog, + "DEBS": debsLog, + "DEBC": debcLog, "DISC": discLog, "INDX": indxLog, "MINR": minrLog, From b5a3807d4a120c5dfb7f7e853e1947b45ba63e5a Mon Sep 17 00:00:00 2001 From: allocz Date: Tue, 11 Aug 2026 17:46:58 +0000 Subject: [PATCH 13/15] rpctest: add names to harness tests and improve formatting The change allow us to see the name and status of the tests running, while also allowing to filter the tests by regexp instead of having to comment when we want to test a single test case in isolation. TestHarness previouly contained lines not wrapped to 80 columns. --- integration/rpctest/rpc_harness_test.go | 81 +++++++++++++++++++------ 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/integration/rpctest/rpc_harness_test.go b/integration/rpctest/rpc_harness_test.go index 4de7e822af..cca41b4549 100644 --- a/integration/rpctest/rpc_harness_test.go +++ b/integration/rpctest/rpc_harness_test.go @@ -638,20 +638,63 @@ func testDebugStream(_ *Harness, t *testing.T) { require.Equal(t, stateShutdown, state) } -var harnessTestCases = []HarnessTestCase{ - testSendOutputs, - testConnectNode, - testActiveHarnesses, - testJoinBlocks, - testJoinMempools, // Depends on results of testJoinBlocks - testGenerateAndSubmitBlock, - testGenerateAndSubmitBlockWithCustomCoinbaseOutputs, - testMemWalletReorg, - testMemWalletLockedOutputs, - testNodeRestart, - testNodeExitError, - testNoGoroutineLeak, - testDebugStream, +var harnessTestCases = []struct { + name string + test HarnessTestCase +}{ + { + name: "testSendOutputs", + test: testSendOutputs, + }, + { + name: "testConnectNode", + test: testConnectNode, + }, + { + name: "testActiveHarnesses", + test: testActiveHarnesses, + }, + { + name: "testJoinBlocks", + test: testJoinBlocks, + }, + { + // Depends on results of testJoinBlocks + name: "testJoinMempools", + test: testJoinMempools, + }, + { + name: "testGenerateAndSubmitBlock", + test: testGenerateAndSubmitBlock, + }, + { + name: "testGenerateAndSubmitBlockWithCustomCoinbaseOutputs", + test: testGenerateAndSubmitBlockWithCustomCoinbaseOutputs, + }, + { + name: "testMemWalletReorg", + test: testMemWalletReorg, + }, + { + name: "testMemWalletLockedOutputs", + test: testMemWalletLockedOutputs, + }, + { + name: "testNodeRestart", + test: testNodeRestart, + }, + { + name: "testNodeExitError", + test: testNodeExitError, + }, + { + name: "testNoGoroutineLeak", + test: testNoGoroutineLeak, + }, + { + name: "testDebugStream", + test: testDebugStream, + }, } var mainHarness *Harness @@ -703,7 +746,8 @@ func TestMain(m *testing.M) { func TestHarness(t *testing.T) { // We should have (numMatureOutputs * 50 BTC) of mature unspendable // outputs. - expectedBalance := btcutil.Amount(numMatureOutputs * 50 * btcutil.SatoshiPerBitcoin) + expectedBalance := btcutil.Amount( + numMatureOutputs * 50 * btcutil.SatoshiPerBitcoin) harnessBalance := mainHarness.ConfirmedBalance() if harnessBalance != expectedBalance { t.Fatalf("expected wallet balance of %v instead have %v", @@ -716,14 +760,17 @@ func TestHarness(t *testing.T) { if err != nil { t.Fatalf("unable to execute getinfo on node: %v", err) } - expectedChainHeight := numMatureOutputs + uint32(mainHarness.ActiveNet.CoinbaseMaturity) + expectedChainHeight := numMatureOutputs + uint32( + mainHarness.ActiveNet.CoinbaseMaturity) if uint32(nodeInfo.Blocks) != expectedChainHeight { t.Errorf("Chain height is %v, should be %v", nodeInfo.Blocks, expectedChainHeight) } for _, testCase := range harnessTestCases { - testCase(mainHarness, t) + t.Run(testCase.name, func(t *testing.T) { + testCase.test(mainHarness, t) + }) } testTearDownAll(t) From c8a31d2169557e57de173e6e321b295d9dde512b Mon Sep 17 00:00:00 2001 From: allocz Date: Tue, 11 Aug 2026 18:04:31 +0000 Subject: [PATCH 14/15] rpctest: ignore redundant create test chain harness option Since the blocks are generated only when both CreateTestChain and NumMatureOutputs are set, there's no obvious reason to keep both. The code was simplified by ignoring the CreateTestChain option. --- integration/rpctest/rpc_harness.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index 78e03a97f5..9a0c93724a 100644 --- a/integration/rpctest/rpc_harness.go +++ b/integration/rpctest/rpc_harness.go @@ -367,7 +367,7 @@ func (h *Harness) SetUp(opts *SetUpOpts) error { // Create a test chain with the desired number of mature coinbase // outputs. - if opts.CreateTestChain && opts.NumMatureOutputs != 0 { + if opts.NumMatureOutputs > 0 { coinbaseMaturity := uint32(h.ActiveNet.CoinbaseMaturity) numToGenerate := coinbaseMaturity + opts.NumMatureOutputs _, err := h.Client.Generate(numToGenerate) From 56762b2273dba6a64ea8f73571a6ec272cbbe4e1 Mon Sep 17 00:00:00 2001 From: allocz Date: Tue, 11 Aug 2026 18:13:17 +0000 Subject: [PATCH 15/15] integration,rpctest: remove unused create test chain option The previous commit made CreateTestChain option useless, so this commit cleans up the codebase by effectively removing the option and all the references to it. --- integration/chain_test.go | 5 +---- integration/csv_fork_test.go | 10 ++-------- integration/p2a_test.go | 5 +---- integration/rawtx_test.go | 5 +---- integration/rpcserver_test.go | 5 +---- integration/rpctest/rpc_harness.go | 3 --- integration/rpctest/rpc_harness_test.go | 12 +++--------- 7 files changed, 9 insertions(+), 36 deletions(-) diff --git a/integration/chain_test.go b/integration/chain_test.go index 360be9effa..e354eb0804 100644 --- a/integration/chain_test.go +++ b/integration/chain_test.go @@ -32,10 +32,7 @@ func TestGetTxSpendingPrevOut(t *testing.T) { require.NoError(t, err) // Setup the node. - sOpts := &rpctest.SetUpOpts{ - CreateTestChain: true, - NumMatureOutputs: 100, - } + sOpts := &rpctest.SetUpOpts{NumMatureOutputs: 100} require.NoError(t, r.SetUp(sOpts)) t.Cleanup(func() { require.NoError(t, r.TearDown(nil)) diff --git a/integration/csv_fork_test.go b/integration/csv_fork_test.go index beaba12e66..372b8b0c2a 100644 --- a/integration/csv_fork_test.go +++ b/integration/csv_fork_test.go @@ -115,10 +115,7 @@ func TestBIP0113Activation(t *testing.T) { if err != nil { t.Fatal("unable to create primary harness: ", err) } - sOpts := &rpctest.SetUpOpts{ - CreateTestChain: true, - NumMatureOutputs: 1, - } + sOpts := &rpctest.SetUpOpts{NumMatureOutputs: 1} if err := r.SetUp(sOpts); err != nil { t.Fatalf("unable to setup test chain: %v", err) } @@ -416,10 +413,7 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) { if err != nil { t.Fatal("unable to create primary harness: ", err) } - sOpts := &rpctest.SetUpOpts{ - CreateTestChain: true, - NumMatureOutputs: 1, - } + sOpts := &rpctest.SetUpOpts{NumMatureOutputs: 1} if err := r.SetUp(sOpts); err != nil { t.Fatalf("unable to setup test chain: %v", err) } diff --git a/integration/p2a_test.go b/integration/p2a_test.go index a912c6b270..71579c7137 100644 --- a/integration/p2a_test.go +++ b/integration/p2a_test.go @@ -38,10 +38,7 @@ func TestPayToAnchorSimple(t *testing.T) { // Initialize the test harness with mining enabled to confirm // transactions. - sOpts := &rpctest.SetUpOpts{ - CreateTestChain: true, - NumMatureOutputs: 25, - } + sOpts := &rpctest.SetUpOpts{NumMatureOutputs: 25} err = harness.SetUp(sOpts) if err != nil { t.Fatalf("unable to setup test harness: %v", err) diff --git a/integration/rawtx_test.go b/integration/rawtx_test.go index fa1bba1492..c53eb938a6 100644 --- a/integration/rawtx_test.go +++ b/integration/rawtx_test.go @@ -33,10 +33,7 @@ func TestTestMempoolAccept(t *testing.T) { require.NoError(t, err) // Setup the node. - sOpts := &rpctest.SetUpOpts{ - CreateTestChain: true, - NumMatureOutputs: 100, - } + sOpts := &rpctest.SetUpOpts{NumMatureOutputs: 100} require.NoError(t, r.SetUp(sOpts)) t.Cleanup(func() { require.NoError(t, r.TearDown(nil)) diff --git a/integration/rpcserver_test.go b/integration/rpcserver_test.go index afb0e255c5..e36b701e45 100644 --- a/integration/rpcserver_test.go +++ b/integration/rpcserver_test.go @@ -316,10 +316,7 @@ func TestMain(m *testing.M) { // Initialize the primary mining node with a chain of length 125, // providing 25 mature coinbases to allow spending from for testing // purposes. - sOpts := &rpctest.SetUpOpts{ - CreateTestChain: true, - NumMatureOutputs: 25, - } + sOpts := &rpctest.SetUpOpts{NumMatureOutputs: 25} if err := primaryHarness.SetUp(sOpts); err != nil { fmt.Println("unable to setup test chain: ", err) diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index 9a0c93724a..1de0b5dac3 100644 --- a/integration/rpctest/rpc_harness.go +++ b/integration/rpctest/rpc_harness.go @@ -285,9 +285,6 @@ func New(opts *HarnessOpts) (*Harness, error) { // SetUpOpts are options that can be passed to SetUp when starting the harness // instance. type SetUpOpts struct { - // CreateTestChain tells the harness to generate blocks. - CreateTestChain bool - // NumMatureOutputs is the count of mature outputs to be generated. NumMatureOutputs uint32 diff --git a/integration/rpctest/rpc_harness_test.go b/integration/rpctest/rpc_harness_test.go index cca41b4549..146f5073c8 100644 --- a/integration/rpctest/rpc_harness_test.go +++ b/integration/rpctest/rpc_harness_test.go @@ -478,10 +478,7 @@ func testMemWalletReorg(r *Harness, t *testing.T) { if err != nil { t.Fatal(err) } - sOpts := &SetUpOpts{ - CreateTestChain: true, - NumMatureOutputs: 5, - } + sOpts := &SetUpOpts{NumMatureOutputs: 5} if err := harness.SetUp(sOpts); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } @@ -559,7 +556,7 @@ func testNodeRestart(_ *Harness, t *testing.T) { // Start the node and mine some blocks. h, err := New(nil) require.NoError(t, err) - err = h.SetUp(&SetUpOpts{CreateTestChain: true, NumMatureOutputs: 1}) + err = h.SetUp(&SetUpOpts{NumMatureOutputs: 1}) require.NoError(t, err) count, err := h.Client.GetBlockCount() require.NoError(t, err) @@ -714,10 +711,7 @@ func TestMain(m *testing.M) { // Initialize the main mining node with a chain of length 125, // providing 25 mature coinbases to allow spending from for testing // purposes. - sOpts := &SetUpOpts{ - CreateTestChain: true, - NumMatureOutputs: numMatureOutputs, - } + sOpts := &SetUpOpts{NumMatureOutputs: numMatureOutputs} if err = mainHarness.SetUp(sOpts); err != nil { fmt.Println("unable to setup test chain: ", err)