diff --git a/go.mod b/go.mod index c65fa3281..0750f833d 100644 --- a/go.mod +++ b/go.mod @@ -28,8 +28,8 @@ require ( github.com/lightninglabs/pool v0.6.5-beta.0.20250305125211-4e860ec4e77f github.com/lightninglabs/pool/auctioneerrpc v1.1.3-0.20250305125211-4e860ec4e77f github.com/lightninglabs/pool/poolrpc v1.0.1-0.20250305125211-4e860ec4e77f - github.com/lightninglabs/taproot-assets v0.5.2-0.20250326140136-a724d385e7ae - github.com/lightningnetwork/lnd v0.19.0-beta.rc1 + github.com/lightninglabs/taproot-assets v0.5.2-0.20250401150538-a9ea76a9ed3c + github.com/lightningnetwork/lnd v0.19.0-beta.rc1.0.20250327183348-eb822a5e117f github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/clock v1.1.1 github.com/lightningnetwork/lnd/fn v1.2.3 diff --git a/go.sum b/go.sum index 7e64ef17b..e287d2cbb 100644 --- a/go.sum +++ b/go.sum @@ -1181,12 +1181,12 @@ github.com/lightninglabs/pool/poolrpc v1.0.1-0.20250305125211-4e860ec4e77f h1:5p github.com/lightninglabs/pool/poolrpc v1.0.1-0.20250305125211-4e860ec4e77f/go.mod h1:lGs2hSVZ+GFpdv3btaIl9icG5/gz7BBRfvmD2iqqNl0= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9Z6CpKxl13mS48idsu6F+cEZf0lkyiV+Dq9g= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -github.com/lightninglabs/taproot-assets v0.5.2-0.20250326140136-a724d385e7ae h1:t2GDmnV/ab+dsaTDjCDTGbCVSMCE13pxc6zu4zQFsJs= -github.com/lightninglabs/taproot-assets v0.5.2-0.20250326140136-a724d385e7ae/go.mod h1:hLK/spdccubmDZjufTqGJrj9mn0hQpOxaJBQ767Idxw= +github.com/lightninglabs/taproot-assets v0.5.2-0.20250401150538-a9ea76a9ed3c h1:Rebx5DVZx3u327vKRrueFjZNlei1RzdGzFmOZmenkiQ= +github.com/lightninglabs/taproot-assets v0.5.2-0.20250401150538-a9ea76a9ed3c/go.mod h1:e3SjXbbi4xKhOzq54c672Z/j9UTRq5DLJGx/URgVTJo= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= -github.com/lightningnetwork/lnd v0.19.0-beta.rc1 h1:FJvsdw4PZ41ykrHi7vNGit9IIohE+IlKpVwL5/1+L+0= -github.com/lightningnetwork/lnd v0.19.0-beta.rc1/go.mod h1:BP+neeFpmeAA7o5hu3zp3FwOEl26idSyPV9zBOavp6E= +github.com/lightningnetwork/lnd v0.19.0-beta.rc1.0.20250327183348-eb822a5e117f h1:+Bejv2Ij/ryUjLacBd5au0acMH0AYs0lhb7ki5rx9ms= +github.com/lightningnetwork/lnd v0.19.0-beta.rc1.0.20250327183348-eb822a5e117f/go.mod h1:BP+neeFpmeAA7o5hu3zp3FwOEl26idSyPV9zBOavp6E= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= diff --git a/itest/assets_test.go b/itest/assets_test.go index 2b3776a06..435c5f83a 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -473,9 +473,9 @@ func assertPendingChannels(t *testing.T, node *HarnessNode, pendingChan.Channel.CustomChannelData, &pendingJSON, ) require.NoError(t, err) - require.Len(t, pendingJSON.Assets, 1) + require.Len(t, pendingJSON.FundingAssets, 1) - require.NotZero(t, pendingJSON.Assets[0].Capacity) + require.NotZero(t, pendingJSON.Capacity) // Check the decimal display of the channel funding blob. If no explicit // value was set, we assume and expect the value of 0. @@ -488,7 +488,7 @@ func assertPendingChannels(t *testing.T, node *HarnessNode, require.Equal( t, expectedDecimalDisplay, - pendingJSON.Assets[0].AssetInfo.DecimalDisplay, + pendingJSON.FundingAssets[0].DecimalDisplay, ) // Check the balance of the pending channel. @@ -501,21 +501,36 @@ func assertPendingChannels(t *testing.T, node *HarnessNode, require.EqualValues(t, remoteSum, pendingRemoteBalance) } +// haveFundingAsset returns true if the given channel has the asset with the +// given asset ID as a funding asset. +func haveFundingAsset(assetChannel *rfqmsg.JsonAssetChannel, + assetID []byte) bool { + + assetIDStr := hex.EncodeToString(assetID) + for _, fundingAsset := range assetChannel.FundingAssets { + if fundingAsset.AssetGenesis.AssetID == assetIDStr { + return true + } + } + + return false +} + func assertAssetChan(t *testing.T, src, dst *HarnessNode, fundingAmount uint64, mintedAsset *taprpc.Asset) { - assetID := mintedAsset.AssetGenesis.AssetId - assetIDStr := hex.EncodeToString(assetID) err := wait.NoError(func() error { a, err := getChannelCustomData(src, dst) if err != nil { return err } - if a.AssetInfo.AssetGenesis.AssetID != assetIDStr { - return fmt.Errorf("expected asset ID %s, got %s", - assetIDStr, a.AssetInfo.AssetGenesis.AssetID) + assetID := mintedAsset.AssetGenesis.AssetId + if !haveFundingAsset(a, assetID) { + return fmt.Errorf("expected asset ID %x, to "+ + "be in channel", assetID) } + if a.Capacity != fundingAmount { return fmt.Errorf("expected capacity %d, got %d", fundingAmount, a.Capacity) @@ -530,10 +545,10 @@ func assertAssetChan(t *testing.T, src, dst *HarnessNode, fundingAmount uint64, ) } - if a.AssetInfo.DecimalDisplay != expectedDecimalDisplay { + if a.FundingAssets[0].DecimalDisplay != expectedDecimalDisplay { return fmt.Errorf("expected decimal display %d, got %d", expectedDecimalDisplay, - a.AssetInfo.DecimalDisplay) + a.FundingAssets[0].DecimalDisplay) } return nil @@ -580,7 +595,7 @@ func assertChannelKnown(t *testing.T, node *HarnessNode, require.NoError(t, err) } -func getChannelCustomData(src, dst *HarnessNode) (*rfqmsg.JsonAssetChanInfo, +func getChannelCustomData(src, dst *HarnessNode) (*rfqmsg.JsonAssetChannel, error) { ctxb := context.Background() @@ -614,12 +629,12 @@ func getChannelCustomData(src, dst *HarnessNode) (*rfqmsg.JsonAssetChanInfo, err) } - if len(assetData.Assets) != 1 { + if len(assetData.FundingAssets) != 1 { return nil, fmt.Errorf("expected 1 asset, got %d", - len(assetData.Assets)) + len(assetData.FundingAssets)) } - return &assetData.Assets[0], nil + return &assetData, nil } func getAssetChannelBalance(t *testing.T, node *HarnessNode, assetID []byte, @@ -706,10 +721,10 @@ func assertChannelAssetBalance(t *testing.T, node *HarnessNode, err := json.Unmarshal(targetChan.CustomChannelData, &assetBalance) require.NoError(t, err) - require.Len(t, assetBalance.Assets, 1) + require.Len(t, assetBalance.FundingAssets, 1) - require.InDelta(t, local, assetBalance.Assets[0].LocalBalance, 1) - require.InDelta(t, remote, assetBalance.Assets[0].RemoteBalance, 1) + require.InDelta(t, local, assetBalance.LocalBalance, 1) + require.InDelta(t, remote, assetBalance.RemoteBalance, 1) } // addRoutingFee adds the default routing fee (1 part per million fee rate plus @@ -1385,6 +1400,9 @@ func closeAssetChannelAndAssert(t *harnessTest, net *NetworkHarness, ) require.NoError(t.t, err) + assertWaitingCloseChannelAssetData(t.t, local, chanPoint) + assertWaitingCloseChannelAssetData(t.t, remote, chanPoint) + mineBlocks(t, net, 1, 1) closeUpdate, err := t.lndHarness.WaitForChannelClose(closeStream) @@ -1405,6 +1423,9 @@ func closeAssetChannelAndAssert(t *harnessTest, net *NetworkHarness, t.t, local, remote, closeTx, closeUpdate, assetID, groupKey, universeTap, ) + + assertClosedChannelAssetData(t.t, local, chanPoint) + assertClosedChannelAssetData(t.t, remote, chanPoint) } // assertDefaultCoOpCloseBalance returns a default implementation of the co-op @@ -2128,8 +2149,8 @@ func newCloseExpiryInfo(t *testing.T, node *HarnessNode) forceCloseExpiryInfo { csvDelay: mainChan.CsvDelay, currentHeight: nodeInfo.BlockHeight, cltvDelays: cltvs, - localAssetBalance: assetData.Assets[0].LocalBalance, - remoteAssetBalance: assetData.Assets[0].RemoteBalance, + localAssetBalance: assetData.LocalBalance, + remoteAssetBalance: assetData.RemoteBalance, t: t, node: node, } @@ -2200,3 +2221,143 @@ func assertInvoiceState(t *testing.T, hn *HarnessNode, payAddr []byte, }, defaultTimeout) require.NoError(t, err, "timeout waiting for invoice settled state") } + +type pendingChan = lnrpc.PendingChannelsResponse_PendingChannel + +// assertWaitingCloseChannelAssetData asserts that the waiting close channel has +// the expected asset data. +func assertPendingChannelAssetData(t *testing.T, node *HarnessNode, + chanPoint *lnrpc.ChannelPoint, find func(string, + *lnrpc.PendingChannelsResponse) (*pendingChan, error)) { + + ctxb := context.Background() + + err := wait.NoError(func() error { + // Make sure we can find the closed channel in the channel + // database. + pendingChannels, err := node.PendingChannels( + ctxb, &lnrpc.PendingChannelsRequest{}, + ) + if err != nil { + return err + } + + targetChanPointStr := fmt.Sprintf("%v:%v", + chanPoint.GetFundingTxidStr(), + chanPoint.GetOutputIndex()) + + targetChan, err := find(targetChanPointStr, pendingChannels) + if err != nil { + return err + } + + if len(targetChan.CustomChannelData) == 0 { + return fmt.Errorf("pending channel %s has no "+ + "custom channel data", targetChanPointStr) + } + + var closeData rfqmsg.JsonAssetChannel + err = json.Unmarshal(targetChan.CustomChannelData, &closeData) + if err != nil { + return fmt.Errorf("error unmarshalling custom channel "+ + "data: %v", err) + } + + if len(closeData.FundingAssets) != 1 { + return fmt.Errorf("expected 1 funding asset, got %d", + len(closeData.FundingAssets)) + } + + return nil + }, defaultTimeout) + require.NoError(t, err, "timeout waiting for pending channel") +} + +// assertPendingForceCloseChannelAssetData asserts that the pending force close +// channel has the expected asset data. +func assertPendingForceCloseChannelAssetData(t *testing.T, node *HarnessNode, + chanPoint *lnrpc.ChannelPoint) { + + assertPendingChannelAssetData( + t, node, chanPoint, func(chanPoint string, + resp *lnrpc.PendingChannelsResponse) (*pendingChan, + error) { + + if len(resp.PendingForceClosingChannels) == 0 { + return nil, fmt.Errorf("no pending force close " + + "channels found") + } + + for _, ch := range resp.PendingForceClosingChannels { + if ch.Channel.ChannelPoint == chanPoint { + return ch.Channel, nil + } + } + + return nil, fmt.Errorf("pending channel %s not found", + chanPoint) + }, + ) +} + +// assertWaitingCloseChannelAssetData asserts that the waiting close channel has +// the expected asset data. +func assertWaitingCloseChannelAssetData(t *testing.T, node *HarnessNode, + chanPoint *lnrpc.ChannelPoint) { + + assertPendingChannelAssetData( + t, node, chanPoint, func(chanPoint string, + resp *lnrpc.PendingChannelsResponse) (*pendingChan, + error) { + + if len(resp.WaitingCloseChannels) == 0 { + return nil, fmt.Errorf("no waiting close " + + "channels found") + } + + for _, ch := range resp.WaitingCloseChannels { + if ch.Channel.ChannelPoint == chanPoint { + return ch.Channel, nil + } + } + + return nil, fmt.Errorf("pending channel %s not found", + chanPoint) + }, + ) +} + +// assertClosedChannelAssetData asserts that the closed channel has the expected +// asset data. +func assertClosedChannelAssetData(t *testing.T, node *HarnessNode, + chanPoint *lnrpc.ChannelPoint) { + + ctxb := context.Background() + + // Make sure we can find the closed channel in the channel database. + closedChannels, err := node.ClosedChannels( + ctxb, &lnrpc.ClosedChannelsRequest{}, + ) + require.NoError(t, err) + + require.NotEmpty(t, closedChannels.Channels) + + targetChanPointStr := fmt.Sprintf("%v:%v", + chanPoint.GetFundingTxidStr(), chanPoint.GetOutputIndex()) + + var closedChan *lnrpc.ChannelCloseSummary + for _, ch := range closedChannels.Channels { + if ch.ChannelPoint == targetChanPointStr { + closedChan = ch + break + } + } + require.NotNil(t, closedChan) + require.NotEmpty(t, closedChan.CustomChannelData) + + var closeData rfqmsg.JsonAssetChannel + err = json.Unmarshal(closedChan.CustomChannelData, &closeData) + require.NoError(t, err) + + require.GreaterOrEqual(t, len(closeData.FundingAssets), 1) +} diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index 5f506a7fa..5c49f91d2 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -3,6 +3,7 @@ package itest import ( "bytes" "context" + "encoding/hex" "fmt" "math" "math/big" @@ -12,6 +13,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/itest" "github.com/lightninglabs/taproot-assets/proof" @@ -3096,6 +3098,14 @@ func testCustomChannelsHtlcForceClose(ctxb context.Context, net *NetworkHarness, t *harnessTest) { runCustomChannelsHtlcForceClose(ctxb, t, net, false) +} + +// testCustomChannelsHtlcForceCloseMpp tests that we can force close a channel +// with HTLCs in both directions and that the HTLC outputs are correctly +// swept, using MPP. +func testCustomChannelsHtlcForceCloseMpp(ctxb context.Context, + net *NetworkHarness, t *harnessTest) { + runCustomChannelsHtlcForceClose(ctxb, t, net, true) } @@ -3253,10 +3263,11 @@ func runCustomChannelsHtlcForceClose(ctx context.Context, t *harnessTest, // At this point, both sides should have 4 (or +4 with MPP) HTLCs // active. numHtlcs := 4 + numAdditionalShards := assetInvoiceAmt / assetsPerMPPShard if mpp { - numAdditionalShards := assetInvoiceAmt / assetsPerMPPShard numHtlcs += numAdditionalShards * 2 } + t.Logf("Asserting both Alice and Bob have %d HTLCs...", numHtlcs) assertNumHtlcs(t.t, alice, numHtlcs) assertNumHtlcs(t.t, bob, numHtlcs) @@ -3278,6 +3289,9 @@ func runCustomChannelsHtlcForceClose(ctx context.Context, t *harnessTest, t.Logf("Channel closed! Mining blocks, close_txid=%v", closeTxid) + // The channel should first be in "waiting close" until it confirms. + assertWaitingCloseChannelAssetData(t.t, alice, aliceChanPoint) + // Next, we'll mine a block which should start the clock ticking on the // relative timeout for the Alice, and Bob. // @@ -3297,6 +3311,9 @@ func runCustomChannelsHtlcForceClose(ctx context.Context, t *harnessTest, t.Logf("Settling Bob's hodl invoice") + // It should then go to "pending force closed". + assertPendingForceCloseChannelAssetData(t.t, alice, aliceChanPoint) + // At this point, the commitment transaction has been mined, and we have // 4 total HTLCs on Alice's commitment transaction: // @@ -3506,6 +3523,10 @@ func runCustomChannelsHtlcForceClose(ctx context.Context, t *harnessTest, // We'll wait for both Alice and Bob to present their respective sweeps // to the sweeper. + numTimeoutHTLCs := 1 + if mpp { + numTimeoutHTLCs += numAdditionalShards + } assertSweepExists( t.t, alice, walletrpc.WitnessType_TAPROOT_HTLC_LOCAL_OFFERED_TIMEOUT, @@ -3517,9 +3538,68 @@ func runCustomChannelsHtlcForceClose(ctx context.Context, t *harnessTest, t.Logf("Confirming initial HTLC timeout txns") + timeoutSweeps, err := waitForNTxsInMempool( + net.Miner.Client, 2, shortTimeout, + ) + require.NoError(t.t, err) + + t.Logf("Asserting balance on sweeps: %v", timeoutSweeps) + // Finally, we'll mine a single block to confirm them. mineBlocks(t, net, 1, 2) + // Make sure Bob swept all his HTLCs. + bobSweeps, err := bob.WalletKitClient.ListSweeps( + ctx, &walletrpc.ListSweepsRequest{ + Verbose: true, + }, + ) + require.NoError(t.t, err) + + var bobSweepTx *wire.MsgTx + for _, sweep := range bobSweeps.GetTransactionDetails().Transactions { + for _, tx := range timeoutSweeps { + if sweep.TxHash == tx.String() { + txBytes, err := hex.DecodeString(sweep.RawTxHex) + require.NoError(t.t, err) + + bobSweepTx = &wire.MsgTx{} + err = bobSweepTx.Deserialize( + bytes.NewReader(txBytes), + ) + require.NoError(t.t, err) + } + } + } + require.NotNil(t.t, bobSweepTx, "Bob's sweep transaction not found") + + // There's always an extra input that pays for the fees. So we can only + // count the remainder as HTLC inputs. + numSweptHTLCs := len(bobSweepTx.TxIn) - 1 + + // If we didn't yet sweep all HTLCs, then we need to wait for another + // sweep. + if numSweptHTLCs < numTimeoutHTLCs { + assertSweepExists( + t.t, bob, + // nolint: lll + walletrpc.WitnessType_TAPROOT_HTLC_OFFERED_REMOTE_TIMEOUT, + ) + + t.Logf("Confirming additional HTLC timeout sweep txns") + + additionalTimeoutSweeps, err := waitForNTxsInMempool( + net.Miner.Client, 1, shortTimeout, + ) + require.NoError(t.t, err) + + t.Logf("Asserting balance on additional timeout sweeps: %v", + additionalTimeoutSweeps) + + // Finally, we'll mine a single block to confirm them. + mineBlocks(t, net, 1, 1) + } + // At this point, Bob's balance should be incremented by an additional // HTLC value. bobExpectedBalance += uint64(assetInvoiceAmt - 1) diff --git a/itest/litd_test_list_on_test.go b/itest/litd_test_list_on_test.go index 4980d6374..cb2496495 100644 --- a/itest/litd_test_list_on_test.go +++ b/itest/litd_test_list_on_test.go @@ -52,6 +52,10 @@ var allTestCases = []*testCase{ name: "test custom channels htlc force close", test: testCustomChannelsHtlcForceClose, }, + { + name: "test custom channels htlc force close MPP", + test: testCustomChannelsHtlcForceCloseMpp, + }, { name: "test custom channels balance consistency", test: testCustomChannelsBalanceConsistency,