Skip to content

Commit 31e7023

Browse files
committed
protofsm: add daemon events for spend+conf registration
1 parent 1d721a4 commit 31e7023

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] | 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,56 @@ 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+
//
115+
// TODO(roasbeef): will also need the confirming tx, block header, etc?
116+
// * need method on the event to bind the conf/spend details
117+
PostConfEvent fn.Option[Event]
118+
}
119+
120+
// daemonSealed indicates that this struct is a DaemonEvent instance.
121+
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
)
@@ -80,6 +82,28 @@ type DaemonAdapters interface {
8082

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

85109
// stateQuery is used by outside callers to query the internal state of the
@@ -285,6 +309,78 @@ func (s *StateMachine[Event, Env]) executeDaemonEvent(event DaemonEvent) error {
285309
}
286310

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

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

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

167179
func (d *dummyAdapters) SendMessages(pub btcec.PublicKey, msgs []lnwire.Message) error {
@@ -176,6 +188,31 @@ func (d *dummyAdapters) BroadcastTransaction(tx *wire.MsgTx, label string) error
176188
return args.Error(0)
177189
}
178190

191+
func (d *dummyAdapters) RegisterConfirmationsNtfn(txid *chainhash.Hash,
192+
pkScript []byte, numConfs, heightHint uint32,
193+
opts ...chainntnfs.NotifierOption,
194+
) (*chainntnfs.ConfirmationEvent, error) {
195+
196+
args := d.Called(txid, pkScript, numConfs)
197+
198+
err := args.Error(0)
199+
return &chainntnfs.ConfirmationEvent{
200+
Confirmed: d.confChan,
201+
}, err
202+
}
203+
204+
func (d *dummyAdapters) RegisterSpendNtfn(outpoint *wire.OutPoint,
205+
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
206+
207+
args := d.Called(outpoint, pkScript, heightHint)
208+
209+
err := args.Error(0)
210+
211+
return &chainntnfs.SpendEvent{
212+
Spend: d.spendChan,
213+
}, err
214+
}
215+
179216
// TestStateMachineTerminateCleanup tests that after the state machine
180217
// terminates, it properly cleans up the state machine.
181218
func TestStateMachineTerminateCleanup(t *testing.T) {

0 commit comments

Comments
 (0)