|
4 | 4 | "bytes"
|
5 | 5 | "encoding/hex"
|
6 | 6 | "testing"
|
| 7 | + "time" |
7 | 8 |
|
8 | 9 | "github.com/btcsuite/btcd/btcec/v2"
|
9 | 10 | "github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
@@ -1919,3 +1920,113 @@ func testPsbtChanFundingWithUnstableUtxos(ht *lntest.HarnessTest) {
|
1919 | 1920 | block = ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
1920 | 1921 | ht.AssertTxInBlock(block, txHash)
|
1921 | 1922 | }
|
| 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 | +} |
0 commit comments