staticaddr/deposit: sweep with owning address keys

Build timeout sweeps from each deposit own script, expiry, and key
locator. Derived-address deposits can now use their unilateral recovery
path without falling back to the legacy root parameters.
This commit is contained in:
Slyghtning 2026-07-10 14:23:13 +02:00
parent eb03aacf6c
commit fd1c651a47
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
3 changed files with 66 additions and 24 deletions

View file

@ -6,7 +6,6 @@ import (
"fmt"
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
@ -27,9 +26,15 @@ func (f *FSM) PublishDepositExpirySweepAction(ctx context.Context,
msgTx := wire.NewMsgTx(2)
params, err := f.cfg.AddressManager.GetStaticAddressParameters(ctx)
if f.deposit.AddressParams == nil {
return f.HandleError(fmt.Errorf("missing static address " +
"parameters"))
}
params := f.deposit.AddressParams
address, err := f.deposit.GetStaticAddressScript()
if err != nil {
return fsm.OnError
return f.HandleError(err)
}
// Add the deposit outpoint as input to the transaction.
@ -96,11 +101,6 @@ func (f *FSM) PublishDepositExpirySweepAction(ctx context.Context,
return f.HandleError(err)
}
address, err := f.cfg.AddressManager.GetStaticAddress(ctx)
if err != nil {
return f.HandleError(err)
}
sig := rawSigs[0]
msgTx.TxIn[0].Witness, err = address.GenTimeoutWitness(sig)
if err != nil {
@ -131,14 +131,10 @@ func (f *FSM) PublishDepositExpirySweepAction(ctx context.Context,
func (f *FSM) WaitForExpirySweepAction(ctx context.Context,
_ fsm.EventContext) fsm.EventType {
var txID *chainhash.Hash
// Only pass the txid if we know it from our own publication.
if f.deposit.ExpirySweepTxid != (chainhash.Hash{}) {
txID = &f.deposit.ExpirySweepTxid
}
// Register by script only so an RBF replacement of the timeout sweep is
// still detected after restart with a stale ExpirySweepTxid.
spendChan, errSpendChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn( //nolint:lll
ctx, txID, f.deposit.TimeOutSweepPkScript, DefaultConfTarget,
ctx, nil, f.deposit.TimeOutSweepPkScript, DefaultConfTarget,
int32(f.deposit.GetConfirmationHeight()),
)
if err != nil {

View file

@ -8,6 +8,8 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/fsm"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
@ -52,6 +54,50 @@ func TestFinalizeDepositActionDoesNotBlock(t *testing.T) {
}
}
func TestWaitForExpirySweepActionRegistersByScriptOnly(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
timeoutPkScript := []byte{0x51, 0x20, 0x01}
confChan := make(chan *chainntnfs.TxConfirmation, 1)
errChan := make(chan error, 1)
chainNotifier := &MockChainNotifier{}
chainNotifier.On(
"RegisterConfirmationsNtfn",
mock.Anything,
mock.MatchedBy(func(txid *chainhash.Hash) bool {
return txid == nil
}),
timeoutPkScript,
int32(DefaultConfTarget),
int32(42),
).Return(confChan, errChan, nil).Once()
depositFSM := &FSM{
cfg: &ManagerConfig{
ChainNotifier: chainNotifier,
},
deposit: &Deposit{
ConfirmationHeight: 42,
ExpirySweepTxid: chainhash.Hash{9},
TimeOutSweepPkScript: timeoutPkScript,
},
}
confirmedTx := wire.NewMsgTx(2)
confirmedTx.AddTxOut(&wire.TxOut{
Value: 1000,
PkScript: timeoutPkScript,
})
confChan <- &chainntnfs.TxConfirmation{Tx: confirmedTx}
event := depositFSM.WaitForExpirySweepAction(ctx, nil)
require.Equal(t, OnExpirySwept, event)
require.Equal(t, confirmedTx.TxHash(), depositFSM.deposit.ExpirySweepTxid)
chainNotifier.AssertExpectations(t)
}
// TestFinalizeDepositActionIgnoresRequestCancellation ensures the cleanup
// notification is tied to the FSM lifetime, not the caller's request context.
func TestFinalizeDepositActionIgnoresRequestCancellation(t *testing.T) {

View file

@ -181,13 +181,13 @@ func NewFSM(ctx context.Context, deposit *Deposit, cfg *ManagerConfig,
finalizedDepositChan chan wire.OutPoint,
recoverStateMachine bool) (*FSM, error) {
params, err := cfg.AddressManager.GetStaticAddressParameters(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get static address "+
"parameters: %w", err)
if deposit.AddressParams == nil {
return nil, fmt.Errorf("missing deposit static address " +
"parameters")
}
params := deposit.AddressParams
address, err := cfg.AddressManager.GetStaticAddress(ctx)
address, err := deposit.GetStaticAddressScript()
if err != nil {
return nil, fmt.Errorf("unable to get static address: %w", err)
}
@ -535,10 +535,10 @@ func (f *FSM) Errorf(format string, args ...any) {
}
// SignDescriptor returns the sign descriptor for the static address output.
func (f *FSM) SignDescriptor(ctx context.Context) (*lndclient.SignDescriptor,
func (f *FSM) SignDescriptor(_ context.Context) (*lndclient.SignDescriptor,
error) {
address, err := f.cfg.AddressManager.GetStaticAddress(ctx)
address, err := f.deposit.GetStaticAddressScript()
if err != nil {
return nil, err
}
@ -546,10 +546,10 @@ func (f *FSM) SignDescriptor(ctx context.Context) (*lndclient.SignDescriptor,
return &lndclient.SignDescriptor{
WitnessScript: address.TimeoutLeaf.Script,
KeyDesc: keychain.KeyDescriptor{
PubKey: f.params.ClientPubkey,
PubKey: f.deposit.AddressParams.ClientPubkey,
},
Output: wire.NewTxOut(
int64(f.deposit.Value), f.params.PkScript,
int64(f.deposit.Value), f.deposit.AddressParams.PkScript,
),
HashType: txscript.SigHashDefault,
InputIndex: 0,