From e7fe6fd2ade449c21c0cfe08db687911a23aa4d4 Mon Sep 17 00:00:00 2001 From: allocz Date: Wed, 24 Jun 2026 20:08:36 +0000 Subject: [PATCH] integration/rpctest,debugstream,btcd: improve test harness integration/rpctest: Fixed a goroutine leak caused by the [memWallet.Start] procedure which was spinning up a goroutine that was never terminated. Also, added a new method [memWallet.Stop], which is called by [Harness.TearDown], sending a stop request to the wallet and waiting until the wallet stops. Also added a test to [TestHarness] to catch goroutine leakage. Changed [New], initializing the [Harness] with an options struct instead of several fields that may be set to the zero value of the type, cleaning up the API and improving extensibility. Previously [Harness.SetUp] was receiving a boolean to generate a test chain and a integer for the required number of mature outputs, but internally, the test chain was being generated only if the number of mature outputs was different than zero and the boolean was true, this means that we can remove the boolean and create the test chain only when the required numbed of mature UTXO's is greater than zero. Add configuration options to [Harness.SetUp] and [Harness.TearDown], enabling future functionality extension without breaking changes and also avoiding the need of passing zero value arguments to get the default config. Add option to [Harness.SetUp] allowing to skip starting RPC and Wallet, avoiding errors being returned in scenarios where the node shuts down before starting the RPC, like when dropping cfindex with --dropcfindex. Add option to [Harness.SetUp] allowing to skip the wait for memwallet to sync up to the node best height, avoiding blocking forever when the node is shut down before the wallet receives all block connected events, because in this case wallet best height would be less than the node best height. Pass btcd args in [Harness.SetUp] instead of [New], enabling restarting the node with different configuration but same state. Make [Harness.TearDown] return error if the btcd process exits with non zero status code, previously the error returned by [exec.Cmd.Wait] was being ignored. This change enables tests to assert if the node stopped successfully or not. NoNodeCleanup option added to [Harness.TearDown], which makes possible to write tests that restart the node while keeping state. NoShutdownSignal option added to [Harness.Teardown], which instead of sending termination signal, blocks until the node process finishes enabling testing scenarios where the node process exits by itself. Added debug handler to [New] options, allowing integration tests to easily process debug events. The btcd executable used by the Harness is now being built with the debug tag, but the debug stream will be started only if the debug handler is also passed to the Harness. debugstream: The package debugstream provides two utility types, [Stream] and [Client]. [Stream] listens on a TCP endpoint and broadcasts events to connected clients. The [Event] type has a uint64 code and a byte slice data fields . The [Stream] implementation is used only when btcd is compiled with the debug tag, otherwise a nop implementation is used. [Client] connects with [Stream] via TCP and starts receiving the events , which are passed into a handler callback, allowing assertions inside tests. All events broadcasted in the stream are stored in memory and sent to all clients, even if the client connects after the broadcast of some events, it still receives all of them. btcd: btcd integrated with debugstream, so that we can send debug events and improve testing capabilities. The hidden flag --debugstream= starts the [Stream] when btcd is compiled with the debug tag, otherwise it is a NOP operation, using StreamNOP, which does nothing. --- btcd.go | 19 ++ config.go | 1 + debugstream/client.go | 113 ++++++++ debugstream/debug.go | 11 + debugstream/event.go | 61 +++++ debugstream/log.go | 23 ++ debugstream/nop.go | 11 + debugstream/stream.go | 251 ++++++++++++++++++ debugstream/stream_nop.go | 20 ++ debugstream/stream_test.go | 216 +++++++++++++++ integration/bip0009_test.go | 8 +- integration/chain_test.go | 12 +- integration/csv_fork_test.go | 24 +- integration/debugstream_test.go | 44 +++ integration/getchaintips_test.go | 4 +- .../invalidate_reconsider_block_test.go | 7 +- integration/p2a_test.go | 46 ++-- integration/prune_test.go | 9 +- integration/rawtx_test.go | 12 +- integration/reorg_test.go | 9 +- integration/rpcserver_test.go | 19 +- integration/rpctest/btcd.go | 5 +- integration/rpctest/memwallet.go | 145 ++++++++-- integration/rpctest/node.go | 129 ++++++--- integration/rpctest/rpc_harness.go | 244 +++++++++++++---- integration/rpctest/rpc_harness_test.go | 100 ++++++- integration/sync_race_test.go | 17 +- log.go | 6 + 28 files changed, 1343 insertions(+), 223 deletions(-) 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 create mode 100644 integration/debugstream_test.go 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 06a8798b06..43cb4d6464 100644 --- a/config.go +++ b/config.go @@ -120,6 +120,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/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..c87df40a50 --- /dev/null +++ b/debugstream/stream.go @@ -0,0 +1,251 @@ +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 <-connCh: + } + } + + <-broadcastLoopDone +out3: + for { + select { + case <-s.broadcastCh: + default: + break out3 + } + } + + for _, p := range s.clients { + p.conn.Close() + } + s.clients = nil + s.events = nil +} + +// 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..3ffa4265ed --- /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 testStreamServer(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 { + testStreamServer(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() +} diff --git a/integration/bip0009_test.go b/integration/bip0009_test.go index 28801beff9..65b644c18c 100644 --- a/integration/bip0009_test.go +++ b/integration/bip0009_test.go @@ -130,11 +130,11 @@ 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, "") + r, err := rpctest.New(rpctest.Opts{Net: &chaincfg.RegressionNetParams}) if err != nil { t.Fatalf("unable to create primary harness: %v", err) } - if err := r.SetUp(false, 0); err != nil { + if err := r.SetUp(); err != nil { t.Fatalf("unable to setup test chain: %v", err) } defer r.TearDown() @@ -383,11 +383,11 @@ 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() if err != nil { t.Fatalf("unable to create primary harness: %v", err) } - if err := r.SetUp(true, 0); err != nil { + if err := r.SetUp(); 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 cfcd07cdcc..568e40d6a6 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" @@ -25,13 +24,14 @@ import ( 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, "") + r, err := rpctest.New() require.NoError(t, err) - // Setup the node. - require.NoError(t, r.SetUp(true, 100)) + // Setup the pruned node. + require.NoError(t, r.SetUp(rpctest.SOpts{ + Args: []string{"--rejectnonstd", "--debuglevel=debug"}, + UTXOCount: 100, + })) t.Cleanup(func() { require.NoError(t, r.TearDown()) }) diff --git a/integration/csv_fork_test.go b/integration/csv_fork_test.go index 656f32cbc4..360eaa7d00 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,12 +110,15 @@ 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, "") + r, err := rpctest.New() if err != nil { t.Fatal("unable to create primary harness: ", err) } - if err := r.SetUp(true, 1); err != nil { + err = r.SetUp(rpctest.SOpts{ + Args: []string{"--rejectnonstd"}, + UTXOCount: 1, + }) + if err != nil { t.Fatalf("unable to setup test chain: %v", err) } defer r.TearDown() @@ -404,16 +406,18 @@ func assertTxInBlock(r *rpctest.Harness, t *testing.T, blockHash *chainhash.Hash func TestBIP0068AndBIP0112Activation(t *testing.T) { t.Parallel() + r, err := rpctest.New() + if err != nil { + t.Fatal("unable to create primary harness: ", err) + } // We'd like the test proper evaluation and validation of the BIP 68 // (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, "") + err = r.SetUp(rpctest.SOpts{ + Args: []string{"--rejectnonstd"}, + UTXOCount: 1, + }) if err != nil { - t.Fatal("unable to create primary harness: ", err) - } - if err := r.SetUp(true, 1); err != nil { t.Fatalf("unable to setup test chain: %v", err) } defer r.TearDown() diff --git a/integration/debugstream_test.go b/integration/debugstream_test.go new file mode 100644 index 0000000000..2128fbd472 --- /dev/null +++ b/integration/debugstream_test.go @@ -0,0 +1,44 @@ +//go:build rpctest + +package integration + +import ( + "context" + "testing" + "time" + + "github.com/btcsuite/btcd/debugstream" + "github.com/btcsuite/btcd/integration/rpctest" + "github.com/stretchr/testify/require" +) + +func TestDebugStream(t *testing.T) { + const ( + sBegin = iota + sNodeStarted + sNodeShutdown + ) + var state uint64 + ctx, cancel := context.WithTimeout(t.Context(), time.Second*10) + defer cancel() + debHandler := func(e debugstream.Event) { + switch { + case state == sBegin && e.Code == debugstream.DEStart: + state = sNodeStarted + + case state == sNodeStarted && e.Code == debugstream.DEShutdown: + state = sNodeShutdown + cancel() + } + } + + h, err := rpctest.New(rpctest.Opts{DebugHandler: debHandler}) + require.NoError(t, err) + h.SetUp() + + err = h.TearDown() + require.NoError(t, err) + + <-ctx.Done() + require.Equal(t, true, sNodeShutdown == state) +} diff --git a/integration/getchaintips_test.go b/integration/getchaintips_test.go index 45750dd4d5..6179a8b7b5 100644 --- a/integration/getchaintips_test.go +++ b/integration/getchaintips_test.go @@ -145,11 +145,11 @@ func TestGetChainTips(t *testing.T) { "0000000000000000000000000000000000000000000000000000" // Set up regtest chain. - r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, nil, "") + r, err := rpctest.New(rpctest.Opts{Net: &chaincfg.RegressionNetParams}) 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(); 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 88d836eb89..3793dba725 100644 --- a/integration/invalidate_reconsider_block_test.go +++ b/integration/invalidate_reconsider_block_test.go @@ -9,12 +9,12 @@ import ( func TestInvalidateAndReconsiderBlock(t *testing.T) { // Set up regtest chain. - r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, nil, "") + r, err := rpctest.New(rpctest.Opts{Net: &chaincfg.RegressionNetParams}) if err != nil { t.Fatalf("TestInvalidateAndReconsiderBlock fail."+ "Unable to create primary harness: %v", err) } - if err := r.SetUp(true, 0); err != nil { + if err := r.SetUp(); err != nil { t.Fatalf("TestInvalidateAndReconsiderBlock fail. "+ "Unable to setup test chain: %v", err) } @@ -57,6 +57,9 @@ func TestInvalidateAndReconsiderBlock(t *testing.T) { // Assert that block 1 is the active chaintip. bestHash, err := r.Client.GetBestBlockHash() + if err != nil { + t.Fatal(err) + } if *bestHash != *block1Hash { t.Fatalf("TestInvalidateAndReconsiderBlock fail. Expected the "+ "best block hash to be block 1 with hash %s but got %s", diff --git a/integration/p2a_test.go b/integration/p2a_test.go index e986db23d0..0d8e929ef1 100644 --- a/integration/p2a_test.go +++ b/integration/p2a_test.go @@ -24,23 +24,22 @@ func TestPayToAnchorSimple(t *testing.T) { t.Skip("Skipping P2A integration test in short mode") } - // Create a btcd instance for testing P2A functionality in a controlled - // environment. The simnet harness accepts non-standard transactions by - // 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, "", - ) + h, err := rpctest.New() if err != nil { t.Fatalf("unable to create test harness: %v", err) } - defer harness.TearDown() + defer h.TearDown() - // Initialize the test harness with mining enabled to confirm + // Start a btcd instance for testing P2A functionality in a controlled + // environment. The simnet harness accepts non-standard transactions by + // default, but the sub-dust and non-empty-witness cases below rely on + // standardness checks running, so we start the node with + // --rejectnonstd. The test harness has mining enabled to confirm // transactions. - err = harness.SetUp(true, 25) + err = h.SetUp(rpctest.SOpts{ + Args: []string{"--rejectnonstd"}, + UTXOCount: 25, + }) if err != nil { t.Fatalf("unable to setup test harness: %v", err) } @@ -59,7 +58,7 @@ func TestPayToAnchorSimple(t *testing.T) { // Use the harness to create a transaction that sends to the P2A // address. This handles all the UTXO selection and signing for us. amount := btcutil.Amount(10_000) - createP2ATxHash, err := harness.SendOutputs([]*wire.TxOut{ + createP2ATxHash, err := h.SendOutputs([]*wire.TxOut{ wire.NewTxOut(int64(amount), p2aPkScript), }, 10) if err != nil { @@ -67,7 +66,7 @@ func TestPayToAnchorSimple(t *testing.T) { } // Mine a block to confirm the P2A creation. - blockHashes, err := harness.Client.Generate(1) + blockHashes, err := h.Client.Generate(1) if err != nil { t.Fatalf("unable to generate block: %v", err) } @@ -88,7 +87,7 @@ func TestPayToAnchorSimple(t *testing.T) { spendP2ATx.AddTxIn(p2aInput) // Send the P2A funds to a regular address. - spendAddr, err := harness.NewAddress() + spendAddr, err := h.NewAddress() if err != nil { t.Fatalf("unable to get spend address: %v", err) } @@ -104,13 +103,13 @@ func TestPayToAnchorSimple(t *testing.T) { // Broadcast the spend transaction to verify network acceptance. P2A // outputs are witness programs and are validated through the normal // transaction validation path in the mempool and consensus. - spendTxHash, err := harness.Client.SendRawTransaction(spendP2ATx, true) + spendTxHash, err := h.Client.SendRawTransaction(spendP2ATx, true) if err != nil { t.Fatalf("unable to send P2A spend transaction: %v", err) } // Mine a block to confirm the spend. - blockHashes, err = harness.Client.Generate(1) + blockHashes, err = h.Client.Generate(1) if err != nil { t.Fatalf("unable to generate block after spend: %v", err) } @@ -118,7 +117,7 @@ func TestPayToAnchorSimple(t *testing.T) { // Ensure the spend transaction was actually mined to prove full P2A // support. // - block, err := harness.Client.GetBlock(blockHashes[0]) + block, err := h.Client.GetBlock(blockHashes[0]) if err != nil { t.Fatalf("unable to get block: %v", err) } @@ -144,13 +143,13 @@ func TestPayToAnchorSimple(t *testing.T) { // sub-dust P2A funding transaction is rejected as non-standard. t.Run("non-empty witness rejected", func(t *testing.T) { // Fund a fresh P2A output we can attempt to spend. - fundTxHash, err := harness.SendOutputs([]*wire.TxOut{ + fundTxHash, err := h.SendOutputs([]*wire.TxOut{ wire.NewTxOut(int64(amount), p2aPkScript), }, 10) if err != nil { t.Fatalf("unable to fund second P2A output: %v", err) } - if _, err := harness.Client.Generate(1); err != nil { + if _, err := h.Client.Generate(1); err != nil { t.Fatalf("unable to mine block: %v", err) } @@ -164,7 +163,7 @@ func TestPayToAnchorSimple(t *testing.T) { badTx.AddTxIn(input) badTx.AddTxOut(wire.NewTxOut(int64(amount-100), spendScript)) - if _, err := harness.Client.SendRawTransaction( + if _, err := h.Client.SendRawTransaction( badTx, true, ); err == nil { @@ -180,7 +179,7 @@ func TestPayToAnchorSimple(t *testing.T) { // Build a transaction paying sub-dust to P2A from a new // harness-funded input. CreateTransaction performs the // signing for us; the dust gate fires when we try to relay it. - dustTx, err := harness.CreateTransaction( + dustTx, err := h.CreateTransaction( []*wire.TxOut{wire.NewTxOut(subDust, p2aPkScript)}, 10, true, ) @@ -188,7 +187,7 @@ func TestPayToAnchorSimple(t *testing.T) { t.Fatalf("unable to build sub-dust funding tx: %v", err) } - if _, err := harness.Client.SendRawTransaction( + if _, err := h.Client.SendRawTransaction( dustTx, true, ); err == nil { @@ -197,4 +196,3 @@ func TestPayToAnchorSimple(t *testing.T) { } }) } - diff --git a/integration/prune_test.go b/integration/prune_test.go index ef69916fd6..85e7650863 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" ) @@ -19,12 +18,12 @@ import ( 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, "") + r, err := rpctest.New() require.NoError(t, err) - if err := r.SetUp(false, 0); err != nil { + // Setup a pruned node. + err = r.SetUp(rpctest.SOpts{Args: []string{"--prune=1536"}}) + if err != nil { require.NoError(t, err) } t.Cleanup(func() { r.TearDown() }) diff --git a/integration/rawtx_test.go b/integration/rawtx_test.go index a211d7d2d8..9bb956b388 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" @@ -26,13 +25,14 @@ import ( 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, "") + r, err := rpctest.New() require.NoError(t, err) - // Setup the node. - require.NoError(t, r.SetUp(true, 100)) + // Setup the pruned node. + require.NoError(t, r.SetUp(rpctest.SOpts{ + Args: []string{"--rejectnonstd", "--debuglevel=debug"}, + UTXOCount: 100, + })) t.Cleanup(func() { require.NoError(t, r.TearDown()) }) diff --git a/integration/reorg_test.go b/integration/reorg_test.go index bfdbe95c20..6fb559fb1b 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,14 +32,14 @@ func TestReorgFromForkPoint(t *testing.T) { forkBranchLen = int32(shorterBlocks) ) - longer, err := rpctest.New(&chaincfg.SimNetParams, nil, []string{}, "") + longer, err := rpctest.New() require.NoError(t, err) - require.NoError(t, longer.SetUp(false, 0)) + require.NoError(t, longer.SetUp()) t.Cleanup(func() { require.NoError(t, longer.TearDown()) }) - shorter, err := rpctest.New(&chaincfg.SimNetParams, nil, []string{}, "") + shorter, err := rpctest.New() require.NoError(t, err) - require.NoError(t, shorter.SetUp(false, 0)) + require.NoError(t, shorter.SetUp()) 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 0649644682..bdf6b863bf 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" @@ -304,13 +303,7 @@ var primaryHarness *rpctest.Harness func TestMain(m *testing.M) { var err error - // 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, "", - ) + primaryHarness, err = rpctest.New() if err != nil { fmt.Println("unable to create primary harness: ", err) os.Exit(1) @@ -319,7 +312,15 @@ 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 { + // + // 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. + err = primaryHarness.SetUp(rpctest.SOpts{ + Args: []string{"--rejectnonstd"}, + UTXOCount: 25, + }) + if 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/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/memwallet.go b/integration/rpctest/memwallet.go index fd645f0201..c1083a2182 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() @@ -222,29 +265,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 +389,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 +402,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/node.go b/integration/rpctest/node.go index b397bb00bc..1a38f94984 100644 --- a/integration/rpctest/node.go +++ b/integration/rpctest/node.go @@ -5,6 +5,7 @@ package rpctest import ( + "errors" "fmt" "log" "os" @@ -30,6 +31,7 @@ type nodeConfig struct { profile string debugLevel string extra []string + startArgs []string nodeDir string exe string @@ -131,6 +133,7 @@ func (n *nodeConfig) arguments() []string { args = append(args, fmt.Sprintf("--debuglevel=%s", n.debugLevel)) } args = append(args, n.extra...) + args = append(args, n.startArgs...) return args } @@ -166,6 +169,8 @@ type node struct { pidFile string dataDir string + + started bool } // newNode creates a new node instance according to the passed config. dataDir @@ -175,64 +180,55 @@ func newNode(config *nodeConfig, dataDir string) (*node, error) { return &node{ config: config, dataDir: dataDir, - cmd: config.command(), }, nil } -// start creates a new btcd process, and writes its pid in a file reserved for -// recording the pid of the launched process. This file can be used to -// terminate the process in case of a hang, or panic. In the case of a failing -// test case, or panic, it is important that the process be stopped via stop(), -// otherwise, it will persist unless explicitly killed. -func (n *node) start() error { - if err := n.cmd.Start(); err != nil { - return err +// 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. +// +// If noSignal is true, then we don't send a signal, but wait for the process to +// stop itself. +func (n *node) stop(noSignal bool) error { + if !n.started || n.cmd == nil || n.cmd.Process == nil { + // return if not properly initialized or error starting the + // process. + return nil } - pid, err := os.Create(filepath.Join(n.dataDir, "btcd.pid")) - if err != nil { - return err - } + var err error + switch { + case !noSignal && runtime.GOOS == "windows": + err = n.cmd.Process.Signal(os.Kill) - n.pidFile = pid.Name() - if _, err = fmt.Fprintf(pid, "%d\n", n.cmd.Process.Pid); err != nil { - return err + case !noSignal: + err = n.cmd.Process.Signal(os.Interrupt) } - if err := pid.Close(); err != nil { - return err + waitErr := n.cmd.Wait() + if waitErr != nil { + err = errors.Join(waitErr, err) } - return nil -} + n.started = false -// 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 { - if n.cmd == nil || n.cmd.Process == nil { - // return if not properly initialized - // or error starting the process - return nil - } - defer n.cmd.Wait() - if runtime.GOOS == "windows" { - return n.cmd.Process.Signal(os.Kill) - } - return n.cmd.Process.Signal(os.Interrupt) + return err } // cleanup cleanups process and args files. The file housing the pid of the // created process will be deleted, as well as any directories created by the // process. func (n *node) cleanup() error { - if n.pidFile != "" { - if err := os.Remove(n.pidFile); err != nil { - log.Printf("unable to remove file %s: %v", n.pidFile, - err) - } + if n.pidFile == "" { + return nil + } + + if err := os.Remove(n.pidFile); err != nil { + log.Printf("unable to remove file %s: %v", n.pidFile, err) } + n.pidFile = "" + // Since the node's main data directory is passed in to the node config, // it isn't our responsibility to clean it up. So we're done after // removing the pid file. @@ -241,13 +237,62 @@ 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 { +// +// If noSignal is true, shutdown will block until the process stops itself, +// without sending a signal to the process. +func (n *node) shutdown(noSignal bool) error { + if !n.started { + return nil + } + + err := n.stop(noSignal) + + if cleanupErr := n.cleanup(); cleanupErr != nil { + err = errors.Join(cleanupErr, err) + } + + return err +} + +// start creates a new btcd process, and writes its pid in a file reserved for +// recording the pid of the launched process. This file can be used to +// terminate the process in case of a hang, or panic. In the case of a failing +// test case, or panic, it is important that the process be stopped via stop(), +// otherwise, it will persist unless explicitly killed. +// +// args are appended to the node start command, enabling cases where the node +// is stopped an then started again with different args. +func (n *node) start(args []string) error { + if n.started { + return fmt.Errorf("node already started") + } + + n.config.startArgs = args + n.cmd = n.config.command() + + if err := n.cmd.Start(); err != nil { return err } - if err := n.cleanup(); err != nil { + + n.started = true + + pid, err := os.Create(filepath.Join(n.dataDir, "btcd.pid")) + if err != nil { + n.shutdown(false) return err } + + n.pidFile = pid.Name() + if _, err = fmt.Fprintf(pid, "%d\n", n.cmd.Process.Pid); err != nil { + n.shutdown(false) + return err + } + + if err := pid.Close(); err != nil { + n.shutdown(false) + return err + } + return nil } diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index 9c9cb85262..6d4d4bd519 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" @@ -20,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" ) @@ -121,28 +123,57 @@ type Harness struct { wallet *memWallet + debugClient *debugstream.Client + testNodeDir string nodeNum int sync.Mutex } +// Opts are options that can be passed to [New] when creating the [Harness], +// otherwise default configuration is used. +type Opts struct { + // Net allows overriding the default SimNet chain config params. + Net *chaincfg.Params + + // Handlers are websocket handlers that can be passed. + Handlers *rpcclient.NotificationHandlers + + // CustomExePath can be set to the path of a custom btcd executable, + // otherwise we build and use a default executable. + CustomExePath string + + // DebugHandler makes the harness start btcd with the debug stream + // enabled and use the DebugHandler as the handler of the debug + // events coming from btcd. + DebugHandler func(debugstream.Event) +} + // 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. +// +// To avoid the need of passing an empty or nil [Opts] to get default +// initialization, opts is variadic, but only the first argument (if present) +// is used. // // NOTE: This function is safe for concurrent access. -func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, - extraArgs []string, customExePath string) (*Harness, error) { - +func New(opts ...Opts) (*Harness, error) { harnessStateMtx.Lock() defer harnessStateMtx.Unlock() + var o Opts + if len(opts) != 0 { + o = opts[0] + } + if o.Net == nil { + // Use SimNet as default network. + o.Net = &chaincfg.SimNetParams + } + + var extraArgs []string // Add a flag for the appropriate network type based on the provided // chain params. - switch activeNet.Net { + switch o.Net.Net { case wire.MainNet: // No extra flags since mainnet is the default case wire.TestNet3: @@ -161,28 +192,34 @@ func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, return nil, err } - nodeTestData, err := os.MkdirTemp(testDir, "rpc-node") + testNodeDir, err := os.MkdirTemp(testDir, "rpc-node") if err != nil { return nil, err } - certFile := filepath.Join(nodeTestData, "rpc.cert") - keyFile := filepath.Join(nodeTestData, "rpc.key") + certFile := filepath.Join(testNodeDir, "rpc.cert") + keyFile := filepath.Join(testNodeDir, "rpc.key") if err := genCertPair(certFile, keyFile); err != nil { return nil, err } - wallet, err := newMemWallet(activeNet, uint32(numTestInstances)) + wallet, err := newMemWallet(o.Net, uint32(numTestInstances)) if err != nil { return nil, err } - miningAddr := fmt.Sprintf("--miningaddr=%s", wallet.coinbaseAddr) extraArgs = append(extraArgs, miningAddr) - config, err := newConfig( - nodeTestData, certFile, keyFile, extraArgs, customExePath, - ) + var debugClient *debugstream.Client + if o.DebugHandler != nil { + p := NextAvailablePort() + streamAddr := fmt.Sprintf("127.0.0.1:%d", p) + debugClient = debugstream.NewClient(streamAddr, o.DebugHandler) + extraArgs = append(extraArgs, "--debugstream="+streamAddr) + } + + config, err := newConfig(testNodeDir, certFile, keyFile, extraArgs, + o.CustomExePath) if err != nil { return nil, err } @@ -190,8 +227,8 @@ func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, // Generate p2p+rpc listening addresses. config.listen, config.rpcListen = ListenAddressGenerator() - // Create the testing node bounded to the simnet. - node, err := newNode(config, nodeTestData) + // Create the testing node. + node, err := newNode(config, testNodeDir) if err != nil { return nil, err } @@ -199,43 +236,48 @@ func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, nodeNum := numTestInstances numTestInstances++ - if handlers == nil { - handlers = &rpcclient.NotificationHandlers{} + if o.Handlers == nil { + o.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 o.Handlers.OnFilteredBlockConnected != nil { + obc := o.Handlers.OnFilteredBlockConnected + o.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 + o.Handlers.OnFilteredBlockConnected = wallet.IngestBlock } - if handlers.OnFilteredBlockDisconnected != nil { - obd := handlers.OnFilteredBlockDisconnected - handlers.OnFilteredBlockDisconnected = func(height int32, header *wire.BlockHeader) { + if o.Handlers.OnFilteredBlockDisconnected != nil { + obd := o.Handlers.OnFilteredBlockDisconnected + o.Handlers.OnFilteredBlockDisconnected = func(height int32, + header *wire.BlockHeader) { + wallet.UnwindBlock(height, header) obd(height, header) } } else { - handlers.OnFilteredBlockDisconnected = wallet.UnwindBlock + o.Handlers.OnFilteredBlockDisconnected = wallet.UnwindBlock } h := &Harness{ - handlers: handlers, + handlers: o.Handlers, node: node, MaxConnRetries: DefaultMaxConnectionRetries, ConnectionRetryTimeout: DefaultConnectionRetryTimeout, - testNodeDir: nodeTestData, - ActiveNet: activeNet, + testNodeDir: testNodeDir, + ActiveNet: o.Net, nodeNum: nodeNum, wallet: wallet, + debugClient: debugClient, } // Track this newly created test instance within the package level @@ -245,21 +287,63 @@ func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, return h, nil } +// SOpts are options that can be passed to [Harness.SetUp], otherwise default +// configuration is used. +type SOpts struct { + // UTXOCount is the count of mature outputs to be mined in a generated + // chain and added to the memwallet. + UTXOCount uint32 + + // Args can be used to pass extra arguments to the btcd instance. + Args []string + + // NoRPCAndWallet can be set to skip initialization of RPC client + // and mem wallet. Note that this option also disables the generation + // of coins via the UTXOCount count. + NoRPCAndWallet bool + + // NoWalletWait can be set to avoid mem wallet sync blocking on + // [Harness.SetUp]. + NoWalletWait bool +} + // 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. +// btcd node, and depending on the harness configuration: starting the debug +// stream client, creating a websockets client, connecting it to the started +// node, and generating and submitting a testchain with the configured number +// of mature 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 { +// To avoid the need of passing an empty or nil [SOpts] to get default +// initialization, opts is variadic, but only the first argument (if present) +// is used. +// +// NOTE: This method and [Harness.TearDown] should always be called from the +// same goroutine as they are not concurrent safe. +func (h *Harness) SetUp(opts ...SOpts) error { + var o SOpts + if len(opts) != 0 { + o = opts[0] + } + // Start the btcd node itself. This spawns a new process which will be - // managed - if err := h.node.start(); err != nil { + // managed. + if err := h.node.start(o.Args); err != nil { return fmt.Errorf("error starting node: %w", err) } + + if h.debugClient != nil { + if err := h.debugClient.Start(); err != nil { + return fmt.Errorf("error connecting debug client: %w", + err) + } + } + + if o.NoRPCAndWallet { + return nil + } if err := h.connectRPCClient(); err != nil { - return fmt.Errorf("error connecting RPC client: %w", err) + return fmt.Errorf("error connecting RPC client: %w", + err) } h.wallet.Start() @@ -279,15 +363,18 @@ 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 o.UTXOCount > 0 { coinbaseMaturity := uint32(h.ActiveNet.CoinbaseMaturity) - numToGenerate := coinbaseMaturity + numMatureOutputs + numToGenerate := coinbaseMaturity + o.UTXOCount _, err := h.Client.Generate(numToGenerate) if err != nil { return err } } + if o.NoWalletWait { + return nil + } // Block until the wallet has fully synced up to the tip of the main // chain. _, height, err := h.Client.GetBestBlock() @@ -295,7 +382,10 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error { return err } ticker := time.NewTicker(time.Millisecond * 100) - for range ticker.C { + for i := 0; ; i++ { + if i > 0 { + <-ticker.C + } walletHeight := h.wallet.SyncedHeight() if walletHeight == height { break @@ -306,11 +396,33 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error { return nil } -// tearDown stops the running rpc test instance. All created processes are -// killed, and temporary directories removed. +// TOpts are options that can be passed to [Harness.TearDown], otherwise default +// configuration is used. +type TOpts struct { + // NoNodeCleanup skips cleaning up node data, which enable tests that + // need to restart the node. + NoNodeCleanup bool + + // NoShutdownSignal waits for node shutdown without sending a stop + // signal when stopping the node, which is useful for when we are + // testing features that stops the node. + NoShutdownSignal bool +} + +// tearDown stops the running rpc test instance. By default all created +// processes are killed, and temporary directories removed. +// +// To avoid the need of passing an empty or nil [TOpts] to get default +// initialization, opts is variadic, but only the first argument (if present) +// is used. // // This function MUST be called with the harness state mutex held (for writes). -func (h *Harness) tearDown() error { +func (h *Harness) tearDown(opts ...TOpts) error { + var o TOpts + if len(opts) != 0 { + o = opts[0] + } + if h.Client != nil { h.Client.Shutdown() h.Client.WaitForShutdown() @@ -321,29 +433,45 @@ func (h *Harness) tearDown() error { h.BatchClient.WaitForShutdown() } - if err := h.node.shutdown(); err != nil { - return err + if h.debugClient != nil { + // Is better to stop the client after stopping btcd, because + // this way we can use the debugHandler to test the btcd + // shutdown behavior. + defer h.debugClient.Stop() } - if err := os.RemoveAll(h.testNodeDir); err != nil { - return err + if h.wallet != nil { + h.wallet.Stop() } - delete(testInstances, h.testNodeDir) + err := h.node.shutdown(o.NoShutdownSignal) - return nil + if !o.NoNodeCleanup { + rmErr := os.RemoveAll(h.testNodeDir) + if rmErr != nil { + err = errors.Join(rmErr, err) + } + + delete(testInstances, h.testNodeDir) + } + + return err } -// TearDown stops the running rpc test instance. All created processes are -// killed, and temporary directories removed. +// TearDown stops the running rpc test instance. By default 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 { +// To avoid the need of passing an empty or nil [TOpts] to get default +// initialization, opts is variadic, but only the first argument (if present) +// is used. +// +// NOTE: This method and [Harness.SetUp] should always be called from the same +// goroutine as they are not concurrent safe. +func (h *Harness) TearDown(opts ...TOpts) 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 3fa8da2ba1..a5c5ccd09e 100644 --- a/integration/rpctest/rpc_harness_test.go +++ b/integration/rpctest/rpc_harness_test.go @@ -11,14 +11,15 @@ package rpctest import ( "fmt" "os" + "runtime" "testing" "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" + "github.com/stretchr/testify/require" ) func testSendOutputs(r *Harness, t *testing.T) { @@ -106,11 +107,11 @@ 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() if err != nil { t.Fatal(err) } - if err := harness.SetUp(false, 0); err != nil { + if err := harness.SetUp(); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } defer harness.TearDown() @@ -154,7 +155,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() if err != nil { t.Fatal(err) } @@ -182,11 +183,11 @@ 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() if err != nil { t.Fatal(err) } - if err := harness.SetUp(false, 0); err != nil { + if err := harness.SetUp(); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } defer harness.TearDown() @@ -282,11 +283,11 @@ 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() if err != nil { t.Fatal(err) } - if err := harness.SetUp(false, 0); err != nil { + if err := harness.SetUp(); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } defer harness.TearDown() @@ -470,11 +471,11 @@ 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() if err != nil { t.Fatal(err) } - if err := harness.SetUp(true, 5); err != nil { + if err := harness.SetUp(SOpts{UTXOCount: 5}); err != nil { t.Fatalf("unable to complete rpctest setup: %v", err) } defer harness.TearDown() @@ -547,6 +548,77 @@ func testMemWalletLockedOutputs(r *Harness, t *testing.T) { } } +func testSkipCleanup(_ *Harness, t *testing.T) { + h, err := New() + if err != nil { + t.Fatal(err) + } + if err := h.SetUp(SOpts{UTXOCount: 1}); err != nil { + t.Fatal(err) + } + defer h.TearDown() + count, err := h.Client.GetBlockCount() + if err != nil { + t.Fatal(err) + } + if count != 101 { + t.Fatalf("unexpected blockcount %d", count) + } + + // Verify that the state is kept between restarts. + for range 3 { + if err := h.TearDown(TOpts{NoNodeCleanup: true}); err != nil { + + t.Fatal(err) + } + if err := h.SetUp(); err != nil { + t.Fatal(err) + } + } + count, err = h.Client.GetBlockCount() + if err != nil { + t.Fatal(err) + } + if count != 101 { + t.Fatalf("unexpected blockcount %d", count) + } +} + +func testTearDownReturnsErrorWhenExitCodeIsNotZero(_ *Harness, t *testing.T) { + h, err := New() + if err != nil { + t.Fatal(err) + } + + // The invalid flag should make the startup of the node fail, the + // failure is asserted on teardown. + err = h.SetUp(SOpts{ + Args: []string{"--invalidflag=0"}, + NoRPCAndWallet: true, + }) + if err != nil { + t.Fatal(err) + } + + // We expect the process to exit with non zero status code. + if err := h.TearDown(); err == nil { + t.Fatal(err) + } +} + +func testNoGoroutineLeak(_ *Harness, t *testing.T) { + gStart := runtime.NumGoroutine() + defer func() { + time.Sleep(time.Millisecond * 50) + require.Equal(t, int(0), runtime.NumGoroutine()-gStart) + }() + + h, err := New() + require.NoError(t, err) + require.NoError(t, h.SetUp(SOpts{UTXOCount: 1})) + require.NoError(t, h.TearDown()) +} + var harnessTestCases = []HarnessTestCase{ testSendOutputs, testConnectNode, @@ -557,6 +629,9 @@ var harnessTestCases = []HarnessTestCase{ testGenerateAndSubmitBlockWithCustomCoinbaseOutputs, testMemWalletReorg, testMemWalletLockedOutputs, + testSkipCleanup, + testTearDownReturnsErrorWhenExitCodeIsNotZero, + testNoGoroutineLeak, } var mainHarness *Harness @@ -567,7 +642,7 @@ const ( func TestMain(m *testing.M) { var err error - mainHarness, err = New(&chaincfg.SimNetParams, nil, nil, "") + mainHarness, err = New() if err != nil { fmt.Println("unable to create main harness: ", err) os.Exit(1) @@ -576,7 +651,8 @@ 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 { + err = mainHarness.SetUp(SOpts{UTXOCount: numMatureOutputs}) + if 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 68cd5ba6a6..03952f9f5e 100644 --- a/integration/sync_race_test.go +++ b/integration/sync_race_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcd/integration/rpctest" "github.com/btcsuite/btcd/wire/v2" "github.com/stretchr/testify/require" @@ -99,9 +98,9 @@ func fakePeerConn(nodeAddr string) error { // sync, it was stuck with a dead sync peer (the sync manager still // has a dead peer as sync peer, so it ignores the new live one). func TestSyncManagerRaceCorruption(t *testing.T) { - stressedHarness, err := rpctest.New(&chaincfg.SimNetParams, nil, nil, "") + stressedHarness, err := rpctest.New() require.NoError(t, err) - require.NoError(t, stressedHarness.SetUp(true, 0)) + require.NoError(t, stressedHarness.SetUp()) t.Cleanup(func() { require.NoError(t, stressedHarness.TearDown()) }) @@ -128,9 +127,9 @@ 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() require.NoError(t, err) - require.NoError(t, newHarness.SetUp(true, 0)) + require.NoError(t, newHarness.SetUp()) defer func() { _ = newHarness.TearDown() }() require.NoError(t, rpctest.ConnectNode(stressedHarness, newHarness), @@ -204,9 +203,9 @@ func dialAndSendVersion( // 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() require.NoError(t, err) - require.NoError(t, harness.SetUp(true, 0)) + require.NoError(t, harness.SetUp()) t.Cleanup(func() { _ = harness.TearDown() }) nodeAddr := harness.P2PAddress() @@ -226,9 +225,9 @@ 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() require.NoError(t, err) - require.NoError(t, helper.SetUp(true, 0)) + require.NoError(t, helper.SetUp()) defer func() { _ = helper.TearDown() }() require.NoError(t, rpctest.ConnectNode(harness, helper)) diff --git a/log.go b/log.go index 92a38eb01b..f3021d8aab 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/mempool" "github.com/btcsuite/btcd/mining" "github.com/btcsuite/btcd/mining/cpuminer" @@ -61,6 +62,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") @@ -79,6 +82,7 @@ func init() { connmgr.UseLogger(cmgrLog) database.UseLogger(bcdbLog) blockchain.UseLogger(chanLog) + debugstream.UseLoggers(debsLog, debcLog) indexers.UseLogger(indxLog) mining.UseLogger(minrLog) cpuminer.UseLogger(minrLog) @@ -97,6 +101,8 @@ var subsystemLoggers = map[string]btclog.Logger{ "BCDB": bcdbLog, "BTCD": btcdLog, "CHAN": chanLog, + "DEBS": debsLog, + "DEBC": debcLog, "DISC": discLog, "INDX": indxLog, "MINR": minrLog,