Skip to content
Open
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
114 changes: 92 additions & 22 deletions chainnotifier_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ type NotifierOptions struct {
// the passed in context to cancel being notified once the required
// number of confirmations have been reached.
ReOrgChan chan struct{}

// ReOrgDepthChan if set, will be sent the depth of a re-org whenever
// the transaction is re-organized out of the chain. It carries the same
// signal as ReOrgChan but with the re-org depth attached. Only
// confirmation notifications populate a depth; spend notifications send
// 0. Like ReOrgChan, setting this keeps the notification listener alive
// past the first event.
ReOrgDepthChan chan int32

// DoneChan if set, will be sent on once the notification is no longer
// under the risk of being re-organized out of the chain, i.e. it has
// reached the backend's re-org safety depth. Setting this keeps the
// notification listener alive past the first event so the terminal Done
// signal can be delivered.
DoneChan chan struct{}
}

// DefaultNotifierOptions returns the set of default options for the notifier.
Expand Down Expand Up @@ -58,6 +73,27 @@ func WithReOrgChan(reOrgChan chan struct{}) NotifierOption {
}
}

// WithReOrgDepthChan configures a channel that will be sent the depth of a
// re-org whenever the transaction/spend is re-organized out of the chain. Like
// WithReOrgChan, setting this keeps the notification listener alive past the
// first event, so the caller must cancel the passed in context to stop being
// notified. Only confirmation notifications populate a non-zero depth.
func WithReOrgDepthChan(reOrgDepthChan chan int32) NotifierOption {
return func(o *NotifierOptions) {
o.ReOrgDepthChan = reOrgDepthChan
}
}

// WithDoneChan configures a channel that will be sent on once the notification
// has reached the backend's re-org safety depth and is therefore complete.
// Setting this keeps the notification listener alive past the first event so
// the terminal Done signal can be delivered.
func WithDoneChan(doneChan chan struct{}) NotifierOption {
return func(o *NotifierOptions) {
o.DoneChan = doneChan
}
}

// ChainNotifierClient exposes base lightning functionality.
type ChainNotifierClient interface {
ServiceClient[chainrpc.ChainNotifierClient]
Expand Down Expand Up @@ -178,16 +214,15 @@ func (s *chainNotifierClient) RegisterSpendNtfn(ctx context.Context,
}
}

processReorg := func() {
if opts.ReOrgChan == nil {
return
}
// reorgAware reports whether the caller asked to keep the listener
// alive past the first event to observe re-orgs or the terminal Done
// signal.
reorgAware := opts.ReOrgChan != nil || opts.ReOrgDepthChan != nil ||
opts.DoneChan != nil

select {
case opts.ReOrgChan <- struct{}{}:
case <-ctx.Done():
return
}
processReorg := func(depth int32) {
deliver(ctx, opts.ReOrgChan, struct{}{})
deliver(ctx, opts.ReOrgDepthChan, depth)
}

s.wg.Add(1)
Expand All @@ -213,12 +248,21 @@ func (s *chainNotifierClient) RegisterSpendNtfn(ctx context.Context,
// we don't return here, since we might want to
// be informed about the new block we got
// confirmed in after a re-org.
if opts.ReOrgChan == nil {
if !reorgAware {
return
}

case *chainrpc.SpendEvent_Reorg:
processReorg()
// The spend notifier does not track re-org
// depth, so a depth of 0 is forwarded.
processReorg(0)

// The spend has reached the backend's re-org safety
// depth: deliver the terminal Done signal and stop.
case *chainrpc.SpendEvent_Done:
deliver(ctx, opts.DoneChan, struct{}{})

return

// Nil event, should never happen.
case nil:
Expand All @@ -237,6 +281,21 @@ func (s *chainNotifierClient) RegisterSpendNtfn(ctx context.Context,
return spendChan, errChan, nil
}

// deliver performs a non-blocking-until-ctx send of v on ch, if ch is
// non-nil. It is used to fan re-org/depth/done signals out to the optional
// caller-supplied channels without blocking the notifier read loop beyond the
// lifetime of the registration context.
func deliver[T any](ctx context.Context, ch chan T, v T) {
if ch == nil {
return
}

select {
case ch <- v:
case <-ctx.Done():
}
}

func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context,
txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32,
optFuncs ...NotifierOption) (chan *chainntnfs.TxConfirmation,
Expand Down Expand Up @@ -267,6 +326,12 @@ func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context,
confChan := make(chan *chainntnfs.TxConfirmation, 1)
errChan := make(chan error, 1)

// reorgAware reports whether the caller asked to keep the listener
// alive past the first confirmation to observe re-orgs or the terminal
// Done signal.
reorgAware := opts.ReOrgChan != nil || opts.ReOrgDepthChan != nil ||
opts.DoneChan != nil

s.wg.Add(1)
go func() {
defer s.wg.Done()
Expand Down Expand Up @@ -325,23 +390,28 @@ func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context,
// we don't return here, since we might want to
// be informed about the new block we got
// confirmed in after a re-org.
if opts.ReOrgChan == nil {
if !reorgAware {
return
}

// On a re-org, we just need to signal, we don't have
// any additional information. But we only signal if the
// caller requested to be notified about re-orgs.
// On a re-org, we signal and forward the re-org depth,
// but only if the caller requested to be notified.
case *chainrpc.ConfEvent_Reorg:
if opts.ReOrgChan != nil {
select {
case opts.ReOrgChan <- struct{}{}:
case <-ctx.Done():
return
}
}
deliver(ctx, opts.ReOrgChan, struct{}{})
deliver(
ctx, opts.ReOrgDepthChan,
int32(c.Reorg.Depth),
)
continue

// The confirmation has reached the backend's re-org
// safety depth: deliver the terminal Done signal and
// stop.
case *chainrpc.ConfEvent_Done:
deliver(ctx, opts.DoneChan, struct{}{})

return

// Nil event, should never happen.
case nil:
errChan <- fmt.Errorf("conf event empty")
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,5 @@ require (
replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display

go 1.25.11

replace github.com/lightningnetwork/lnd => github.com/ellemouton/lnd v0.8.0-beta-rc3.0.20260701213016-8b4da077a431
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ellemouton/lnd v0.8.0-beta-rc3.0.20260701213016-8b4da077a431 h1:KOLxRQYXb++mDgkqXhudfh5M2rZ/GG1USFsyb44LBSU=
github.com/ellemouton/lnd v0.8.0-beta-rc3.0.20260701213016-8b4da077a431/go.mod h1:LWfQaNNXlZghUJqIIh+JsgfxiVtC+ecZkrIe5bm9o4U=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
Expand Down Expand Up @@ -279,8 +281,6 @@ github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display h1:Y2WiPkBS
github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
github.com/lightningnetwork/lightning-onion v1.4.0 h1:qWE1icOH4AKXRcq1KCzt6P/TesqptgBTP++V7wowTc0=
github.com/lightningnetwork/lightning-onion v1.4.0/go.mod h1:YDPkvVTVQ6FBBE6Yj93tDd7zA3iTSrryi9xq46i7bKE=
github.com/lightningnetwork/lnd v0.21.0-beta.rc2.0.20260630214209-40c64f9db30d h1:mDcB9mwuwJXRbDmxps6mBgGs318Iz5ZbOrt0ieEx0os=
github.com/lightningnetwork/lnd v0.21.0-beta.rc2.0.20260630214209-40c64f9db30d/go.mod h1:LWfQaNNXlZghUJqIIh+JsgfxiVtC+ecZkrIe5bm9o4U=
github.com/lightningnetwork/lnd/actor v0.0.6 h1:Ge8N2wivARG+27qJBwTlB0vwsypStZYZy8vk4Zl38sU=
github.com/lightningnetwork/lnd/actor v0.0.6/go.mod h1:YAsoniSbY/cAM9HTVNfZLvt7RI6swDxy6wzPspTcMZg=
github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0=
Expand Down
Loading