Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ linters:
- google.golang.org/protobuf
- github.com/lightningnetwork/lnd/sqldb
- github.com/lightningnetwork/lightning-onion
# Temporary pins for configurable persisted output leases.
- github.com/btcsuite/btcwallet
- github.com/btcsuite/btcwallet/wtxmgr
replace-local: true

gosec:
Expand Down
7 changes: 7 additions & 0 deletions docs/release-notes/release-notes-0.22.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@

## RPC Additions

* WalletKit output leases can now [remain active until their spending
transaction reaches a requested confirmation
depth](https://github.com/lightningnetwork/lnd/pull/11125). The option is
available on both `LeaseOutput` and inputs selected by `FundPsbt`; a zero
depth preserves the existing wall-clock behavior.

* The `routerrpc.EstimateRouteFee` RPC now supports [restricting fee estimates
to specific first-hop outgoing
channels](https://github.com/lightningnetwork/lnd/pull/10501) via the new
Expand Down Expand Up @@ -163,6 +169,7 @@

# Contributors (Alphabetical Order)

* Andras Banki-Horvath
* bitromortac
* Boris Nagaev
* Erick Cestari
Expand Down
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
module github.com/lightningnetwork/lnd

// TODO: drop these replaces after the btcwallet and wtxmgr changes are released.
replace github.com/btcsuite/btcwallet => github.com/bhandras/btcwallet v0.11.1-0.20260826103350-2146fd732eb2

replace github.com/btcsuite/btcwallet/wtxmgr => github.com/bhandras/btcwallet/wtxmgr v1.0.1-0.20260826100206-9f3be588589b

require (
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bhandras/btcwallet v0.11.1-0.20260826103350-2146fd732eb2 h1:+m8gBc/FLsneN8BnrzYmoOJxxuVhuHrXDVK+avHoQzs=
github.com/bhandras/btcwallet v0.11.1-0.20260826103350-2146fd732eb2/go.mod h1:fk+cTe1whukIKyp+dxPV/FFF32Uu+7bV/safqh5ekcc=
github.com/bhandras/btcwallet/wtxmgr v1.0.1-0.20260826100206-9f3be588589b h1:7QxTLjx9bZNTSawL9kDKSna+qqSBjoNrnAttXfJTxf8=
github.com/bhandras/btcwallet/wtxmgr v1.0.1-0.20260826100206-9f3be588589b/go.mod h1:Raor7IBIwHSIKE9Lr5o+R9rwX7sRMHU1zjxgEQgn9h8=
github.com/btcsuite/btcd v0.26.0 h1:yntnSshlG3+H7dTwIOR4LTFXDPojVBsFORBNN5y5c/c=
github.com/btcsuite/btcd v0.26.0/go.mod h1:7ft7+a/MoJHFouFopCb1zyiR9IWPlrcPVn6K/lJ1dcA=
github.com/btcsuite/btcd/address/v2 v2.0.0 h1:UVu8Hal6Siu4XastFe+JX5JkeBYONbDUIY5E+SVTs6I=
Expand Down Expand Up @@ -57,8 +61,6 @@ github.com/btcsuite/btclog v1.0.0 h1:sEkpKJMmfGiyZjADwEIgB1NSwMyfdD1FB8v6+w1T0Ns
github.com/btcsuite/btclog v1.0.0/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ=
github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b h1:MQ+Q6sDy37V1wP1Yu79A5KqJutolqUGwA99UZWQDWZM=
github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b/go.mod h1:XItGUfVOxotJL8kkuk2Hj3EVow5KCugXl3wWfQ6K0AE=
github.com/btcsuite/btcwallet v0.18.0 h1:VSRClNLT7NX0wmJEGALz3jOZRRjWPpUdp7VI1Akie1o=
github.com/btcsuite/btcwallet v0.18.0/go.mod h1:1ZMc1EEskov+AKKv4kCMZqN8BwVh9rpXwEyxbeWy2A4=
github.com/btcsuite/btcwallet/wallet/txauthor v1.4.0 h1:oIkGj32YK1CvWaJGlVwZA1f+y/KVHkfrd2PoST0ZpQs=
github.com/btcsuite/btcwallet/wallet/txauthor v1.4.0/go.mod h1:sGrBjcqQ8UPexuRajFs72+o544CJn3Pavv/5H0VAWVk=
github.com/btcsuite/btcwallet/wallet/txrules v1.3.0 h1:D5aGMwWIxdqek3xEJs4eOdMoh6iga2EI2xSlaXCdnNo=
Expand All @@ -67,8 +69,6 @@ github.com/btcsuite/btcwallet/wallet/txsizes v1.3.0 h1:2W9qt0edMoX8crx0Wm4Cv+eAj
github.com/btcsuite/btcwallet/wallet/txsizes v1.3.0/go.mod h1:42aE6+LMZSSEisQAa15Xml25ncuJFfhCrkcpB5OmkZk=
github.com/btcsuite/btcwallet/walletdb v1.6.0 h1:Yund5XbdqFxNW7+R2Sxs02bMC5fMrmORj4GN8MV55no=
github.com/btcsuite/btcwallet/walletdb v1.6.0/go.mod h1:q9xif0Csp52GVb3l252BbHCuyiCnuEbrPWu/HAsvaYc=
github.com/btcsuite/btcwallet/wtxmgr v1.6.0 h1:ivSSnYCD4Kb5yAMZVyBA1VMYABIFcopPEcmHCrRZXcE=
github.com/btcsuite/btcwallet/wtxmgr v1.6.0/go.mod h1:Raor7IBIwHSIKE9Lr5o+R9rwX7sRMHU1zjxgEQgn9h8=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc=
Expand Down
31 changes: 26 additions & 5 deletions lnrpc/walletrpc/psbt.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ func verifyInputsUnspent(inputs []*wire.TxIn, utxos []*lnwallet.Utxo) error {
// (the passed outpoints), using either the optional custom lock ID and duration
// or the wallet's internal static lock ID with the default 10-minute duration.
func lockInputs(w lnwallet.WalletController, outpoints []wire.OutPoint,
customLockID *wtxmgr.LockID, customLockDuration time.Duration) (
customLockID *wtxmgr.LockID, customLockDuration time.Duration,
releaseAfterSpendConfs uint32) (
[]*base.ListLeasedOutputResult, error) {

locks := make(
Expand Down Expand Up @@ -74,9 +75,29 @@ func lockInputs(w lnwallet.WalletController, outpoints []wire.OutPoint,
return nil, fmt.Errorf("fetch outpoint info: %w", err)
}

expiration, err := w.LeaseOutput(
lock.LockID, lock.Outpoint, lockDuration,
)
var expiration time.Time
if releaseAfterSpendConfs > 0 {
leaser, ok := w.(lnwallet.OutputLeaserWithOptions)
if !ok {
return nil, fmt.Errorf(
"wallet does not support " +
"release-after-spend " +
"output leases",
)
}

leaseOpts := lnwallet.LeaseOutputOptions{
ReleaseAfterSpendConfs: releaseAfterSpendConfs,
}
expiration, err = leaser.LeaseOutputWithOptions(
lock.LockID, lock.Outpoint, lockDuration,
leaseOpts,
)
} else {
expiration, err = w.LeaseOutput(
lock.LockID, lock.Outpoint, lockDuration,
)
}
if err != nil {
// If we run into a problem with locking one output, we
// should try to unlock those that we successfully
Expand All @@ -85,7 +106,7 @@ func lockInputs(w lnwallet.WalletController, outpoints []wire.OutPoint,
for i := 0; i < idx; i++ {
op := locks[i].Outpoint
if err := w.ReleaseOutput(
chanfunding.LndInternalLockID, op,
locks[i].LockID, op,
); err != nil {
log.Errorf("could not release the "+
"lock on %v: %v", op, err)
Expand Down
129 changes: 129 additions & 0 deletions lnrpc/walletrpc/psbt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//go:build walletrpc
// +build walletrpc

package walletrpc

import (
"errors"
"testing"
"time"

"github.com/btcsuite/btcd/wire/v2"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/stretchr/testify/require"
)

// leaseOptionsWallet records the optional lease settings passed by lockInputs.
type leaseOptionsWallet struct {
*mock.WalletController

leaseCalls []lnwallet.LeaseOutputOptions
releasedIDs []wtxmgr.LockID
failCall int
}

// legacyLeaseWallet records calls to the original lease method but does not
// implement OutputLeaserWithOptions.
type legacyLeaseWallet struct {
*mock.WalletController

leaseCalls int
}

// LeaseOutput records any fallback to the legacy lease path.
func (w *legacyLeaseWallet) LeaseOutput(_ wtxmgr.LockID, _ wire.OutPoint,
_ time.Duration) (time.Time, error) {

w.leaseCalls++

return time.Unix(123, 0), nil
}

// LeaseOutputWithOptions records the requested behavior and optionally fails
// one call so the partial-lock rollback path can be asserted.
func (w *leaseOptionsWallet) LeaseOutputWithOptions(_ wtxmgr.LockID,
_ wire.OutPoint, _ time.Duration,
opts lnwallet.LeaseOutputOptions) (time.Time, error) {

w.leaseCalls = append(w.leaseCalls, opts)
if w.failCall > 0 && len(w.leaseCalls) == w.failCall {
return time.Time{}, errors.New("lease failed")
}

return time.Unix(123, 0), nil
}

// ReleaseOutput records the lock ID used to roll back an acquired lease.
func (w *leaseOptionsWallet) ReleaseOutput(id wtxmgr.LockID,
_ wire.OutPoint) error {

w.releasedIDs = append(w.releasedIDs, id)

return nil
}

// TestLockInputsForwardsReleaseAfterSpend verifies that FundPsbt's lease helper
// passes the requested confirmation depth to every selected input.
func TestLockInputsForwardsReleaseAfterSpend(t *testing.T) {
t.Parallel()

wallet := &leaseOptionsWallet{
WalletController: &mock.WalletController{},
}
lockID := wtxmgr.LockID{1, 2, 3}
outpoints := []wire.OutPoint{
{Index: 1},
{Index: 2},
}

locks, err := lockInputs(
wallet, outpoints, &lockID, time.Hour, 6,
)
require.NoError(t, err)
require.Len(t, locks, 2)
require.Len(t, wallet.leaseCalls, 2)
for _, opts := range wallet.leaseCalls {
require.Equal(t, uint32(6), opts.ReleaseAfterSpendConfs)
}
}

// TestLockInputsRejectsUnsupportedLeaseOptions verifies an option-bearing
// FundPsbt lease fails before falling back to a time-only wallet lease.
func TestLockInputsRejectsUnsupportedLeaseOptions(t *testing.T) {
t.Parallel()

wallet := &legacyLeaseWallet{
WalletController: &mock.WalletController{},
}

_, err := lockInputs(
wallet, []wire.OutPoint{{Index: 1}}, nil, time.Hour, 6,
)
require.ErrorContains(
t, err, "wallet does not support release-after-spend output leases",
)
require.Zero(t, wallet.leaseCalls,
"unsupported options must not create a shorter legacy lease")
}

// TestLockInputsRollbackUsesActualLockID verifies that a later lease failure
// releases earlier inputs with the ID that acquired them.
func TestLockInputsRollbackUsesActualLockID(t *testing.T) {
t.Parallel()

wallet := &leaseOptionsWallet{
WalletController: &mock.WalletController{},
failCall: 2,
}
lockID := wtxmgr.LockID{9, 8, 7}
outpoints := []wire.OutPoint{
{Index: 1},
{Index: 2},
}

_, err := lockInputs(wallet, outpoints, &lockID, time.Hour, 6)
require.ErrorContains(t, err, "lease failed")
require.Equal(t, []wtxmgr.LockID{lockID}, wallet.releasedIDs)
}
Loading