Skip to content

Commit 579f6f0

Browse files
authored
Merge pull request lightningnetwork#9724 from bhandras/fundpsbt-custom-input-lock
walletrpc: allow custom lock ID and duration in `FundPsbt`
2 parents 337d9a9 + e86bea3 commit 579f6f0

File tree

11 files changed

+545
-319
lines changed

11 files changed

+545
-319
lines changed

docs/release-notes/release-notes-0.19.0.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,9 @@ close transaction.
302302
`lnrpc.HTLC`. This field is used to indicate whether a given HTLC has been
303303
locked in by the remote peer.
304304

305+
* [Allow custom lock ID and
306+
duration in FundPsbt](https://github.com/lightningnetwork/lnd/pull/9724) RPC.
307+
305308
## lncli Updates
306309

307310
* [Fixed](https://github.com/lightningnetwork/lnd/pull/9605) a case where

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,10 @@ var allTestCases = []*lntest.TestCase{
273273
Name: "fund psbt",
274274
TestFunc: testFundPsbt,
275275
},
276+
{
277+
Name: "fund psbt custom lock",
278+
TestFunc: testFundPsbtCustomLock,
279+
},
276280
{
277281
Name: "resolution handoff",
278282
TestFunc: testResHandoff,

itest/lnd_psbt_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/hex"
66
"testing"
7+
"time"
78

89
"github.com/btcsuite/btcd/btcec/v2"
910
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
@@ -1919,3 +1920,113 @@ func testPsbtChanFundingWithUnstableUtxos(ht *lntest.HarnessTest) {
19191920
block = ht.MineBlocksAndAssertNumTxes(1, 1)[0]
19201921
ht.AssertTxInBlock(block, txHash)
19211922
}
1923+
1924+
// testFundPsbtCustomLock verifies that FundPsbt correctly locks inputs
1925+
// using a custom lock ID and expiration time.
1926+
func testFundPsbtCustomLock(ht *lntest.HarnessTest) {
1927+
alice := ht.NewNodeWithCoins("Alice", nil)
1928+
1929+
// Define a custom lock ID and a short expiration for testing.
1930+
customLockID := ht.Random32Bytes()
1931+
lockDurationSeconds := uint64(30)
1932+
1933+
ht.Logf("Using custom lock ID: %x with expiration: %d seconds",
1934+
customLockID, lockDurationSeconds)
1935+
1936+
// Generate an address for the output.
1937+
aliceAddr := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{
1938+
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
1939+
})
1940+
outputs := map[string]uint64{
1941+
aliceAddr.Address: 100_000,
1942+
}
1943+
1944+
// Build the FundPsbt request using custom lock parameters.
1945+
req := &walletrpc.FundPsbtRequest{
1946+
Template: &walletrpc.FundPsbtRequest_Raw{
1947+
Raw: &walletrpc.TxTemplate{Outputs: outputs},
1948+
},
1949+
Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{
1950+
SatPerVbyte: 2,
1951+
},
1952+
MinConfs: 1,
1953+
CustomLockId: customLockID,
1954+
LockExpirationSeconds: lockDurationSeconds,
1955+
}
1956+
1957+
// Capture the current time for later expiration validation.
1958+
callTime := time.Now()
1959+
1960+
// Execute the FundPsbt call and validate the response.
1961+
fundResp := alice.RPC.FundPsbt(req)
1962+
require.NotEmpty(ht, fundResp.FundedPsbt)
1963+
1964+
// Ensure the response includes at least one locked UTXO.
1965+
require.GreaterOrEqual(ht, len(fundResp.LockedUtxos), 1)
1966+
1967+
// Parse the PSBT and map locked outpoints for quick lookup.
1968+
fundedPacket, err := psbt.NewFromRawBytes(
1969+
bytes.NewReader(fundResp.FundedPsbt), false,
1970+
)
1971+
require.NoError(ht, err)
1972+
1973+
lockedOutpointsMap := make(map[string]struct{})
1974+
for _, utxo := range fundResp.LockedUtxos {
1975+
lockedOutpointsMap[lntest.LnrpcOutpointToStr(utxo.Outpoint)] =
1976+
struct{}{}
1977+
}
1978+
1979+
// Check that all PSBT inputs are among the locked UTXOs.
1980+
require.Len(ht, fundedPacket.UnsignedTx.TxIn, len(lockedOutpointsMap))
1981+
for _, txIn := range fundedPacket.UnsignedTx.TxIn {
1982+
_, ok := lockedOutpointsMap[txIn.PreviousOutPoint.String()]
1983+
require.True(
1984+
ht, ok, "Missing locked input: %v",
1985+
txIn.PreviousOutPoint,
1986+
)
1987+
}
1988+
1989+
// Verify leases via ListLeases call.
1990+
ht.Logf("Verifying leases via ListLeases...")
1991+
leasesResp := alice.RPC.ListLeases()
1992+
require.NoError(ht, err)
1993+
require.Len(ht, leasesResp.LockedUtxos, len(lockedOutpointsMap))
1994+
1995+
for _, lease := range leasesResp.LockedUtxos {
1996+
// Validate that the lease matches our locked UTXOs.
1997+
require.Contains(
1998+
ht, lockedOutpointsMap,
1999+
lntest.LnrpcOutpointToStr(lease.Outpoint),
2000+
)
2001+
2002+
// Confirm lock ID and expiration.
2003+
require.EqualValues(ht, customLockID, lease.Id)
2004+
2005+
expectedExpiration := callTime.Unix() +
2006+
int64(lockDurationSeconds)
2007+
2008+
// Validate that the expiration time is within a small delta (5
2009+
// seconds) of the expected value. This accounts for any latency
2010+
// in the RPC call or processing time (to avoid flakes in CI).
2011+
const leaseExpirationDelta = 5.0
2012+
require.InDelta(
2013+
ht, expectedExpiration, lease.Expiration,
2014+
leaseExpirationDelta,
2015+
)
2016+
}
2017+
2018+
// We use this extra wait time to ensure the lock is released after the
2019+
// expiration time.
2020+
const extraWaitSeconds = 2
2021+
2022+
// Wait for the lock to expire, then confirm it's released.
2023+
waitDuration := time.Duration(
2024+
lockDurationSeconds+extraWaitSeconds,
2025+
) * time.Second
2026+
ht.Logf("Waiting %v for lock to expire...", waitDuration)
2027+
time.Sleep(waitDuration)
2028+
2029+
ht.Logf("Verifying lease expiration...")
2030+
leasesRespAfter := alice.RPC.ListLeases()
2031+
require.Empty(ht, leasesRespAfter.LockedUtxos)
2032+
}

lnrpc/walletrpc/psbt.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package walletrpc
66
import (
77
"fmt"
88
"math"
9+
"time"
910

1011
"github.com/btcsuite/btcd/wire"
1112
base "github.com/btcsuite/btcwallet/wallet"
@@ -40,31 +41,41 @@ func verifyInputsUnspent(inputs []*wire.TxIn, utxos []*lnwallet.Utxo) error {
4041
return nil
4142
}
4243

43-
// lockInputs requests a lock lease for all inputs specified in a PSBT packet
44-
// by using the internal, static lock ID of lnd's wallet.
45-
func lockInputs(w lnwallet.WalletController,
46-
outpoints []wire.OutPoint) ([]*base.ListLeasedOutputResult, error) {
44+
// lockInputs requests lock leases for all inputs specified in a PSBT packet
45+
// (the passed outpoints), using either the optional custom lock ID and duration
46+
// or the wallet's internal static lock ID with the default 10-minute duration.
47+
func lockInputs(w lnwallet.WalletController, outpoints []wire.OutPoint,
48+
customLockID *wtxmgr.LockID, customLockDuration time.Duration) (
49+
[]*base.ListLeasedOutputResult, error) {
4750

4851
locks := make(
4952
[]*base.ListLeasedOutputResult, len(outpoints),
5053
)
5154
for idx := range outpoints {
5255
lock := &base.ListLeasedOutputResult{
5356
LockedOutput: &wtxmgr.LockedOutput{
54-
LockID: chanfunding.LndInternalLockID,
5557
Outpoint: outpoints[idx],
5658
},
5759
}
5860

61+
lock.LockID = chanfunding.LndInternalLockID
62+
if customLockID != nil {
63+
lock.LockID = *customLockID
64+
}
65+
66+
lockDuration := chanfunding.DefaultLockDuration
67+
if customLockDuration != 0 {
68+
lockDuration = customLockDuration
69+
}
70+
5971
// Get the details about this outpoint.
6072
utxo, err := w.FetchOutpointInfo(&lock.Outpoint)
6173
if err != nil {
6274
return nil, fmt.Errorf("fetch outpoint info: %w", err)
6375
}
6476

6577
expiration, err := w.LeaseOutput(
66-
lock.LockID, lock.Outpoint,
67-
chanfunding.DefaultLockDuration,
78+
lock.LockID, lock.Outpoint, lockDuration,
6879
)
6980
if err != nil {
7081
// If we run into a problem with locking one output, we

0 commit comments

Comments
 (0)