Merge pull request #1198 from jtobin/capacity-check

itest: add channel capacity assertions
This commit is contained in:
Jared Tobin 2026-02-16 13:51:38 +04:00 committed by GitHub
commit 8d601c6e45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 150 additions and 0 deletions

View file

@ -3,6 +3,7 @@ package itest
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"math"
"math/big"
@ -35,6 +36,7 @@ import (
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntest/port"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
@ -5486,3 +5488,146 @@ func testCustomChannelsMultiChannelPathfinding(ctx context.Context,
t.t, alice, bob, invoiceResp.PaymentRequest, assetIDPences,
)
}
// testCustomChannelsPassiveAssets tests that passive assets (assets
// in the same input commitment but not used for channel funding) are
// not included in the funding assets sent to the responder
func testCustomChannelsPassiveAssets(ctx context.Context, net *NetworkHarness,
t *harnessTest) {
lndArgs := slices.Clone(lndArgsTemplate)
litdArgs := slices.Clone(litdArgsTemplate)
charliePort := port.NextAvailablePort()
litdArgs = append(litdArgs, fmt.Sprintf(
"--taproot-assets.proofcourieraddr=%s://%s",
proof.UniverseRpcCourierType,
fmt.Sprintf(node.ListenerFormat, charliePort),
))
charlie, err := net.NewNodeWithPort(
t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs...,
)
require.NoError(t.t, err)
dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...)
require.NoError(t.t, err)
nodes := []*HarnessNode{charlie, dave}
connectAllNodes(t.t, net, nodes)
fundAllNodes(t.t, net, nodes)
charlieTap := newTapClient(t.t, charlie)
daveTap := newTapClient(t.t, dave)
// Mint two assets in the same batch with the same group key. This puts
// them in the same anchor UTXO/commitment.
assetA := &mintrpc.MintAsset{
AssetType: taprpc.AssetType_NORMAL,
Name: "asset-a-for-channel",
AssetMeta: dummyMetaData,
Amount: 100_000,
NewGroupedAsset: true,
}
assetB := &mintrpc.MintAsset{
AssetType: taprpc.AssetType_NORMAL,
Name: "asset-b-passive",
AssetMeta: dummyMetaData,
Amount: 50_000,
GroupedAsset: true,
GroupAnchor: "asset-a-for-channel",
}
mintedAssets := itest.MintAssetsConfirmBatch(
t.t, t.lndHarness.Miner.Client, charlieTap,
[]*mintrpc.MintAssetRequest{
{Asset: assetA},
{Asset: assetB},
},
)
require.Len(t.t, mintedAssets, 2)
mintedA := mintedAssets[0]
mintedB := mintedAssets[1]
assetIDA := mintedA.AssetGenesis.AssetId
// Both assets should have the same group key.
require.NotNil(t.t, mintedA.AssetGroup)
require.NotNil(t.t, mintedB.AssetGroup)
groupKey := mintedA.AssetGroup.TweakedGroupKey
require.Equal(t.t, groupKey, mintedB.AssetGroup.TweakedGroupKey)
// Check if both assets share the same anchor outpoint (i.e. UTXO).
outpointA := mintedA.ChainAnchor.AnchorOutpoint
outpointB := mintedB.ChainAnchor.AnchorOutpoint
t.Logf("Asset A anchor outpoint: %s", outpointA)
t.Logf("Asset B anchor outpoint: %s", outpointB)
require.Equal(t.t, outpointA, outpointB,
"assets must be in same UTXO for passive asset test")
syncUniverses(t.t, charlieTap, dave)
mineBlocks(t, net, 1, 0)
// Fund a channel using the group key. We request 75,000 units which
// forces Asset A (100,000) to be selected since Asset B (50,000) is
// insufficient. Asset B should become passive and not appear in Dave's
// view of the channel.
const fundingAmount = 75_000
fundResp, err := charlieTap.FundChannel(
ctx, &tchrpc.FundChannelRequest{
AssetAmount: fundingAmount,
GroupKey: groupKey,
PeerPubkey: daveTap.node.PubKey[:],
FeeRateSatPerVbyte: 5,
PushSat: DefaultPushSat,
},
)
require.NoError(t.t, err)
mineBlocks(t, net, 6, 1)
// Assert Dave (responder) only sees Asset A in the channel, not the
// passive Asset B. Use wait.NoError to handle the race condition where
// the channel may not be immediately visible after mining.
var chanData *rfqmsg.JsonAssetChannel
err = wait.NoError(func() error {
var chanErr error
chanData, chanErr = getChannelCustomData(dave, charlie)
if chanErr != nil {
return chanErr
}
// There should be exactly one funding asset (Asset A).
if len(chanData.FundingAssets) != 1 {
return fmt.Errorf("expected 1 funding asset, got %d",
len(chanData.FundingAssets))
}
// Verify it's Asset A, not Asset B.
fundingAssetID := chanData.FundingAssets[0].AssetGenesis.AssetID
if hex.EncodeToString(assetIDA) != fundingAssetID {
return fmt.Errorf("funding asset should be Asset A, "+
"got %s", fundingAssetID)
}
// Verify capacity matches funding amount, not inflated by
// Asset B.
if chanData.Capacity != uint64(fundingAmount) {
return fmt.Errorf("capacity should be %d, not "+
"inflated by passive assets, got %d",
fundingAmount, chanData.Capacity)
}
return nil
}, defaultTimeout)
require.NoError(t.t, err)
chanPoint := &lnrpc.ChannelPoint{
OutputIndex: uint32(fundResp.OutputIndex),
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
FundingTxidStr: fundResp.Txid,
},
}
closeAssetChannelAndAssert(
t, net, charlie, dave, chanPoint, [][]byte{assetIDA}, nil,
charlieTap, noOpCoOpCloseBalanceCheck,
)
}

View file

@ -101,6 +101,11 @@ var allTestCases = []*testCase{
test: testCustomChannelsSingleAssetMultiInput,
noAliceBob: true,
},
{
name: "custom channels passive assets",
test: testCustomChannelsPassiveAssets,
noAliceBob: true,
},
{
name: "custom channels oracle pricing",
test: testCustomChannelsOraclePricing,