From 7f69ceb2d43bd9aca79354eb96608e0a891db126 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 23 Jan 2024 18:54:46 -0800 Subject: [PATCH] protofsm: add daemon events for spend+conf registration --- protofsm/daemon_events.go | 54 ++++++++++++++++++- protofsm/state_machine.go | 96 ++++++++++++++++++++++++++++++++++ protofsm/state_machine_test.go | 37 +++++++++++++ 3 files changed, 186 insertions(+), 1 deletion(-) diff --git a/protofsm/daemon_events.go b/protofsm/daemon_events.go index 39719d65f..5a269c7f1 100644 --- a/protofsm/daemon_events.go +++ b/protofsm/daemon_events.go @@ -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() {} diff --git a/protofsm/state_machine.go b/protofsm/state_machine.go index f1a1e3b54..e25d82515 100644 --- a/protofsm/state_machine.go +++ b/protofsm/state_machine.go @@ -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) diff --git a/protofsm/state_machine_test.go b/protofsm/state_machine_test.go index 71760726e..17a5a32c4 100644 --- a/protofsm/state_machine_test.go +++ b/protofsm/state_machine_test.go @@ -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.