mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
instantout: recheck reservation timing during recovery
Refresh the chain height when a swap resumes, verify both the reservation and HTLC windows, and select the HTLC path when either remaining window is too short.
This commit is contained in:
parent
b689e361c1
commit
b596eabab3
3 changed files with 125 additions and 1 deletions
|
|
@ -51,6 +51,10 @@ const (
|
|||
// htlcExpiryDelta is the delta in blocks we require between the htlc
|
||||
// expiry and reservation expiry.
|
||||
htlcExpiryDelta = int32(40)
|
||||
|
||||
// htlcRecoverySafetyDelta leaves one urgent confirmation target for
|
||||
// the HTLC and another for its preimage sweep after recovery.
|
||||
htlcRecoverySafetyDelta = 2 * urgentConfTarget
|
||||
)
|
||||
|
||||
// InitInstantOutCtx contains the context for the InitInstantOutAction.
|
||||
|
|
@ -63,6 +67,9 @@ type InitInstantOutCtx struct {
|
|||
sweepAddress btcutil.Address
|
||||
}
|
||||
|
||||
// RecoverInstantOutCtx marks an action as being resumed after restart.
|
||||
type RecoverInstantOutCtx struct{}
|
||||
|
||||
// InitInstantOutAction is the first action that is executed when the instant
|
||||
// out FSM is started. It will send the instant out request to the server.
|
||||
func (f *FSM) InitInstantOutAction(ctx context.Context,
|
||||
|
|
@ -382,6 +389,46 @@ func (f *FSM) BuildHTLCAction(ctx context.Context,
|
|||
func (f *FSM) PushPreimageAction(ctx context.Context,
|
||||
eventCtx fsm.EventContext) fsm.EventType {
|
||||
|
||||
// A recovered swap may have been offline long enough that the server's
|
||||
// reservation timeout is now close. Fall back to the already finalized
|
||||
// HTLC instead of revealing the preimage without enough time to publish
|
||||
// that safety transaction.
|
||||
if _, ok := eventCtx.(*RecoverInstantOutCtx); ok {
|
||||
info, err := f.cfg.LndClient.GetInfo(ctx)
|
||||
if err != nil {
|
||||
f.LastActionError = fmt.Errorf(
|
||||
"unable to get recovery chain height: %w", err,
|
||||
)
|
||||
|
||||
return OnErrorPublishHtlc
|
||||
}
|
||||
|
||||
currentHeight := int64(info.BlockHeight)
|
||||
minHtlcExpiry := currentHeight +
|
||||
int64(htlcRecoverySafetyDelta)
|
||||
if int64(f.InstantOut.CltvExpiry) < minHtlcExpiry {
|
||||
f.LastActionError = fmt.Errorf("instant out HTLC expires at "+
|
||||
"height %d, before recovery safety height %d",
|
||||
f.InstantOut.CltvExpiry, minHtlcExpiry)
|
||||
|
||||
return OnErrorPublishHtlc
|
||||
}
|
||||
|
||||
minReservationExpiry := currentHeight +
|
||||
int64(htlcExpiryDelta)
|
||||
for _, res := range f.InstantOut.Reservations {
|
||||
if int64(res.Expiry) >= minReservationExpiry {
|
||||
continue
|
||||
}
|
||||
|
||||
f.LastActionError = fmt.Errorf("reservation %x expires at "+
|
||||
"height %d, before recovery safety height %d",
|
||||
res.ID, res.Expiry, minReservationExpiry)
|
||||
|
||||
return OnErrorPublishHtlc
|
||||
}
|
||||
}
|
||||
|
||||
// First we'll create the musig2 context.
|
||||
coopSessions, coopClientNonces, err := f.InstantOut.createMusig2Session(
|
||||
ctx, f.cfg.Signer,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/instantout/reservation"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -29,6 +30,23 @@ type cleanupTrackingSigner struct {
|
|||
cleaned [][32]byte
|
||||
}
|
||||
|
||||
type fixedHeightLightningClient struct {
|
||||
lndclient.LightningClient
|
||||
|
||||
height uint32
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *fixedHeightLightningClient) GetInfo(context.Context) (
|
||||
*lndclient.Info, error) {
|
||||
|
||||
if c.err != nil {
|
||||
return nil, c.err
|
||||
}
|
||||
|
||||
return &lndclient.Info{BlockHeight: c.height}, nil
|
||||
}
|
||||
|
||||
func (s *cleanupTrackingSigner) MuSig2Cleanup(_ context.Context,
|
||||
sessionID [32]byte) error {
|
||||
|
||||
|
|
@ -113,3 +131,61 @@ func TestCleanupMuSig2Sessions(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.Equal(t, [][32]byte{firstID, secondID}, signer.cleaned)
|
||||
}
|
||||
|
||||
// TestPushPreimageRejectsExpiringReservation verifies that recovery takes the
|
||||
// on-chain fallback before revealing the preimage when a reservation is too
|
||||
// close to its server-controlled timeout.
|
||||
func TestPushPreimageRejectsExpiringReservation(t *testing.T) {
|
||||
instantOutFSM := &FSM{
|
||||
StateMachine: &fsm.StateMachine{},
|
||||
cfg: &Config{
|
||||
LndClient: &fixedHeightLightningClient{height: 100},
|
||||
},
|
||||
InstantOut: &InstantOut{
|
||||
CltvExpiry: 200,
|
||||
Reservations: []*reservation.Reservation{
|
||||
{
|
||||
ID: reservation.ID{1},
|
||||
Expiry: 139,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
event := instantOutFSM.PushPreimageAction(
|
||||
t.Context(), &RecoverInstantOutCtx{},
|
||||
)
|
||||
require.Equal(t, OnErrorPublishHtlc, event)
|
||||
require.ErrorContains(
|
||||
t, instantOutFSM.LastActionError, "before recovery safety height",
|
||||
)
|
||||
}
|
||||
|
||||
// TestPushPreimageRejectsExpiringHtlc verifies that recovery uses a fresh
|
||||
// chain height and leaves time to confirm both fallback transactions.
|
||||
func TestPushPreimageRejectsExpiringHtlc(t *testing.T) {
|
||||
instantOutFSM := &FSM{
|
||||
StateMachine: &fsm.StateMachine{},
|
||||
cfg: &Config{
|
||||
LndClient: &fixedHeightLightningClient{height: 100},
|
||||
},
|
||||
InstantOut: &InstantOut{
|
||||
CltvExpiry: 105,
|
||||
Reservations: []*reservation.Reservation{
|
||||
{
|
||||
ID: reservation.ID{1},
|
||||
Expiry: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
event := instantOutFSM.PushPreimageAction(
|
||||
t.Context(), &RecoverInstantOutCtx{},
|
||||
)
|
||||
require.Equal(t, OnErrorPublishHtlc, event)
|
||||
require.ErrorContains(
|
||||
t, instantOutFSM.LastActionError,
|
||||
"instant out HTLC expires at height 105",
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,8 +119,9 @@ func (m *Manager) recoverInstantOuts(ctx context.Context) error {
|
|||
|
||||
// As SendEvent can block, we'll start a goroutine to process
|
||||
// the event.
|
||||
recoverCtx := &RecoverInstantOutCtx{}
|
||||
go func() {
|
||||
err := instantOutFSM.SendEvent(ctx, OnRecover, nil)
|
||||
err := instantOutFSM.SendEvent(ctx, OnRecover, recoverCtx)
|
||||
if err != nil {
|
||||
log.Errorf("FSM %v Error sending recover "+
|
||||
"event %v, state: %v",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue