Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 134 additions & 37 deletions btcwbackend/chain_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,13 @@ type ChainBackend struct {
neutrinoCS *neutrino.ChainService

// notifier provides chain notification services backed by
// neutrino's compact block filter scanning.
notifier *neutrinonotify.NeutrinoNotifier
// neutrino's compact block filter scanning. Stored under the
// chainntnfs interface (concrete type is
// *neutrinonotify.NeutrinoNotifier in production) so reorg-aware
// forwarder tests can substitute a stub that drives the full
// Confirmed / NegativeConf / Done lifecycle without spinning up a
// neutrino chain service.
notifier chainntnfs.ChainNotifier

// feeEstimator provides fee estimation from a web API since
// neutrino has no mempool visibility.
Expand Down Expand Up @@ -400,40 +405,89 @@ func (b *ChainBackend) RegisterConf(ctx context.Context, txid *chainhash.Hash,
cancelOnce.Do(event.Cancel)
}

// Create channels to convert neutrino's confirmation lifecycle
// (which already drives chainntnfs's NegativeConf/Done channels
// directly — neutrino emits these natively from its compact-
// block-filter scanner) into our backend-agnostic types.
// NegativeConf carries a reorg depth that the cross-backend
// chainsource layer intentionally drops, so we forward a bare
// struct{} signal on reorgChan instead.
confChan := make(chan *chainsource.TxConfirmation, 1)
reorgChan := make(chan uint64, 1)
doneChan := make(chan struct{}, 1)

go func() {
// Defers run in LIFO order. event.Cancel() must run first
// so the upstream notifier stops writing to its internal
// channels before we cancel notifyCtx (which any in-flight
// downstream sends are still using) and finally close the
// outgoing chans. Reversing this order would race the
// upstream notifier against closed channels.
defer close(confChan)
defer close(reorgChan)
defer close(doneChan)
defer cancel()
defer safeCancel()

select {
case lndConf, ok := <-event.Confirmed:
if !ok {
return
}
for {
select {
case lndConf, ok := <-event.Confirmed:
if !ok {
return
}

conf := &chainsource.TxConfirmation{
BlockHash: lndConf.BlockHash,
BlockHeight: lndConf.BlockHeight,
TxIndex: lndConf.TxIndex,
Tx: lndConf.Tx,
Block: lndConf.Block,
}
conf := &chainsource.TxConfirmation{
BlockHash: lndConf.BlockHash,
BlockHeight: lndConf.BlockHeight,
TxIndex: lndConf.TxIndex,
Tx: lndConf.Tx,
Block: lndConf.Block,
}

select {
case confChan <- conf:
case <-notifyCtx.Done():
return
}

case _, ok := <-event.NegativeConf:
if !ok {
event.NegativeConf = nil

continue
}

select {
case reorgChan <- uint64(0):
case <-notifyCtx.Done():
return
}

case _, ok := <-event.Done:
if !ok {
event.Done = nil

continue
}

select {
case doneChan <- struct{}{}:
case <-notifyCtx.Done():
return
}

return

select {
case confChan <- conf:
case <-notifyCtx.Done():
return
}

case <-notifyCtx.Done():
return
}
Comment on lines +432 to 484

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Mutating the fields of the event struct (e.g., event.NegativeConf = nil and event.Done = nil) directly is a code smell and a potential data race. The event struct is returned by an external package (chainntnfs), and mutating its fields concurrently from a background goroutine can race with other goroutines reading those fields (such as the notifier itself, or test assertions/mocks). Instead, use local variables for the channels within the select loop to safely nil them out and disable the select cases.

		confirmedChan := event.Confirmed
		negativeConfChan := event.NegativeConf
		doneEventChan := event.Done

		for {
			select {
			case lndConf, ok := <-confirmedChan:
				if !ok {
					return
				}

				conf := &chainsource.TxConfirmation{
					BlockHash:   lndConf.BlockHash,
					BlockHeight: lndConf.BlockHeight,
					TxIndex:     lndConf.TxIndex,
					Tx:          lndConf.Tx,
					Block:       lndConf.Block,
				}

				select {
				case confChan <- conf:
				case <-notifyCtx.Done():
					return
				}

			case _, ok := <-negativeConfChan:
				if !ok {
					negativeConfChan = nil

					continue
				}

				select {
				case reorgChan <- struct{}{}:
				case <-notifyCtx.Done():
					return
				}

			case _, ok := <-doneEventChan:
				if !ok {
					doneEventChan = nil

					continue
				}

				select {
				case doneChan <- struct{}{}:
				case <-notifyCtx.Done():
					return
				}

				return

			case <-notifyCtx.Done():
				return
			}
		}

}()
Comment on lines 419 to 485

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Modifying the fields of the shared event struct (event.NegativeConf = nil and event.Done = nil) inside the concurrent forwarding goroutine can lead to data races if the struct is accessed or inspected elsewhere.

Instead, copy the channels to local variables before the loop and nil those out to safely disable the select cases.

	go func() {
		// Defers run in LIFO order. event.Cancel() must run first
		// so the upstream notifier stops writing to its internal
		// channels before we cancel notifyCtx (which any in-flight
		// downstream sends are still using) and finally close the
		// outgoing chans. Reversing this order would race the
		// upstream notifier against closed channels.
		defer close(confChan)
		defer close(reorgChan)
		defer close(doneChan)
		defer cancel()
		defer safeCancel()

		upstreamConfirmed := event.Confirmed
		upstreamNegConf := event.NegativeConf
		upstreamDone := event.Done

		for {
			select {
			case lndConf, ok := <-upstreamConfirmed:
				if !ok {
					return
				}

				conf := &chainsource.TxConfirmation{
					BlockHash:   lndConf.BlockHash,
					BlockHeight: lndConf.BlockHeight,
					TxIndex:     lndConf.TxIndex,
					Tx:          lndConf.Tx,
					Block:       lndConf.Block,
				}

				select {
				case confChan <- conf:
				case <-notifyCtx.Done():
					return
				}

			case _, ok := <-upstreamNegConf:
				if !ok {
					upstreamNegConf = nil

					continue
				}

				select {
				case reorgChan <- struct{}{}:
				case <-notifyCtx.Done():
					return
				}

			case _, ok := <-upstreamDone:
				if !ok {
					upstreamDone = nil

					continue
				}

				select {
				case doneChan <- struct{}{}:
				case <-notifyCtx.Done():
					return
				}

				return

			case <-notifyCtx.Done():
				return
			}
		}
	}()


return &chainsource.ConfRegistration{
Confirmed: confChan,
Reorged: reorgChan,
Done: doneChan,
Cancel: func() {
cancel()
safeCancel()
Expand Down Expand Up @@ -470,40 +524,83 @@ func (b *ChainBackend) RegisterSpend(ctx context.Context,
cancelOnce.Do(event.Cancel)
}

// Create channels to convert neutrino's spend lifecycle into our
// backend-agnostic types. neutrino's Reorg carries no payload, so
// we forward a bare struct{} signal.
spendChan := make(chan *chainsource.SpendDetail, 1)
reorgChan := make(chan uint64, 1)
doneChan := make(chan struct{}, 1)

go func() {
// LIFO defer: event.Cancel() first so the upstream notifier
// stops writing, then cancel notifyCtx so in-flight downstream
// sends unblock, and finally close the outgoing chans.
defer close(spendChan)
defer close(reorgChan)
defer close(doneChan)
defer cancel()
defer safeCancel()

select {
case lndSpend, ok := <-event.Spend:
if !ok {
return
}
for {
select {
case lndSpend, ok := <-event.Spend:
if !ok {
return
}

spend := &chainsource.SpendDetail{
SpentOutPoint: lndSpend.SpentOutPoint,
SpenderTxHash: lndSpend.SpenderTxHash,
SpendingTx: lndSpend.SpendingTx,
SpenderInputIndex: lndSpend.SpenderInputIndex,
SpendingHeight: lndSpend.SpendingHeight,
}
spend := &chainsource.SpendDetail{
SpentOutPoint: lndSpend.SpentOutPoint,
SpenderTxHash: lndSpend.SpenderTxHash,
SpendingTx: lndSpend.SpendingTx,
SpenderInputIndex: lndSpend.
SpenderInputIndex,
SpendingHeight: lndSpend.SpendingHeight,
}

select {
case spendChan <- spend:
case <-notifyCtx.Done():
return
}

case _, ok := <-event.Reorg:
if !ok {
event.Reorg = nil

continue
}

select {
case reorgChan <- uint64(0):
case <-notifyCtx.Done():
return
}

case _, ok := <-event.Done:
if !ok {
event.Done = nil

continue
}

select {
case doneChan <- struct{}{}:
case <-notifyCtx.Done():
return
}

return

select {
case spendChan <- spend:
case <-notifyCtx.Done():
return
}

case <-notifyCtx.Done():
return
}
Comment on lines +544 to 597

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Mutating the fields of the event struct (e.g., event.Reorg = nil and event.Done = nil) directly is a code smell and a potential data race. The event struct is returned by an external package (chainntnfs), and mutating its fields concurrently from a background goroutine can race with other goroutines reading those fields (such as the notifier itself, or test assertions/mocks). Instead, use local variables for the channels within the select loop to safely nil them out and disable the select cases.

		spendEventChan := event.Spend
		reorgEventChan := event.Reorg
		doneEventChan := event.Done

		for {
			select {
			case lndSpend, ok := <-spendEventChan:
				if !ok {
					return
				}

				spend := &chainsource.SpendDetail{
					SpentOutPoint:     lndSpend.SpentOutPoint,
					SpenderTxHash:     lndSpend.SpenderTxHash,
					SpendingTx:        lndSpend.SpendingTx,
					SpenderInputIndex: lndSpend.SpenderInputIndex,
					SpendingHeight:    lndSpend.SpendingHeight,
				}

				select {
				case spendChan <- spend:
				case <-notifyCtx.Done():
					return
				}

			case _, ok := <-reorgEventChan:
				if !ok {
					reorgEventChan = nil

					continue
				}

				select {
				case reorgChan <- struct{}{}:
				case <-notifyCtx.Done():
					return
				}

			case _, ok := <-doneEventChan:
				if !ok {
					doneEventChan = nil

					continue
				}

				select {
				case doneChan <- struct{}{}:
				case <-notifyCtx.Done():
					return
				}

				return

			case <-notifyCtx.Done():
				return
			}
		}

}()
Comment on lines 534 to 598

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Modifying the fields of the shared event struct (event.Reorg = nil and event.Done = nil) inside the concurrent forwarding goroutine can lead to data races if the struct is accessed or inspected elsewhere.

Instead, copy the channels to local variables before the loop and nil those out to safely disable the select cases.

	go func() {
		// LIFO defer: event.Cancel() first so the upstream notifier
		// stops writing, then cancel notifyCtx so in-flight downstream
		// sends unblock, and finally close the outgoing chans.
		defer close(spendChan)
		defer close(reorgChan)
		defer close(doneChan)
		defer cancel()
		defer safeCancel()

		upstreamSpend := event.Spend
		upstreamReorg := event.Reorg
		upstreamDone := event.Done

		for {
			select {
			case lndSpend, ok := <-upstreamSpend:
				if !ok {
					return
				}

				spend := &chainsource.SpendDetail{
					SpentOutPoint:     lndSpend.SpentOutPoint,
					SpenderTxHash:     lndSpend.SpenderTxHash,
					SpendingTx:        lndSpend.SpendingTx,
					SpenderInputIndex: lndSpend.SpenderInputIndex,
					SpendingHeight:    lndSpend.SpendingHeight,
				}

				select {
				case spendChan <- spend:
				case <-notifyCtx.Done():
					return
				}

			case _, ok := <-upstreamReorg:
				if !ok {
					upstreamReorg = nil

					continue
				}

				select {
				case reorgChan <- struct{}{}:
				case <-notifyCtx.Done():
					return
				}

			case _, ok := <-upstreamDone:
				if !ok {
					upstreamDone = nil

					continue
				}

				select {
				case doneChan <- struct{}{}:
				case <-notifyCtx.Done():
					return
				}

				return

			case <-notifyCtx.Done():
				return
			}
		}
	}()


return &chainsource.SpendRegistration{
Spend: spendChan,
Spend: spendChan,
Reorged: reorgChan,
Done: doneChan,
Cancel: func() {
cancel()
safeCancel()
Expand Down
Loading
Loading