Skip to content

Commit

Permalink
chain: add unit tests
Browse files Browse the repository at this point in the history
Add TestNeutrinoClientSequentialStartStop to verify that the
neutrino client can have Start() and Stop() called sequentially.
Test case fails with -race flag enabled.

Add TestNeutrinoClientNotifyReceived to verify that calls to
NotifyReceived call the Update method of the rescan object.  Verify
that there is no race condition on any state variables of the
NeutrinoClient.  Pass test with -race flag enabled.

Add a unit test that demonstrates a segmentation fault exists when
concurrent calls to NotifyReceived, NotifyBlocks and Rescan methods of
the NeutrinoClient are executed.  The rescan property is set to nil
and a segmentation fault arises.
  • Loading branch information
MStreet3 committed Nov 26, 2022
1 parent d68e378 commit c053839
Show file tree
Hide file tree
Showing 2 changed files with 362 additions and 0 deletions.
171 changes: 171 additions & 0 deletions chain/mocks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package chain

import (
"container/list"
"errors"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/gcs"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/neutrino"
"github.com/lightninglabs/neutrino/banman"
"github.com/lightninglabs/neutrino/headerfs"
)

var (
ErrNotImplemented = errors.New("not implemented")
testBestBlock = &headerfs.BlockStamp{
Height: 42,
}
)

var (
_ rescanner = (*mockRescanner)(nil)
_ neutrinoChainService = (*mockChainService)(nil)
)

// newMockNeutrinoClient constructs a neutrino client with a mock chain
// service implementation and mock rescanner interface implementation.
func newMockNeutrinoClient() *NeutrinoClient {
// newRescanFunc returns a mockRescanner
newRescanFunc := func(ro ...neutrino.RescanOption) rescanner {
return &mockRescanner{
updateArgs: list.New(),
}
}

return &NeutrinoClient{
CS: &mockChainService{},
newRescan: newRescanFunc,
}
}

// mockRescanner is a mock implementation of a rescanner interface for use in
// tests. Only the Update method is implemented.
type mockRescanner struct {
updateArgs *list.List
}

func (m *mockRescanner) Start() <-chan error {
errs := make(chan error)
return errs
}

func (m *mockRescanner) WaitForShutdown() {
// no-op
}

func (m *mockRescanner) Update(opts ...neutrino.UpdateOption) error {
m.updateArgs.PushBack(opts)
return nil
}

// mockChainService is a mock implementation of a chain service for use in
// tests. Only the Start, GetBlockHeader and BestBlock methods are implemented.
type mockChainService struct {
bestBlock func() (*headerfs.BlockStamp, error)
}

func (m *mockChainService) Start() error {
return nil
}

func (m *mockChainService) BestBlock() (*headerfs.BlockStamp, error) {
impl := m.getBestBlock()
return impl()
}

func (m *mockChainService) GetBlockHeader(
*chainhash.Hash) (*wire.BlockHeader, error) {

return &wire.BlockHeader{}, nil
}

// getBestBlock returns a BestBlock implementation that defaults to simply
// returning the value of testBestBlock with no error.
func (m *mockChainService) getBestBlock() func() (*headerfs.BlockStamp, error) {
if m.bestBlock == nil {
m.bestBlock = func() (*headerfs.BlockStamp, error) {
return testBestBlock, nil
}
}
return m.bestBlock
}

func (m *mockChainService) GetBlock(chainhash.Hash,
...neutrino.QueryOption) (*btcutil.Block, error) {

return nil, ErrNotImplemented
}

func (m *mockChainService) GetBlockHeight(*chainhash.Hash) (int32, error) {
return 0, ErrNotImplemented
}

func (m *mockChainService) GetBlockHash(int64) (*chainhash.Hash, error) {
return nil, ErrNotImplemented
}

func (m *mockChainService) IsCurrent() bool {
return false
}

func (m *mockChainService) SendTransaction(*wire.MsgTx) error {
return ErrNotImplemented
}

func (m *mockChainService) GetCFilter(chainhash.Hash,
wire.FilterType, ...neutrino.QueryOption) (*gcs.Filter, error) {

return nil, ErrNotImplemented
}

func (m *mockChainService) GetUtxo(
_ ...neutrino.RescanOption) (*neutrino.SpendReport, error) {

return nil, ErrNotImplemented
}

func (m *mockChainService) BanPeer(string, banman.Reason) error {
return ErrNotImplemented
}

func (m *mockChainService) IsBanned(addr string) bool {
panic(ErrNotImplemented)
}

func (m *mockChainService) AddPeer(*neutrino.ServerPeer) {
panic(ErrNotImplemented)
}

func (m *mockChainService) AddBytesSent(uint64) {
panic(ErrNotImplemented)
}

func (m *mockChainService) AddBytesReceived(uint64) {
panic(ErrNotImplemented)
}

func (m *mockChainService) NetTotals() (uint64, uint64) {
panic(ErrNotImplemented)
}

func (m *mockChainService) UpdatePeerHeights(*chainhash.Hash,
int32, *neutrino.ServerPeer,
) {
panic(ErrNotImplemented)
}

func (m *mockChainService) ChainParams() chaincfg.Params {
panic(ErrNotImplemented)
}

func (m *mockChainService) Stop() error {
panic(ErrNotImplemented)
}

func (m *mockChainService) PeerByAddr(string) *neutrino.ServerPeer {
panic(ErrNotImplemented)
}
191 changes: 191 additions & 0 deletions chain/neutrino_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package chain

import (
"sync"
"testing"
"time"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/stretchr/testify/require"
)

// TestNeutrinoClientSequentialStartStop ensures that the client
// can sequentially Start and Stop without errors or races.
func TestNeutrinoClientSequentialStartStop(t *testing.T) {
var (
timeout = time.After(1 * time.Second)
nc = newMockNeutrinoClient()
wantRestarts = 50
)

// callStartStop starts the neutrino client, requires no error on
// startup, immediately stops the client and waits for shutdown.
// The returned channel is closed once shutdown is complete.
callStartStop := func() <-chan struct{} {
done := make(chan struct{})

go func() {
defer close(done)

err := nc.Start()
require.NoError(t, err)
nc.Stop()
nc.WaitForShutdown()
}()

return done
}

// For each wanted restart, execute callStartStop and wait until the
// call is done before continuing to the next execution. Waiting for
// a read from done forces all executions of callStartStop to be done
// sequentially.
//
// The test fails if all of the wanted restarts cannot be completed
// sequentially before the timeout is reached.
for i := 0; i < wantRestarts; i++ {
select {
case <-timeout:
t.Fatal("timed out")
case <-callStartStop():
}
}
}

// TestNeutrinoClientNotifyReceived verifies that a call to NotifyReceived sets
// the client into the scanning state and that subsequent calls while scanning
// will call Update on the client's Rescanner.
func TestNeutrinoClientNotifyReceived(t *testing.T) {
var (
timeout = time.After(1 * time.Second)
nc = newMockNeutrinoClient()
wantNotifyReceivedCalls = 50
wantUpdateCalls = wantNotifyReceivedCalls - 1
)

// executeCalls calls NotifyReceived() synchronously n times without
// blocking the test and requires no error after each call.
executeCalls := func(n int) <-chan struct{} {
var (
addrs []btcutil.Address
done = make(chan struct{})
)

go func() {
defer close(done)

for i := 0; i < n; i++ {
err := nc.NotifyReceived(addrs)
require.NoError(t, err)
}
}()

return done
}

// Wait for all calls to complete or test to time out.
select {
case <-timeout:
t.Fatal("timed out")
case <-executeCalls(wantNotifyReceivedCalls):
// Require that the expected number of calls to Update were made
// once done sending all NotifyReceived calls.
mockRescan := nc.rescan.(*mockRescanner)
gotUpdateCalls := mockRescan.updateArgs.Len()
require.Equal(t, wantUpdateCalls, gotUpdateCalls)
}
}

// TestNeutrinoClientNotifyReceivedRescan verifies concurrent calls to
// NotifyBlocks, NotifyReceived and Rescan do not result in a data race
// and that there is no panic on replacing the rescan goroutine single instance.
func TestNeutrinoClientNotifyReceivedRescan(t *testing.T) {
var (
addrs []btcutil.Address
nc = newMockNeutrinoClient()
timeout = time.After(1 * time.Second)
wantRoutines = 100
)

// Define closures to wrap desired neutrino client method calls.

// callRescan calls the Rescan() method and asserts it completes
// with no errors. Rescan() is called with the hash of an empty header
// on each call.
var (
emptyHeader = &wire.BlockHeader{}
startHash = emptyHeader.BlockHash()
)

callRescan := func(wg *sync.WaitGroup) {
defer wg.Done()

err := nc.Rescan(&startHash, addrs, nil)
require.NoError(t, err)
}

// callNotifyReceived calls the NotifyReceived() method and asserts it
// completes with no errors.
callNotifyReceived := func(wg *sync.WaitGroup) {
defer wg.Done()

err := nc.NotifyReceived(addrs)
require.NoError(t, err)
}

// callNotifyBlocks calls the NotifyBlocks() method and asserts it
// completes with no errors.
callNotifyBlocks := func(wg *sync.WaitGroup) {
defer wg.Done()

err := nc.NotifyBlocks()
require.NoError(t, err)
}

// executeCalls launches the wanted number of goroutines, waits
// for them to finish and signals all done by closing the returned
// channel.
executeCalls := func(n int) <-chan struct{} {
var (
done = make(chan struct{})
wg sync.WaitGroup
)

go func() {
defer close(done)
defer wg.Wait()

wg.Add(n)
for i := 0; i < n; i++ {
if i%3 == 0 {
wg.Add(1)
go callRescan(&wg)
continue
}

if i%10 == 0 {
wg.Add(1)
go callNotifyBlocks(&wg)
continue
}

wg.Add(1)
go callNotifyReceived(&wg)
}
}()

return done
}

// Start the client.
err := nc.Start()
require.NoError(t, err)

// Wait for all calls to complete or test to time out.
select {
case <-timeout:
t.Fatal("timed out")
case <-executeCalls(wantRoutines):
}
}

0 comments on commit c053839

Please sign in to comment.