staticaddr: require confirmed deposits for withdraws and channel opens

Deposited can now include mempool outputs for static loop-ins, but
withdrawals and static channel opens still require confirmed funding
inputs. Filter automatic channel-open selection to confirmed deposits
and reject explicit unconfirmed selections, including withdraw-all
requests that would otherwise silently include mempool deposits.
This commit is contained in:
Slyghtning 2026-07-08 13:53:07 +02:00
parent e8dd3aa009
commit 557ba99513
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
5 changed files with 225 additions and 9 deletions

View file

@ -1805,8 +1805,9 @@ func (s *swapClientServer) WithdrawDeposits(ctx context.Context,
return nil, err
}
for _, d := range deposits {
outpoints = append(outpoints, d.OutPoint)
outpoints, err = withdrawAllDepositOutpoints(deposits)
if err != nil {
return nil, err
}
case isUtxoSelected:
@ -1829,6 +1830,25 @@ func (s *swapClientServer) WithdrawDeposits(ctx context.Context,
}, err
}
// withdrawAllDepositOutpoints returns all deposit outpoints for an `all`
// withdrawal request. The request must fail if any deposited output is still
// unconfirmed because `all` should not silently downgrade to a subset.
func withdrawAllDepositOutpoints(deposits []*deposit.Deposit) ([]wire.OutPoint,
error) {
outpoints := make([]wire.OutPoint, 0, len(deposits))
for _, d := range deposits {
if d.GetConfirmationHeight() <= 0 {
return nil, fmt.Errorf("can't withdraw all deposits while " +
"some deposits are unconfirmed")
}
outpoints = append(outpoints, d.OutPoint)
}
return outpoints, nil
}
// ListStaticAddressDeposits returns a list of all sufficiently confirmed
// deposits behind the static address and displays properties like value,
// state or blocks til expiry.

View file

@ -2,6 +2,10 @@ package loopd
import (
"testing"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/staticaddr/deposit"
)
// TestDepositBlocksUntilExpiry checks blocks-until-expiry handling for
@ -21,3 +25,62 @@ func TestDepositBlocksUntilExpiry(t *testing.T) {
}
})
}
// TestWithdrawAllDepositOutpoints checks `all` withdrawal handling for
// confirmed and unconfirmed deposits.
func TestWithdrawAllDepositOutpoints(t *testing.T) {
t.Run("rejects unconfirmed", func(t *testing.T) {
deposits := []*deposit.Deposit{
{
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{1},
Index: 1,
},
},
{
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{2},
Index: 2,
},
ConfirmationHeight: 123,
},
}
_, err := withdrawAllDepositOutpoints(deposits)
if err == nil {
t.Fatal("expected unconfirmed deposit to fail all withdrawal")
}
})
t.Run("returns all confirmed", func(t *testing.T) {
first := wire.OutPoint{
Hash: chainhash.Hash{3},
Index: 3,
}
second := wire.OutPoint{
Hash: chainhash.Hash{4},
Index: 4,
}
deposits := []*deposit.Deposit{
{
OutPoint: first,
ConfirmationHeight: 123,
},
{
OutPoint: second,
ConfirmationHeight: 124,
},
}
outpoints, err := withdrawAllDepositOutpoints(deposits)
if err != nil {
t.Fatalf("expected confirmed deposits to succeed: %v", err)
}
if len(outpoints) != 2 {
t.Fatalf("expected 2 outpoints, got %d", len(outpoints))
}
if outpoints[0] != first || outpoints[1] != second {
t.Fatal("expected all confirmed outpoints to remain selected")
}
})
}