From 8dab512981ff8dd68fc92a64f239ebc24f62a859 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Wed, 16 Apr 2025 20:43:36 +0200 Subject: [PATCH] walletrpc: make use of custom lock ID and lock duration in FundPsbt --- lnrpc/walletrpc/psbt.go | 25 ++++++++---- lnrpc/walletrpc/walletkit_server.go | 49 +++++++++++++++++++----- lnrpc/walletrpc/walletkit_server_test.go | 2 +- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/lnrpc/walletrpc/psbt.go b/lnrpc/walletrpc/psbt.go index 057751964..5d8472292 100644 --- a/lnrpc/walletrpc/psbt.go +++ b/lnrpc/walletrpc/psbt.go @@ -6,6 +6,7 @@ package walletrpc import ( "fmt" "math" + "time" "github.com/btcsuite/btcd/wire" base "github.com/btcsuite/btcwallet/wallet" @@ -40,10 +41,12 @@ func verifyInputsUnspent(inputs []*wire.TxIn, utxos []*lnwallet.Utxo) error { return nil } -// lockInputs requests a lock lease for all inputs specified in a PSBT packet -// by using the internal, static lock ID of lnd's wallet. -func lockInputs(w lnwallet.WalletController, - outpoints []wire.OutPoint) ([]*base.ListLeasedOutputResult, error) { +// lockInputs requests lock leases for all inputs specified in a PSBT packet +// (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) ( + []*base.ListLeasedOutputResult, error) { locks := make( []*base.ListLeasedOutputResult, len(outpoints), @@ -51,11 +54,20 @@ func lockInputs(w lnwallet.WalletController, for idx := range outpoints { lock := &base.ListLeasedOutputResult{ LockedOutput: &wtxmgr.LockedOutput{ - LockID: chanfunding.LndInternalLockID, Outpoint: outpoints[idx], }, } + lock.LockID = chanfunding.LndInternalLockID + if customLockID != nil { + lock.LockID = *customLockID + } + + lockDuration := chanfunding.DefaultLockDuration + if customLockDuration != 0 { + lockDuration = customLockDuration + } + // Get the details about this outpoint. utxo, err := w.FetchOutpointInfo(&lock.Outpoint) if err != nil { @@ -63,8 +75,7 @@ func lockInputs(w lnwallet.WalletController, } expiration, err := w.LeaseOutput( - lock.LockID, lock.Outpoint, - chanfunding.DefaultLockDuration, + lock.LockID, lock.Outpoint, lockDuration, ) if err != nil { // If we run into a problem with locking one output, we diff --git a/lnrpc/walletrpc/walletkit_server.go b/lnrpc/walletrpc/walletkit_server.go index c8eaccc1d..a51e3f401 100644 --- a/lnrpc/walletrpc/walletkit_server.go +++ b/lnrpc/walletrpc/walletkit_server.go @@ -1600,6 +1600,24 @@ func (w *WalletKit) FundPsbt(_ context.Context, account = req.Account } + var customLockID *wtxmgr.LockID + if len(req.CustomLockId) > 0 { + lockID := wtxmgr.LockID{} + if len(req.CustomLockId) != len(lockID) { + return nil, fmt.Errorf("custom lock ID must be " + + "exactly 32 bytes") + } + + copy(lockID[:], req.CustomLockId) + customLockID = &lockID + } + + var customLockDuration time.Duration + if req.LockExpirationSeconds != 0 { + customLockDuration = time.Duration(req.LockExpirationSeconds) * + time.Second + } + // There are three ways a user can specify what we call the template (a // list of inputs and outputs to use in the PSBT): Either as a PSBT // packet directly with no coin selection, a PSBT with coin selection or @@ -1619,6 +1637,7 @@ func (w *WalletKit) FundPsbt(_ context.Context, return w.fundPsbtInternalWallet( account, keyScopeFromChangeAddressType(req.ChangeType), packet, minConfs, feeSatPerKW, coinSelectionStrategy, + customLockID, customLockDuration, ) // The template is specified as a PSBT with the intention to perform @@ -1701,6 +1720,7 @@ func (w *WalletKit) FundPsbt(_ context.Context, return w.fundPsbtCoinSelect( account, changeIndex, packet, minConfs, changeType, feeSatPerKW, coinSelectionStrategy, maxFeeRatio, + customLockID, customLockDuration, ) // The template is specified as a RPC message. We need to create a new @@ -1758,6 +1778,7 @@ func (w *WalletKit) FundPsbt(_ context.Context, return w.fundPsbtInternalWallet( account, keyScopeFromChangeAddressType(req.ChangeType), packet, minConfs, feeSatPerKW, coinSelectionStrategy, + customLockID, customLockDuration, ) default: @@ -1770,8 +1791,9 @@ func (w *WalletKit) FundPsbt(_ context.Context, // wallet that does not allow specifying custom inputs while selecting coins. func (w *WalletKit) fundPsbtInternalWallet(account string, keyScope *waddrmgr.KeyScope, packet *psbt.Packet, minConfs int32, - feeSatPerKW chainfee.SatPerKWeight, - strategy base.CoinSelectionStrategy) (*FundPsbtResponse, error) { + feeSatPerKW chainfee.SatPerKWeight, strategy base.CoinSelectionStrategy, + customLockID *wtxmgr.LockID, customLockDuration time.Duration) ( + *FundPsbtResponse, error) { // The RPC parsing part is now over. Several of the following operations // require us to hold the global coin selection lock, so we do the rest @@ -1885,7 +1907,8 @@ func (w *WalletKit) fundPsbtInternalWallet(account string, } response, err = w.lockAndCreateFundingResponse( - packet, outpoints, changeIndex, + packet, outpoints, changeIndex, customLockID, + customLockDuration, ) return err @@ -1904,7 +1927,8 @@ func (w *WalletKit) fundPsbtCoinSelect(account string, changeIndex int32, packet *psbt.Packet, minConfs int32, changeType chanfunding.ChangeAddressType, feeRate chainfee.SatPerKWeight, strategy base.CoinSelectionStrategy, - maxFeeRatio float64) (*FundPsbtResponse, error) { + maxFeeRatio float64, customLockID *wtxmgr.LockID, + customLockDuration time.Duration) (*FundPsbtResponse, error) { // We want to make sure we don't select any inputs that are already // specified in the template. To do that, we require those inputs to @@ -2019,7 +2043,10 @@ func (w *WalletKit) fundPsbtCoinSelect(account string, changeIndex int32, } // We're done. Let's serialize and return the updated package. - return w.lockAndCreateFundingResponse(packet, nil, changeIndex) + return w.lockAndCreateFundingResponse( + packet, nil, changeIndex, customLockID, + customLockDuration, + ) } // The RPC parsing part is now over. Several of the following operations @@ -2091,7 +2118,8 @@ func (w *WalletKit) fundPsbtCoinSelect(account string, changeIndex int32, } response, err = w.lockAndCreateFundingResponse( - packet, addedOutpoints, changeIndex, + packet, addedOutpoints, changeIndex, customLockID, + customLockDuration, ) return err @@ -2136,8 +2164,9 @@ func (w *WalletKit) assertNotAvailable(inputs []*wire.TxIn, minConfs int32, // lockAndCreateFundingResponse locks the given outpoints and creates a funding // response with the serialized PSBT, the change index and the locked UTXOs. func (w *WalletKit) lockAndCreateFundingResponse(packet *psbt.Packet, - newOutpoints []wire.OutPoint, changeIndex int32) (*FundPsbtResponse, - error) { + newOutpoints []wire.OutPoint, changeIndex int32, + customLockID *wtxmgr.LockID, customLockDuration time.Duration) ( + *FundPsbtResponse, error) { // Make sure we can properly serialize the packet. If this goes wrong // then something isn't right with the inputs, and we probably shouldn't @@ -2148,7 +2177,9 @@ func (w *WalletKit) lockAndCreateFundingResponse(packet *psbt.Packet, return nil, fmt.Errorf("error serializing funded PSBT: %w", err) } - locks, err := lockInputs(w.cfg.Wallet, newOutpoints) + locks, err := lockInputs( + w.cfg.Wallet, newOutpoints, customLockID, customLockDuration, + ) if err != nil { return nil, fmt.Errorf("could not lock inputs: %w", err) } diff --git a/lnrpc/walletrpc/walletkit_server_test.go b/lnrpc/walletrpc/walletkit_server_test.go index d567eb028..40fb62ef2 100644 --- a/lnrpc/walletrpc/walletkit_server_test.go +++ b/lnrpc/walletrpc/walletkit_server_test.go @@ -660,7 +660,7 @@ func TestFundPsbtCoinSelect(t *testing.T) { "", tc.changeIndex, copiedPacket, 0, tc.changeType, tc.feeRate, rpcServer.cfg.CoinSelectionStrategy, - tc.maxFeeRatio, + tc.maxFeeRatio, nil, 0, ) switch {