Skip to content

Commit 795a527

Browse files
committed
protofsm: add daemon events for spend+conf registration
1 parent 95eb995 commit 795a527

File tree

3 files changed

+189
-1
lines changed

3 files changed

+189
-1
lines changed

protofsm/daemon_events.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package protofsm
22

33
import (
44
"github.com/btcsuite/btcd/btcec/v2"
5+
"github.com/btcsuite/btcd/chaincfg/chainhash"
56
"github.com/btcsuite/btcd/wire"
67
"github.com/lightningnetwork/lnd/fn"
78
"github.com/lightningnetwork/lnd/lnwire"
@@ -21,7 +22,8 @@ type DaemonEventSet []DaemonEvent
2122
// DaemonEvents is a special type constraint that enumerates all the possible
2223
// types of daemon events.
2324
type DaemonEvents interface {
24-
SendMsgEvent[any] | DisableChannelEvent | BroadcastTxn
25+
SendMsgEvent[any] | DisableChannelEvent | BroadcastTxn |
26+
RegisterSpend[any] | RegisterConf[any]
2527
}
2628

2729
// SendPredicate is a function that returns true if the target message should
@@ -76,3 +78,56 @@ type BroadcastTxn struct {
7678

7779
// daemonSealed indicates that this struct is a DaemonEvent instance.
7880
func (b *BroadcastTxn) daemonSealed() {}
81+
82+
// RegisterSpend is used to request that a certain event is sent into the state
83+
// machien once the specified outpoint has been spent.
84+
type RegisterSpend[Event any] struct {
85+
// OutPoint is the outpoint on chain to watch.
86+
OutPoint wire.OutPoint
87+
88+
// PkScript is the script that we expect to be spent along with the
89+
// outpoint.
90+
PkScript []byte
91+
92+
// HeightHint is a value used to give the chain scanner a hint on how
93+
// far back it needs to start its search.
94+
HeightHint uint32
95+
96+
// PostSpendEvent is an event that's sent back to the requester once a
97+
// transaction spending the outpoint has been confirmed in the main
98+
// chain.
99+
PostSpendEvent fn.Option[Event]
100+
}
101+
102+
// daemonSealed indicates that this struct is a DaemonEvent instance.
103+
func (r *RegisterSpend[E]) daemonSealed() {}
104+
105+
// RegisterConf is used to request that a certain event is sent into the state
106+
// machien once the specified outpoint has been spent.
107+
type RegisterConf[Event any] struct {
108+
// Txid is the txid of the txn we want to watch the chain for.
109+
Txid chainhash.Hash
110+
111+
// PkScript is the script that we expect to be created along with the
112+
// outpoint.
113+
PkScript []byte
114+
115+
// HeightHint is a value used to give the chain scanner a hint on how
116+
// far back it needs to start its search.
117+
HeightHint uint32
118+
119+
// NumConfs is the number of confirmations that the spending
120+
// transaction needs to dispatch an event.
121+
NumConfs fn.Option[uint32]
122+
123+
// PostConfEvent is an event that's sent back to the requester once the
124+
// transaction specified above has confirmed in the chain with
125+
// sufficient depth.
126+
//
127+
// TODO(roasbeef): will also need the confirming tx, block header, etc?
128+
// * need method on the event to bind the conf/spend details
129+
PostConfEvent fn.Option[Event]
130+
}
131+
132+
// daemonSealed indicates that this struct is a DaemonEvent instance.
133+
func (r *RegisterConf[E]) daemonSealed() {}

protofsm/state_machine.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
"time"
77

88
"github.com/btcsuite/btcd/btcec/v2"
9+
"github.com/btcsuite/btcd/chaincfg/chainhash"
910
"github.com/btcsuite/btcd/wire"
11+
"github.com/lightningnetwork/lnd/chainntnfs"
1012
"github.com/lightningnetwork/lnd/fn"
1113
"github.com/lightningnetwork/lnd/lnwire"
1214
)
@@ -83,6 +85,28 @@ type DaemonAdapters interface {
8385

8486
// DisableChannel disables the target channel.
8587
DisableChannel(wire.OutPoint) error
88+
89+
// RegisterConfirmationsNtfn registers an intent to be notified once
90+
// txid reaches numConfs confirmations. We also pass in the pkScript as
91+
// the default light client instead needs to match on scripts created
92+
// in the block. If a nil txid is passed in, then not only should we
93+
// match on the script, but we should also dispatch once the
94+
// transaction containing the script reaches numConfs confirmations.
95+
// This can be useful in instances where we only know the script in
96+
// advance, but not the transaction containing it.
97+
//
98+
// TODO(roasbeef): could abstract further?
99+
RegisterConfirmationsNtfn(txid *chainhash.Hash, pkScript []byte,
100+
numConfs, heightHint uint32,
101+
opts ...chainntnfs.NotifierOption,
102+
) (*chainntnfs.ConfirmationEvent, error)
103+
104+
// RegisterSpendNtfn registers an intent to be notified once the target
105+
// outpoint is successfully spent within a transaction. The script that
106+
// the outpoint creates must also be specified. This allows this
107+
// interface to be implemented by BIP 158-like filtering.
108+
RegisterSpendNtfn(outpoint *wire.OutPoint, pkScript []byte,
109+
heightHint uint32) (*chainntnfs.SpendEvent, error)
86110
}
87111

88112
// stateQuery is used by outside callers to query the internal state of the
@@ -298,6 +322,78 @@ func (s *StateMachine[Event, Env]) executeDaemonEvent(event DaemonEvent) error {
298322
}
299323

300324
return nil
325+
326+
// The state machine has requested a new event to be sent once a
327+
// transaction spending a specified outpoint has confirmed.
328+
case *RegisterSpend[Event]:
329+
spendEvent, err := s.daemon.RegisterSpendNtfn(
330+
&daemonEvent.OutPoint, daemonEvent.PkScript,
331+
daemonEvent.HeightHint,
332+
)
333+
if err != nil {
334+
return fmt.Errorf("unable to register spend: %w", err)
335+
}
336+
337+
s.wg.Add(1)
338+
go func() {
339+
defer s.wg.Done()
340+
for {
341+
select {
342+
case <-spendEvent.Spend:
343+
// If there's a post-send event, then
344+
// we'll send that into the current
345+
// state now.
346+
postSpend := daemonEvent.PostSpendEvent
347+
postSpend.WhenSome(func(e Event) {
348+
s.SendEvent(e)
349+
})
350+
351+
return
352+
353+
case <-s.quit:
354+
return
355+
}
356+
}
357+
}()
358+
359+
return nil
360+
361+
// The state machine has requested a new event to be sent once a
362+
// specified txid+pkScript pair has confirmed.
363+
case *RegisterConf[Event]:
364+
numConfs := daemonEvent.NumConfs.UnwrapOr(1)
365+
confEvent, err := s.daemon.RegisterConfirmationsNtfn(
366+
&daemonEvent.Txid, daemonEvent.PkScript,
367+
numConfs, daemonEvent.HeightHint,
368+
)
369+
if err != nil {
370+
return fmt.Errorf("unable to register conf: %w", err)
371+
}
372+
373+
s.wg.Add(1)
374+
go func() {
375+
defer s.wg.Done()
376+
for {
377+
select {
378+
case <-confEvent.Confirmed:
379+
// If there's a post-conf event, then
380+
// we'll send that into the current
381+
// state now.
382+
//
383+
// TODO(roasbeef): refactor to
384+
// dispatchAfterRecv w/ above
385+
postConf := daemonEvent.PostConfEvent
386+
postConf.WhenSome(func(e Event) {
387+
s.SendEvent(e)
388+
})
389+
390+
return
391+
392+
case <-s.quit:
393+
return
394+
}
395+
}
396+
}()
301397
}
302398

303399
return fmt.Errorf("unknown daemon event: %T", event)

protofsm/state_machine_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"testing"
88

99
"github.com/btcsuite/btcd/btcec/v2"
10+
"github.com/btcsuite/btcd/chaincfg/chainhash"
1011
"github.com/btcsuite/btcd/wire"
12+
"github.com/lightningnetwork/lnd/chainntnfs"
1113
"github.com/lightningnetwork/lnd/fn"
1214
"github.com/lightningnetwork/lnd/lnwire"
1315
"github.com/stretchr/testify/mock"
@@ -163,6 +165,16 @@ func assertStateTransitions[Event any, Env Environment](
163165

164166
type dummyAdapters struct {
165167
mock.Mock
168+
169+
confChan chan *chainntnfs.TxConfirmation
170+
spendChan chan *chainntnfs.SpendDetail
171+
}
172+
173+
func newDaemonAdapters() *dummyAdapters {
174+
return &dummyAdapters{
175+
confChan: make(chan *chainntnfs.TxConfirmation, 1),
176+
spendChan: make(chan *chainntnfs.SpendDetail, 1),
177+
}
166178
}
167179

168180
func (d *dummyAdapters) SendMessages(pub btcec.PublicKey, msgs []lnwire.Message) error {
@@ -183,6 +195,31 @@ func (d *dummyAdapters) DisableChannel(op wire.OutPoint) error {
183195
return args.Error(0)
184196
}
185197

198+
func (d *dummyAdapters) RegisterConfirmationsNtfn(txid *chainhash.Hash,
199+
pkScript []byte, numConfs, heightHint uint32,
200+
opts ...chainntnfs.NotifierOption,
201+
) (*chainntnfs.ConfirmationEvent, error) {
202+
203+
args := d.Called(txid, pkScript, numConfs)
204+
205+
err := args.Error(0)
206+
return &chainntnfs.ConfirmationEvent{
207+
Confirmed: d.confChan,
208+
}, err
209+
}
210+
211+
func (d *dummyAdapters) RegisterSpendNtfn(outpoint *wire.OutPoint,
212+
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
213+
214+
args := d.Called(outpoint, pkScript, heightHint)
215+
216+
err := args.Error(0)
217+
218+
return &chainntnfs.SpendEvent{
219+
Spend: d.spendChan,
220+
}, err
221+
}
222+
186223
// TestStateMachineTerminateCleanup tests that after the state machine
187224
// terminates, it properly cleans up the state machine.
188225
func TestStateMachineTerminateCleanup(t *testing.T) {

0 commit comments

Comments
 (0)