Skip to content

Commit 7f69ceb

Browse files
committed
protofsm: add daemon events for spend+conf registration
1 parent 3bae7f3 commit 7f69ceb

File tree

3 files changed

+186
-1
lines changed

3 files changed

+186
-1
lines changed

protofsm/daemon_events.go

Lines changed: 53 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] | BroadcastTxn
25+
SendMsgEvent[any] | BroadcastTxn | RegisterSpend[any] |
26+
RegisterConf[any]
2527
}
2628

2729
// SendPredicate is a function that returns true if the target message should
@@ -64,3 +66,53 @@ type BroadcastTxn struct {
6466

6567
// daemonSealed indicates that this struct is a DaemonEvent instance.
6668
func (b *BroadcastTxn) daemonSealed() {}
69+
70+
// RegisterSpend is used to request that a certain event is sent into the state
71+
// machien once the specified outpoint has been spent.
72+
type RegisterSpend[Event any] struct {
73+
// OutPoint is the outpoint on chain to watch.
74+
OutPoint wire.OutPoint
75+
76+
// PkScript is the script that we expect to be spent along with the
77+
// outpoint.
78+
PkScript []byte
79+
80+
// HeightHint is a value used to give the chain scanner a hint on how
81+
// far back it needs to start its search.
82+
HeightHint uint32
83+
84+
// PostSpendEvent is an event that's sent back to the requester once a
85+
// transaction spending the outpoint has been confirmed in the main
86+
// chain.
87+
PostSpendEvent fn.Option[Event]
88+
}
89+
90+
// daemonSealed indicates that this struct is a DaemonEvent instance.
91+
func (r *RegisterSpend[E]) daemonSealed() {}
92+
93+
// RegisterConf is used to request that a certain event is sent into the state
94+
// machien once the specified outpoint has been spent.
95+
type RegisterConf[Event any] struct {
96+
// Txid is the txid of the txn we want to watch the chain for.
97+
Txid chainhash.Hash
98+
99+
// PkScript is the script that we expect to be created along with the
100+
// outpoint.
101+
PkScript []byte
102+
103+
// HeightHint is a value used to give the chain scanner a hint on how
104+
// far back it needs to start its search.
105+
HeightHint uint32
106+
107+
// NumConfs is the number of confirmations that the spending
108+
// transaction needs to dispatch an event.
109+
NumConfs fn.Option[uint32]
110+
111+
// PostConfEvent is an event that's sent back to the requester once the
112+
// transaction specified above has confirmed in the chain with
113+
// sufficient depth.
114+
PostConfEvent fn.Option[Event]
115+
}
116+
117+
// daemonSealed indicates that this struct is a DaemonEvent instance.
118+
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
)
@@ -77,6 +79,28 @@ type DaemonAdapters interface {
7779

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

82106
// stateQuery is used by outside callers to query the internal state of the
@@ -282,6 +306,78 @@ func (s *StateMachine[Event, Env]) executeDaemonEvent(event DaemonEvent) error {
282306
}
283307

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

287383
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"
@@ -160,6 +162,16 @@ func assertStateTransitions[Event any, Env Environment](
160162

161163
type dummyAdapters struct {
162164
mock.Mock
165+
166+
confChan chan *chainntnfs.TxConfirmation
167+
spendChan chan *chainntnfs.SpendDetail
168+
}
169+
170+
func newDaemonAdapters() *dummyAdapters {
171+
return &dummyAdapters{
172+
confChan: make(chan *chainntnfs.TxConfirmation, 1),
173+
spendChan: make(chan *chainntnfs.SpendDetail, 1),
174+
}
163175
}
164176

165177
func (d *dummyAdapters) SendMessages(pub btcec.PublicKey, msgs []lnwire.Message) error {
@@ -174,6 +186,31 @@ func (d *dummyAdapters) BroadcastTransaction(tx *wire.MsgTx, label string) error
174186
return args.Error(0)
175187
}
176188

189+
func (d *dummyAdapters) RegisterConfirmationsNtfn(txid *chainhash.Hash,
190+
pkScript []byte, numConfs, heightHint uint32,
191+
opts ...chainntnfs.NotifierOption,
192+
) (*chainntnfs.ConfirmationEvent, error) {
193+
194+
args := d.Called(txid, pkScript, numConfs)
195+
196+
err := args.Error(0)
197+
return &chainntnfs.ConfirmationEvent{
198+
Confirmed: d.confChan,
199+
}, err
200+
}
201+
202+
func (d *dummyAdapters) RegisterSpendNtfn(outpoint *wire.OutPoint,
203+
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
204+
205+
args := d.Called(outpoint, pkScript, heightHint)
206+
207+
err := args.Error(0)
208+
209+
return &chainntnfs.SpendEvent{
210+
Spend: d.spendChan,
211+
}, err
212+
}
213+
177214
// TestStateMachineInternalEvents tests that the state machine is able to add
178215
// new internal events to the event queue for further processing during a state
179216
// transition.

0 commit comments

Comments
 (0)