mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-08-29 15:11:09 +02:00
Merge branch '0-19-2-branch-rc1-9725' into 0-19-2-branch-rc1
This commit is contained in:
@@ -72,6 +72,10 @@ func (b *BroadcastTxn) daemonSealed() {}
|
|||||||
// custom state machine event.
|
// custom state machine event.
|
||||||
type SpendMapper[Event any] func(*chainntnfs.SpendDetail) Event
|
type SpendMapper[Event any] func(*chainntnfs.SpendDetail) Event
|
||||||
|
|
||||||
|
// ConfMapper is a function that's used to map a confirmation notification to a
|
||||||
|
// custom state machine event.
|
||||||
|
type ConfMapper[Event any] func(*chainntnfs.TxConfirmation) Event
|
||||||
|
|
||||||
// RegisterSpend is used to request that a certain event is sent into the state
|
// RegisterSpend is used to request that a certain event is sent into the state
|
||||||
// machine once the specified outpoint has been spent.
|
// machine once the specified outpoint has been spent.
|
||||||
type RegisterSpend[Event any] struct {
|
type RegisterSpend[Event any] struct {
|
||||||
@@ -112,10 +116,9 @@ type RegisterConf[Event any] struct {
|
|||||||
// transaction needs to dispatch an event.
|
// transaction needs to dispatch an event.
|
||||||
NumConfs fn.Option[uint32]
|
NumConfs fn.Option[uint32]
|
||||||
|
|
||||||
// PostConfEvent is an event that's sent back to the requester once the
|
// PostConfMapper is a special conf mapper, that if present, will be
|
||||||
// transaction specified above has confirmed in the chain with
|
// used to map the protofsm confirmation event to a custom event.
|
||||||
// sufficient depth.
|
PostConfMapper fn.Option[ConfMapper[Event]]
|
||||||
PostConfEvent fn.Option[Event]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// daemonSealed indicates that this struct is a DaemonEvent instance.
|
// daemonSealed indicates that this struct is a DaemonEvent instance.
|
||||||
|
@@ -522,16 +522,19 @@ func (s *StateMachine[Event, Env]) executeDaemonEvent(ctx context.Context,
|
|||||||
launched := s.gm.Go(ctx, func(ctx context.Context) {
|
launched := s.gm.Go(ctx, func(ctx context.Context) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-confEvent.Confirmed:
|
//nolint:ll
|
||||||
// If there's a post-conf event, then
|
case conf, ok := <-confEvent.Confirmed:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a post-conf mapper, then
|
||||||
// we'll send that into the current
|
// we'll send that into the current
|
||||||
// state now.
|
// state now.
|
||||||
//
|
postConfMapper := daemonEvent.PostConfMapper
|
||||||
// TODO(roasbeef): refactor to
|
postConfMapper.WhenSome(func(f ConfMapper[Event]) {
|
||||||
// dispatchAfterRecv w/ above
|
customEvent := f(conf)
|
||||||
postConf := daemonEvent.PostConfEvent
|
s.SendEvent(ctx, customEvent)
|
||||||
postConf.WhenSome(func(e Event) {
|
|
||||||
s.SendEvent(ctx, e)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@@ -40,6 +40,34 @@ type daemonEvents struct {
|
|||||||
func (s *daemonEvents) dummy() {
|
func (s *daemonEvents) dummy() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type confDetailsEvent struct {
|
||||||
|
blockHash chainhash.Hash
|
||||||
|
blockHeight uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *confDetailsEvent) dummy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
type registerConf struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registerConf) dummy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
type spendDetailsEvent struct {
|
||||||
|
spenderTxHash chainhash.Hash
|
||||||
|
spendingHeight int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *spendDetailsEvent) dummy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
type registerSpend struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registerSpend) dummy() {
|
||||||
|
}
|
||||||
|
|
||||||
type dummyEnv struct {
|
type dummyEnv struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
@@ -74,7 +102,7 @@ var (
|
|||||||
func (d *dummyStateStart) ProcessEvent(event dummyEvents, env *dummyEnv,
|
func (d *dummyStateStart) ProcessEvent(event dummyEvents, env *dummyEnv,
|
||||||
) (*StateTransition[dummyEvents, *dummyEnv], error) {
|
) (*StateTransition[dummyEvents, *dummyEnv], error) {
|
||||||
|
|
||||||
switch event.(type) {
|
switch newEvent := event.(type) {
|
||||||
case *goToFin:
|
case *goToFin:
|
||||||
return &StateTransition[dummyEvents, *dummyEnv]{
|
return &StateTransition[dummyEvents, *dummyEnv]{
|
||||||
NextState: &dummyStateFin{},
|
NextState: &dummyStateFin{},
|
||||||
@@ -127,6 +155,100 @@ func (d *dummyStateStart) ProcessEvent(event dummyEvents, env *dummyEnv,
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
|
// This state will emit a RegisterConf event which uses a mapper to
|
||||||
|
// transition to the final state upon confirmation.
|
||||||
|
case *registerConf:
|
||||||
|
confMapper := func(
|
||||||
|
conf *chainntnfs.TxConfirmation) dummyEvents {
|
||||||
|
|
||||||
|
// Map the conf details into our custom event.
|
||||||
|
return &confDetailsEvent{
|
||||||
|
blockHash: *conf.BlockHash,
|
||||||
|
blockHeight: conf.BlockHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
regConfEvent := &RegisterConf[dummyEvents]{
|
||||||
|
Txid: chainhash.Hash{1},
|
||||||
|
PkScript: []byte{0x01},
|
||||||
|
HeightHint: 100,
|
||||||
|
PostConfMapper: fn.Some[ConfMapper[dummyEvents]](
|
||||||
|
confMapper,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StateTransition[dummyEvents, *dummyEnv]{
|
||||||
|
// Stay in the start state until the conf event is
|
||||||
|
// received and mapped.
|
||||||
|
NextState: &dummyStateStart{
|
||||||
|
canSend: d.canSend,
|
||||||
|
},
|
||||||
|
NewEvents: fn.Some(EmittedEvent[dummyEvents]{
|
||||||
|
ExternalEvents: DaemonEventSet{
|
||||||
|
regConfEvent,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
// This event contains details from the confirmation and signals us to
|
||||||
|
// transition to the final state.
|
||||||
|
case *confDetailsEvent:
|
||||||
|
// We received the mapped confirmation details, transition to
|
||||||
|
// the confirmed state.
|
||||||
|
return &StateTransition[dummyEvents, *dummyEnv]{
|
||||||
|
NextState: &dummyStateConfirmed{
|
||||||
|
blockHash: newEvent.blockHash,
|
||||||
|
blockHeight: newEvent.blockHeight,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
// This state will emit a RegisterSpend event which uses a mapper to
|
||||||
|
// transition to the spent state upon spend detection.
|
||||||
|
case *registerSpend:
|
||||||
|
spendMapper := func(
|
||||||
|
spend *chainntnfs.SpendDetail) dummyEvents {
|
||||||
|
|
||||||
|
// Map the spend details into our custom event.
|
||||||
|
return &spendDetailsEvent{
|
||||||
|
spenderTxHash: *spend.SpenderTxHash,
|
||||||
|
spendingHeight: spend.SpendingHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
regSpendEvent := &RegisterSpend[dummyEvents]{
|
||||||
|
OutPoint: wire.OutPoint{Hash: chainhash.Hash{3}},
|
||||||
|
PkScript: []byte{0x03},
|
||||||
|
HeightHint: 300,
|
||||||
|
PostSpendEvent: fn.Some[SpendMapper[dummyEvents]](
|
||||||
|
spendMapper,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StateTransition[dummyEvents, *dummyEnv]{
|
||||||
|
// Stay in the start state until the spend event is
|
||||||
|
// received and mapped.
|
||||||
|
NextState: &dummyStateStart{
|
||||||
|
canSend: d.canSend,
|
||||||
|
},
|
||||||
|
NewEvents: fn.Some(EmittedEvent[dummyEvents]{
|
||||||
|
ExternalEvents: DaemonEventSet{
|
||||||
|
regSpendEvent,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
// This event contains details from the spend notification and signals
|
||||||
|
// us to transition to the spent state.
|
||||||
|
case *spendDetailsEvent:
|
||||||
|
// We received the mapped spend details, transition to the
|
||||||
|
// spent state.
|
||||||
|
return &StateTransition[dummyEvents, *dummyEnv]{
|
||||||
|
NextState: &dummyStateSpent{
|
||||||
|
spenderTxHash: newEvent.spenderTxHash,
|
||||||
|
spendingHeight: newEvent.spendingHeight,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("unknown event: %T", event)
|
return nil, fmt.Errorf("unknown event: %T", event)
|
||||||
@@ -155,12 +277,64 @@ func (d *dummyStateFin) IsTerminal() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertState[Event any, Env Environment](t *testing.T,
|
type dummyStateConfirmed struct {
|
||||||
m *StateMachine[Event, Env], expectedState State[Event, Env]) {
|
blockHash chainhash.Hash
|
||||||
|
blockHeight uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyStateConfirmed) String() string {
|
||||||
|
return "dummyStateConfirmed"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyStateConfirmed) ProcessEvent(event dummyEvents, env *dummyEnv,
|
||||||
|
) (*StateTransition[dummyEvents, *dummyEnv], error) {
|
||||||
|
|
||||||
|
// This is a terminal state, no further transitions.
|
||||||
|
return &StateTransition[dummyEvents, *dummyEnv]{
|
||||||
|
NextState: d,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyStateConfirmed) IsTerminal() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type dummyStateSpent struct {
|
||||||
|
spenderTxHash chainhash.Hash
|
||||||
|
spendingHeight int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyStateSpent) String() string {
|
||||||
|
return "dummyStateSpent"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyStateSpent) ProcessEvent(event dummyEvents, env *dummyEnv,
|
||||||
|
) (*StateTransition[dummyEvents, *dummyEnv], error) {
|
||||||
|
|
||||||
|
// This is a terminal state, no further transitions.
|
||||||
|
return &StateTransition[dummyEvents, *dummyEnv]{
|
||||||
|
NextState: d,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyStateSpent) IsTerminal() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertState asserts that the state machine is currently in the expected
|
||||||
|
// state type and returns the state cast to that type.
|
||||||
|
func assertState[Event any, Env Environment, S State[Event, Env]](t *testing.T,
|
||||||
|
m *StateMachine[Event, Env], expectedState S) S {
|
||||||
|
|
||||||
state, err := m.CurrentState()
|
state, err := m.CurrentState()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.IsType(t, expectedState, state)
|
require.IsType(t, expectedState, state)
|
||||||
|
|
||||||
|
// Perform the type assertion to return the concrete type.
|
||||||
|
concreteState, ok := state.(S)
|
||||||
|
require.True(t, ok, "state type assertion failed")
|
||||||
|
|
||||||
|
return concreteState
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertStateTransitions[Event any, Env Environment](
|
func assertStateTransitions[Event any, Env Environment](
|
||||||
@@ -415,6 +589,156 @@ func TestStateMachineDaemonEvents(t *testing.T) {
|
|||||||
env.AssertExpectations(t)
|
env.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestStateMachineConfMapper tests that the state machine is able to properly
|
||||||
|
// map the confirmation event into a custom event that can be used to trigger a
|
||||||
|
// state transition.
|
||||||
|
func TestStateMachineConfMapper(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create the state machine.
|
||||||
|
env := &dummyEnv{}
|
||||||
|
startingState := &dummyStateStart{}
|
||||||
|
adapters := newDaemonAdapters()
|
||||||
|
|
||||||
|
cfg := StateMachineCfg[dummyEvents, *dummyEnv]{
|
||||||
|
Daemon: adapters,
|
||||||
|
InitialState: startingState,
|
||||||
|
Env: env,
|
||||||
|
}
|
||||||
|
stateMachine := NewStateMachine(cfg)
|
||||||
|
|
||||||
|
stateSub := stateMachine.RegisterStateEvents()
|
||||||
|
defer stateMachine.RemoveStateSub(stateSub)
|
||||||
|
|
||||||
|
stateMachine.Start(ctx)
|
||||||
|
defer stateMachine.Stop()
|
||||||
|
|
||||||
|
// Expect the RegisterConfirmationsNtfn call when we send the event.
|
||||||
|
// We use NumConfs=1 as the default.
|
||||||
|
adapters.On(
|
||||||
|
"RegisterConfirmationsNtfn", &chainhash.Hash{1}, []byte{0x01},
|
||||||
|
uint32(1),
|
||||||
|
).Return(nil)
|
||||||
|
|
||||||
|
// Send the event that triggers RegisterConf emission.
|
||||||
|
stateMachine.SendEvent(ctx, ®isterConf{})
|
||||||
|
|
||||||
|
// We should transition back to the starting state initially.
|
||||||
|
expectedStates := []State[dummyEvents, *dummyEnv]{
|
||||||
|
&dummyStateStart{}, &dummyStateStart{},
|
||||||
|
}
|
||||||
|
assertStateTransitions(t, stateSub, expectedStates)
|
||||||
|
|
||||||
|
// Assert the registration call was made.
|
||||||
|
adapters.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Now, simulate the confirmation event coming back from the notifier.
|
||||||
|
// Populate it with some data to be mapped.
|
||||||
|
simulatedConf := &chainntnfs.TxConfirmation{
|
||||||
|
BlockHash: &chainhash.Hash{2},
|
||||||
|
BlockHeight: 123,
|
||||||
|
}
|
||||||
|
adapters.confChan <- simulatedConf
|
||||||
|
|
||||||
|
// This should trigger the mapper and send the confDetailsEvent,
|
||||||
|
// transitioning us to the final state.
|
||||||
|
expectedStates = []State[dummyEvents, *dummyEnv]{&dummyStateConfirmed{}}
|
||||||
|
assertStateTransitions(t, stateSub, expectedStates)
|
||||||
|
|
||||||
|
// Final state assertion.
|
||||||
|
finalState := assertState(t, &stateMachine, &dummyStateConfirmed{})
|
||||||
|
|
||||||
|
// Assert that the details from the confirmation event were correctly
|
||||||
|
// propagated to the final state.
|
||||||
|
require.Equal(t,
|
||||||
|
*simulatedConf.BlockHash, finalState.blockHash,
|
||||||
|
)
|
||||||
|
require.Equal(t,
|
||||||
|
simulatedConf.BlockHeight, finalState.blockHeight,
|
||||||
|
)
|
||||||
|
|
||||||
|
adapters.AssertExpectations(t)
|
||||||
|
env.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStateMachineSpendMapper tests that the state machine is able to properly
|
||||||
|
// map the spend event into a custom event that can be used to trigger a state
|
||||||
|
// transition.
|
||||||
|
func TestStateMachineSpendMapper(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create the state machine.
|
||||||
|
env := &dummyEnv{}
|
||||||
|
startingState := &dummyStateStart{}
|
||||||
|
adapters := newDaemonAdapters()
|
||||||
|
|
||||||
|
cfg := StateMachineCfg[dummyEvents, *dummyEnv]{
|
||||||
|
Daemon: adapters,
|
||||||
|
InitialState: startingState,
|
||||||
|
Env: env,
|
||||||
|
}
|
||||||
|
stateMachine := NewStateMachine(cfg)
|
||||||
|
|
||||||
|
stateSub := stateMachine.RegisterStateEvents()
|
||||||
|
defer stateMachine.RemoveStateSub(stateSub)
|
||||||
|
|
||||||
|
stateMachine.Start(ctx)
|
||||||
|
defer stateMachine.Stop()
|
||||||
|
|
||||||
|
// Expect the RegisterSpendNtfn call when we send the event.
|
||||||
|
targetOutpoint := &wire.OutPoint{Hash: chainhash.Hash{3}}
|
||||||
|
targetPkScript := []byte{0x03}
|
||||||
|
targetHeightHint := uint32(300)
|
||||||
|
adapters.On(
|
||||||
|
"RegisterSpendNtfn", targetOutpoint, targetPkScript,
|
||||||
|
targetHeightHint,
|
||||||
|
).Return(nil)
|
||||||
|
|
||||||
|
// Send the event that triggers RegisterSpend emission.
|
||||||
|
stateMachine.SendEvent(ctx, ®isterSpend{})
|
||||||
|
|
||||||
|
// We should transition back to the starting state initially.
|
||||||
|
expectedStates := []State[dummyEvents, *dummyEnv]{
|
||||||
|
&dummyStateStart{}, &dummyStateStart{},
|
||||||
|
}
|
||||||
|
assertStateTransitions(t, stateSub, expectedStates)
|
||||||
|
|
||||||
|
// Assert the registration call was made.
|
||||||
|
adapters.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Now, simulate the spend event coming back from the notifier. Populate
|
||||||
|
// it with some data to be mapped.
|
||||||
|
simulatedSpend := &chainntnfs.SpendDetail{
|
||||||
|
SpentOutPoint: targetOutpoint,
|
||||||
|
SpenderTxHash: &chainhash.Hash{4},
|
||||||
|
SpendingTx: &wire.MsgTx{},
|
||||||
|
SpendingHeight: 456,
|
||||||
|
}
|
||||||
|
adapters.spendChan <- simulatedSpend
|
||||||
|
|
||||||
|
// This should trigger the mapper and send the spendDetailsEvent,
|
||||||
|
// transitioning us to the spent state.
|
||||||
|
expectedStates = []State[dummyEvents, *dummyEnv]{&dummyStateSpent{}}
|
||||||
|
assertStateTransitions(t, stateSub, expectedStates)
|
||||||
|
|
||||||
|
// Final state assertion.
|
||||||
|
finalState := assertState(t, &stateMachine, &dummyStateSpent{})
|
||||||
|
|
||||||
|
// Assert that the details from the spend event were correctly
|
||||||
|
// propagated to the final state.
|
||||||
|
require.Equal(t,
|
||||||
|
*simulatedSpend.SpenderTxHash, finalState.spenderTxHash,
|
||||||
|
)
|
||||||
|
require.Equal(t,
|
||||||
|
simulatedSpend.SpendingHeight, finalState.spendingHeight,
|
||||||
|
)
|
||||||
|
|
||||||
|
adapters.AssertExpectations(t)
|
||||||
|
env.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
type dummyMsgMapper struct {
|
type dummyMsgMapper struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user