mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr/deposit: replay startup block to recovered deposits
The deposit manager consumes one block epoch before recovered deposit FSMs are started. That left already-expired recovered deposits idle until another block arrived. Remember the startup height and deliver it to active deposit FSMs after recovery and reconciliation have finished. Move the block notification fanout into a helper so startup replay and normal block handling use the same path. Add coverage that starts the manager with a recovered deposit at its expiry height and verifies the expiry sweep is signed and published immediately.
This commit is contained in:
parent
3df71f9a19
commit
3b29ef3629
2 changed files with 95 additions and 19 deletions
|
|
@ -128,9 +128,11 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error {
|
|||
return err
|
||||
}
|
||||
|
||||
var startupHeight uint32
|
||||
select {
|
||||
case height := <-newBlockChan:
|
||||
m.currentHeight.Store(uint32(height))
|
||||
startupHeight = uint32(height)
|
||||
m.currentHeight.Store(startupHeight)
|
||||
|
||||
case err = <-newBlockErrChan:
|
||||
return err
|
||||
|
|
@ -154,6 +156,13 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error {
|
|||
log.Errorf("unable to reconcile deposits: %v", err)
|
||||
}
|
||||
|
||||
// The startup height was consumed before recovered deposit FSMs existed.
|
||||
// Replay it so already-expired recovered deposits can act immediately.
|
||||
err = m.notifyActiveDeposits(ctx, startupHeight)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start the deposit notifier.
|
||||
m.pollDeposits(ctx)
|
||||
|
||||
|
|
@ -171,24 +180,9 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error {
|
|||
log.Errorf("unable to reconcile deposits: %v", err)
|
||||
}
|
||||
|
||||
// Inform all active deposits about a new block arrival.
|
||||
m.mu.Lock()
|
||||
activeDeposits := make([]*FSM, 0, len(m.activeDeposits))
|
||||
for _, fsm := range m.activeDeposits {
|
||||
activeDeposits = append(activeDeposits, fsm)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
for _, fsm := range activeDeposits {
|
||||
select {
|
||||
case fsm.blockNtfnChan <- uint32(height):
|
||||
|
||||
case <-fsm.quitChan:
|
||||
continue
|
||||
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
err = m.notifyActiveDeposits(ctx, uint32(height))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case outpoint := <-m.finalizedDepositChan:
|
||||
|
|
@ -205,6 +199,33 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error {
|
|||
}
|
||||
}
|
||||
|
||||
// notifyActiveDeposits informs all active deposit FSMs about a new block
|
||||
// height.
|
||||
func (m *Manager) notifyActiveDeposits(ctx context.Context,
|
||||
height uint32) error {
|
||||
|
||||
m.mu.Lock()
|
||||
activeDeposits := make([]*FSM, 0, len(m.activeDeposits))
|
||||
for _, fsm := range m.activeDeposits {
|
||||
activeDeposits = append(activeDeposits, fsm)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
for _, fsm := range activeDeposits {
|
||||
select {
|
||||
case fsm.blockNtfnChan <- height:
|
||||
|
||||
case <-fsm.quitChan:
|
||||
continue
|
||||
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// recoverDeposits recovers static address parameters, previous deposits and
|
||||
// state machines from the database and starts the deposit notifier.
|
||||
func (m *Manager) recoverDeposits(ctx context.Context) error {
|
||||
|
|
|
|||
|
|
@ -346,6 +346,61 @@ func TestManager(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestManagerReplaysStartupBlockToRecoveredDeposits verifies that the initial
|
||||
// block epoch consumed during startup is delivered to recovered deposit FSMs.
|
||||
func TestManagerReplaysStartupBlockToRecoveredDeposits(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
const defaultTimeout = 30 * time.Second
|
||||
|
||||
testContext := newManagerTestContext(t)
|
||||
|
||||
initChan := make(chan struct{})
|
||||
runErrChan := make(chan error, 1)
|
||||
go func() {
|
||||
runErrChan <- testContext.manager.Run(ctx, initChan)
|
||||
}()
|
||||
|
||||
// Send only the startup block at the recovered deposit's expiry height.
|
||||
testContext.blockChan <- int32(
|
||||
defaultDepositConfirmations + defaultExpiry,
|
||||
)
|
||||
|
||||
select {
|
||||
case <-initChan:
|
||||
|
||||
case err := <-runErrChan:
|
||||
require.NoError(t, err, "manager failed to start")
|
||||
|
||||
case <-time.After(defaultTimeout):
|
||||
t.Fatal("manager timed out starting")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-testContext.mockLnd.SignOutputRawChannel:
|
||||
|
||||
case <-time.After(defaultTimeout):
|
||||
t.Fatal("did not receive sign request")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-testContext.mockLnd.TxPublishChannel:
|
||||
|
||||
case <-time.After(defaultTimeout):
|
||||
t.Fatal("did not receive published expiry tx")
|
||||
}
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case err := <-runErrChan:
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
|
||||
case <-time.After(defaultTimeout):
|
||||
t.Fatal("manager did not stop")
|
||||
}
|
||||
}
|
||||
|
||||
// ManagerTestContext is a helper struct that contains all the necessary
|
||||
// components to test the reservation manager.
|
||||
type ManagerTestContext struct {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue