mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr/deposit: replay startup block after recovery
The first block epoch is consumed before recovered deposit FSMs exist. Replay that startup height after recovery so already-expired deposits can run expiry handling immediately after restart.
This commit is contained in:
parent
1abe617991
commit
ac12d251f5
2 changed files with 175 additions and 0 deletions
|
|
@ -136,6 +136,14 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error {
|
|||
err = m.reconcileDeposits(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("unable to reconcile deposits: %v", err)
|
||||
} else {
|
||||
// The startup height was consumed before recovered deposit FSMs
|
||||
// existed. Replay it so already-expired recovered deposits can act
|
||||
// immediately, but only after their wallet view is fresh.
|
||||
err = m.notifyActiveDeposits(ctx, startupHeight)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Start the deposit notifier.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package deposit
|
|||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -141,6 +142,22 @@ func (m *mockAddressManager) ListUnspent(ctx context.Context,
|
|||
args.Error(1)
|
||||
}
|
||||
|
||||
// listUnspentOverride delegates all address-manager methods except
|
||||
// ListUnspent to another implementation.
|
||||
type listUnspentOverride struct {
|
||||
AddressManager
|
||||
|
||||
listUnspent func(context.Context, int32, int32) ([]*lnwallet.Utxo,
|
||||
error)
|
||||
}
|
||||
|
||||
// ListUnspent calls the override's ListUnspent implementation.
|
||||
func (l *listUnspentOverride) ListUnspent(ctx context.Context,
|
||||
minConfs, maxConfs int32) ([]*lnwallet.Utxo, error) {
|
||||
|
||||
return l.listUnspent(ctx, minConfs, maxConfs)
|
||||
}
|
||||
|
||||
func (m *mockAddressManager) GetTaprootAddress(clientPubkey,
|
||||
serverPubkey *btcec.PublicKey, expiry int64) (*btcutil.AddressTaproot,
|
||||
error) {
|
||||
|
|
@ -314,6 +331,156 @@ 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")
|
||||
}
|
||||
}
|
||||
|
||||
// TestManagerSkipsExpiryNotificationOnReconcileFailure verifies that deposit
|
||||
// FSMs cannot make an expiry decision from stale confirmation data when wallet
|
||||
// reconciliation fails at startup or while processing a later block.
|
||||
func TestManagerSkipsExpiryNotificationOnReconcileFailure(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
startupHeight int32
|
||||
blockHeight int32
|
||||
}{
|
||||
{
|
||||
name: "startup",
|
||||
startupHeight: int32(
|
||||
defaultDepositConfirmations + defaultExpiry,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "block",
|
||||
startupHeight: int32(defaultDepositConfirmations),
|
||||
blockHeight: int32(
|
||||
defaultDepositConfirmations + defaultExpiry,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
testContext := newManagerTestContext(t)
|
||||
baseAddressManager := testContext.mockAddressManager
|
||||
var listUnspentCalls int
|
||||
testContext.manager.cfg.AddressManager =
|
||||
&listUnspentOverride{
|
||||
AddressManager: baseAddressManager,
|
||||
listUnspent: func(ctx context.Context,
|
||||
minConfs, maxConfs int32) (
|
||||
[]*lnwallet.Utxo, error) {
|
||||
|
||||
listUnspentCalls++
|
||||
if testCase.blockHeight != 0 &&
|
||||
listUnspentCalls == 1 {
|
||||
|
||||
return baseAddressManager.ListUnspent(
|
||||
ctx, minConfs, maxConfs,
|
||||
)
|
||||
}
|
||||
|
||||
return nil, errors.New(
|
||||
"injected reconciliation failure",
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
initChan := make(chan struct{})
|
||||
runErrChan := make(chan error, 1)
|
||||
go func() {
|
||||
runErrChan <- testContext.manager.Run(ctx, initChan)
|
||||
}()
|
||||
|
||||
testContext.blockChan <- testCase.startupHeight
|
||||
|
||||
select {
|
||||
case <-initChan:
|
||||
|
||||
case err := <-runErrChan:
|
||||
require.NoError(t, err, "manager failed to start")
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("manager timed out starting")
|
||||
}
|
||||
|
||||
if testCase.blockHeight != 0 {
|
||||
testContext.blockChan <- testCase.blockHeight
|
||||
}
|
||||
|
||||
select {
|
||||
case <-testContext.mockLnd.SignOutputRawChannel:
|
||||
t.Fatal("expiry sweep signed with stale deposit data")
|
||||
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
}
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case err := <-runErrChan:
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
|
||||
case <-time.After(time.Second):
|
||||
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