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/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() +} diff --git a/integration/bip0009_test.go b/integration/bip0009_test.go index 28801beff9..9f01ba9b92 100644 --- a/integration/bip0009_test.go +++ b/integration/bip0009_test.go @@ -130,14 +130,15 @@ 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) } - 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() + defer r.TearDown(nil) // Short-circuit deployments that are configured as always active. if deploymentID < uint32(len(r.ActiveNet.Deployments)) { @@ -383,14 +384,14 @@ 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) } - 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() + 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 cfcd07cdcc..e354eb0804 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,14 +25,17 @@ 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. - require.NoError(t, r.SetUp(true, 100)) + sOpts := &rpctest.SetUpOpts{NumMatureOutputs: 100} + 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 656f32cbc4..372b8b0c2a 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,15 +110,16 @@ 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) } - if err := r.SetUp(true, 1); err != nil { + sOpts := &rpctest.SetUpOpts{NumMatureOutputs: 1} + 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 @@ -408,15 +408,16 @@ 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) } - if err := r.SetUp(true, 1); err != nil { + sOpts := &rpctest.SetUpOpts{NumMatureOutputs: 1} + 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 45750dd4d5..64f91d5fea 100644 --- a/integration/getchaintips_test.go +++ b/integration/getchaintips_test.go @@ -145,14 +145,15 @@ 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) } - 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() + 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 88d836eb89..a3315b9189 100644 --- a/integration/invalidate_reconsider_block_test.go +++ b/integration/invalidate_reconsider_block_test.go @@ -9,16 +9,17 @@ 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) } - 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) } - defer r.TearDown() + defer r.TearDown(nil) // Generate 4 blocks. // diff --git a/integration/p2a_test.go b/integration/p2a_test.go index e986db23d0..71579c7137 100644 --- a/integration/p2a_test.go +++ b/integration/p2a_test.go @@ -29,18 +29,17 @@ 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) } - defer harness.TearDown() + defer harness.TearDown(nil) // Initialize the test harness with mining enabled to confirm // transactions. - err = harness.SetUp(true, 25) + sOpts := &rpctest.SetUpOpts{NumMatureOutputs: 25} + err = harness.SetUp(sOpts) if err != nil { t.Fatalf("unable to setup test harness: %v", err) } @@ -197,4 +196,3 @@ func TestPayToAnchorSimple(t *testing.T) { } }) } - diff --git a/integration/prune_test.go b/integration/prune_test.go index ef69916fd6..6a67be1502 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,14 +19,14 @@ 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 { + 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 a211d7d2d8..c53eb938a6 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,14 +26,17 @@ 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. - require.NoError(t, r.SetUp(true, 100)) + sOpts := &rpctest.SetUpOpts{NumMatureOutputs: 100} + 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 bfdbe95c20..34b731eaa1 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,15 +32,15 @@ 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()) }) + require.NoError(t, longer.SetUp(nil)) + t.Cleanup(func() { require.NoError(t, longer.TearDown(nil)) }) - 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()) }) + require.NoError(t, shorter.SetUp(nil)) + 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 0649644682..e36b701e45 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) @@ -319,7 +316,8 @@ 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{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 @@ -327,7 +325,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/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..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/node.go b/integration/rpctest/node.go index b397bb00bc..7d2c4a29fd 100644 --- a/integration/rpctest/node.go +++ b/integration/rpctest/node.go @@ -5,6 +5,7 @@ package rpctest import ( + "errors" "fmt" "log" "os" @@ -208,18 +209,29 @@ 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 return nil } - defer n.cmd.Wait() - if runtime.GOOS == "windows" { - return n.cmd.Process.Signal(os.Kill) + + var signalErr error + switch { + case signal && runtime.GOOS == "windows": + signalErr = n.cmd.Process.Signal(os.Kill) + + case signal: + 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 @@ -241,14 +253,15 @@ 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 { - return err - } - if err := n.cleanup(); err != nil { - return err - } - return 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 { + stopErr := n.stop(signal) + cleanupErr := n.cleanup() + + return errors.Join(stopErr, cleanupErr) } // genCertPair generates a key/cert pair to the paths provided. diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index 9c9cb85262..1de0b5dac3 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" ) @@ -116,41 +118,70 @@ type Harness struct { Client *rpcclient.Client BatchClient *rpcclient.Client + nodeConfig *nodeConfig node *node handlers *rpcclient.NotificationHandlers wallet *memWallet + debugClient *debugstream.Client + testNodeDir string nodeNum int 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 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 + // 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 + + // 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. // // 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 *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 +203,24 @@ 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, - ) + 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 { return nil, err } @@ -190,52 +228,51 @@ 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) - if err != nil { - return nil, err - } - 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, - node: node, + handlers: opts.Handlers, + nodeConfig: config, MaxConnRetries: DefaultMaxConnectionRetries, ConnectionRetryTimeout: DefaultConnectionRetryTimeout, testNodeDir: nodeTestData, - ActiveNet: activeNet, + ActiveNet: opts.Params, nodeNum: nodeNum, wallet: wallet, + debugClient: debugClient, } // Track this newly created test instance within the package level @@ -245,6 +282,28 @@ func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, return h, nil } +// SetUpOpts are options that can be passed to SetUp when starting the harness +// instance. +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 + + // 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 // simnet node, creating a websockets client and connecting to the started // node, and finally: optionally generating and submitting a testchain with a @@ -252,12 +311,38 @@ func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, // // 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 { - // Start the btcd node itself. This spawns a new process which will be - // managed +func (h *Harness) SetUp(opts *SetUpOpts) error { + var err error + if opts == nil { + opts = &SetUpOpts{} + } + + // 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 h.debugClient != nil { + err := h.debugClient.Start() + if err != nil { + return err + } + } + + if opts.NoRPCClientAndWallet { + return nil + } + if err := h.connectRPCClient(); err != nil { return fmt.Errorf("error connecting RPC client: %w", err) } @@ -279,15 +364,19 @@ 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.NumMatureOutputs > 0 { coinbaseMaturity := uint32(h.ActiveNet.CoinbaseMaturity) - numToGenerate := coinbaseMaturity + numMatureOutputs + numToGenerate := coinbaseMaturity + opts.NumMatureOutputs _, err := h.Client.Generate(numToGenerate) if err != nil { return err } } + 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() @@ -306,11 +395,27 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) 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() @@ -321,17 +426,34 @@ func (h *Harness) tearDown() error { h.BatchClient.WaitForShutdown() } - if err := h.node.shutdown(); err != nil { - return err + if h.wallet != nil { + h.wallet.Stop() } - if err := os.RemoveAll(h.testNodeDir); 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 + } + + // 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 + // 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) } // TearDown stops the running rpc test instance. All created processes are @@ -339,11 +461,11 @@ func (h *Harness) tearDown() error { // // 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) 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 3fa8da2ba1..146f5073c8 100644 --- a/integration/rpctest/rpc_harness_test.go +++ b/integration/rpctest/rpc_harness_test.go @@ -9,16 +9,20 @@ package rpctest import ( + "errors" "fmt" "os" + "os/exec" + "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/debugstream" "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,14 +110,14 @@ 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) } - 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() + defer harness.TearDown(nil) // Establish a p2p connection from our new local harness to the main // harness. @@ -154,11 +158,11 @@ 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) } - defer harness1.TearDown() + defer harness1.TearDown(nil) // With the harness created above, a single harness should be detected // as active. @@ -182,14 +186,14 @@ 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) } - 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() + defer harness.TearDown(nil) nodeSlice := []*Harness{r, harness} @@ -282,14 +286,14 @@ 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) } - 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() + defer harness.TearDown(nil) nodeSlice := []*Harness{r, harness} blocksSynced := make(chan struct{}) @@ -470,14 +474,15 @@ 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) } - if err := harness.SetUp(true, 5); err != nil { + sOpts := &SetUpOpts{NumMatureOutputs: 5} + 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) @@ -547,16 +552,146 @@ func testMemWalletLockedOutputs(r *Harness, t *testing.T) { } } -var harnessTestCases = []HarnessTestCase{ - testSendOutputs, - testConnectNode, - testActiveHarnesses, - testJoinBlocks, - testJoinMempools, // Depends on results of testJoinBlocks - testGenerateAndSubmitBlock, - testGenerateAndSubmitBlockWithCustomCoinbaseOutputs, - testMemWalletReorg, - testMemWalletLockedOutputs, +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{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)) +} + +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)) +} + +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 = []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 @@ -567,7 +702,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) @@ -576,7 +711,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 { + sOpts := &SetUpOpts{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 @@ -584,7 +720,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) } @@ -604,7 +740,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", @@ -617,14 +754,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) 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 } } diff --git a/integration/sync_race_test.go b/integration/sync_race_test.go index b678fa1809..476ecdf027 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,13 +298,12 @@ 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)) + require.NoError(t, stressedHarness.SetUp(nil)) t.Cleanup(func() { - require.NoError(t, stressedHarness.TearDown()) + require.NoError(t, stressedHarness.TearDown(nil)) }) nodeAddr := stressedHarness.P2PAddress() @@ -326,10 +324,10 @@ 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() }() + require.NoError(t, newHarness.SetUp(nil)) + defer func() { _ = newHarness.TearDown(nil) }() require.NoError(t, rpctest.ConnectNode(stressedHarness, newHarness), "stressed node must connect to the new node") @@ -420,10 +418,10 @@ 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() }) + require.NoError(t, harness.SetUp(nil)) + t.Cleanup(func() { _ = harness.TearDown(nil) }) nodeAddr := harness.P2PAddress() @@ -463,10 +461,10 @@ 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() }() + require.NoError(t, helper.SetUp(nil)) + defer func() { _ = helper.TearDown(nil) }() require.NoError(t, rpctest.ConnectNode(harness, helper)) 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,