-
Notifications
You must be signed in to change notification settings - Fork 9
btcwbackend: reorg-aware chain notifier forwarding (C0b) #559
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
@@ -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
419
to
485
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Modifying the fields of the shared 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() | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mutating the fields of the 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Modifying the fields of the shared 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() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mutating the fields of the
eventstruct (e.g.,event.NegativeConf = nilandevent.Done = nil) directly is a code smell and a potential data race. Theeventstruct 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.