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,