protofsm: add daemon events for spend+conf registration

This commit is contained in:
Olaoluwa Osuntokun 2024-01-23 18:54:46 -08:00
parent 3bae7f32cd
commit 7f69ceb2d4
No known key found for this signature in database
GPG key ID: 90525F7DEEE0AD86
3 changed files with 186 additions and 1 deletions

View file

@ -2,6 +2,7 @@ package protofsm
import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwire"
@ -21,7 +22,8 @@ type DaemonEventSet []DaemonEvent
// DaemonEvents is a special type constraint that enumerates all the possible
// types of daemon events.
type DaemonEvents interface {
SendMsgEvent[any] | BroadcastTxn
SendMsgEvent[any] | BroadcastTxn | RegisterSpend[any] |
RegisterConf[any]
}
// SendPredicate is a function that returns true if the target message should
@ -64,3 +66,53 @@ type BroadcastTxn struct {
// daemonSealed indicates that this struct is a DaemonEvent instance.
func (b *BroadcastTxn) daemonSealed() {}
// RegisterSpend is used to request that a certain event is sent into the state
// machien once the specified outpoint has been spent.
type RegisterSpend[Event any] struct {
// OutPoint is the outpoint on chain to watch.
OutPoint wire.OutPoint
// PkScript is the script that we expect to be spent along with the
// outpoint.
PkScript []byte
// HeightHint is a value used to give the chain scanner a hint on how
// far back it needs to start its search.
HeightHint uint32
// PostSpendEvent is an event that's sent back to the requester once a
// transaction spending the outpoint has been confirmed in the main
// chain.
PostSpendEvent fn.Option[Event]
}
// daemonSealed indicates that this struct is a DaemonEvent instance.
func (r *RegisterSpend[E]) daemonSealed() {}
// RegisterConf is used to request that a certain event is sent into the state
// machien once the specified outpoint has been spent.
type RegisterConf[Event any] struct {
// Txid is the txid of the txn we want to watch the chain for.
Txid chainhash.Hash
// PkScript is the script that we expect to be created along with the
// outpoint.
PkScript []byte
// HeightHint is a value used to give the chain scanner a hint on how
// far back it needs to start its search.
HeightHint uint32
// NumConfs is the number of confirmations that the spending
// transaction needs to dispatch an event.
NumConfs fn.Option[uint32]
// PostConfEvent is an event that's sent back to the requester once the
// transaction specified above has confirmed in the chain with
// sufficient depth.
PostConfEvent fn.Option[Event]
}
// daemonSealed indicates that this struct is a DaemonEvent instance.
func (r *RegisterConf[E]) daemonSealed() {}

View file

@ -6,7 +6,9 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwire"
)
@ -77,6 +79,28 @@ type DaemonAdapters interface {
// BroadcastTransaction broadcasts a transaction with the target label.
BroadcastTransaction(*wire.MsgTx, string) error
// RegisterConfirmationsNtfn registers an intent to be notified once
// txid reaches numConfs confirmations. We also pass in the pkScript as
// the default light client instead needs to match on scripts created
// in the block. If a nil txid is passed in, then not only should we
// match on the script, but we should also dispatch once the
// transaction containing the script reaches numConfs confirmations.
// This can be useful in instances where we only know the script in
// advance, but not the transaction containing it.
//
// TODO(roasbeef): could abstract further?
RegisterConfirmationsNtfn(txid *chainhash.Hash, pkScript []byte,
numConfs, heightHint uint32,
opts ...chainntnfs.NotifierOption,
) (*chainntnfs.ConfirmationEvent, error)
// RegisterSpendNtfn registers an intent to be notified once the target
// outpoint is successfully spent within a transaction. The script that
// the outpoint creates must also be specified. This allows this
// interface to be implemented by BIP 158-like filtering.
RegisterSpendNtfn(outpoint *wire.OutPoint, pkScript []byte,
heightHint uint32) (*chainntnfs.SpendEvent, error)
}
// 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 {
}
return nil
// The state machine has requested a new event to be sent once a
// transaction spending a specified outpoint has confirmed.
case *RegisterSpend[Event]:
spendEvent, err := s.daemon.RegisterSpendNtfn(
&daemonEvent.OutPoint, daemonEvent.PkScript,
daemonEvent.HeightHint,
)
if err != nil {
return fmt.Errorf("unable to register spend: %w", err)
}
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
select {
case <-spendEvent.Spend:
// If there's a post-send event, then
// we'll send that into the current
// state now.
postSpend := daemonEvent.PostSpendEvent
postSpend.WhenSome(func(e Event) {
s.SendEvent(e)
})
return
case <-s.quit:
return
}
}
}()
return nil
// The state machine has requested a new event to be sent once a
// specified txid+pkScript pair has confirmed.
case *RegisterConf[Event]:
numConfs := daemonEvent.NumConfs.UnwrapOr(1)
confEvent, err := s.daemon.RegisterConfirmationsNtfn(
&daemonEvent.Txid, daemonEvent.PkScript,
numConfs, daemonEvent.HeightHint,
)
if err != nil {
return fmt.Errorf("unable to register conf: %w", err)
}
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
select {
case <-confEvent.Confirmed:
// If there's a post-conf event, then
// we'll send that into the current
// state now.
//
// TODO(roasbeef): refactor to
// dispatchAfterRecv w/ above
postConf := daemonEvent.PostConfEvent
postConf.WhenSome(func(e Event) {
s.SendEvent(e)
})
return
case <-s.quit:
return
}
}
}()
}
return fmt.Errorf("unknown daemon event: %T", event)

View file

@ -7,7 +7,9 @@ import (
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/mock"
@ -160,6 +162,16 @@ func assertStateTransitions[Event any, Env Environment](
type dummyAdapters struct {
mock.Mock
confChan chan *chainntnfs.TxConfirmation
spendChan chan *chainntnfs.SpendDetail
}
func newDaemonAdapters() *dummyAdapters {
return &dummyAdapters{
confChan: make(chan *chainntnfs.TxConfirmation, 1),
spendChan: make(chan *chainntnfs.SpendDetail, 1),
}
}
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
return args.Error(0)
}
func (d *dummyAdapters) RegisterConfirmationsNtfn(txid *chainhash.Hash,
pkScript []byte, numConfs, heightHint uint32,
opts ...chainntnfs.NotifierOption,
) (*chainntnfs.ConfirmationEvent, error) {
args := d.Called(txid, pkScript, numConfs)
err := args.Error(0)
return &chainntnfs.ConfirmationEvent{
Confirmed: d.confChan,
}, err
}
func (d *dummyAdapters) RegisterSpendNtfn(outpoint *wire.OutPoint,
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
args := d.Called(outpoint, pkScript, heightHint)
err := args.Error(0)
return &chainntnfs.SpendEvent{
Spend: d.spendChan,
}, err
}
// TestStateMachineInternalEvents tests that the state machine is able to add
// new internal events to the event queue for further processing during a state
// transition.