Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions itest/custom_channels/custom_channels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ var testCases = []*ccTestCase{
name: "fee",
test: testCustomChannelsFee,
},
{
name: "coop close fee baseline",
test: testCustomChannelsCoopCloseFeeBaseline,
},
{
name: "breach",
test: testCustomChannelsBreach,
Expand Down
106 changes: 106 additions & 0 deletions itest/custom_channels/fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import (
"context"
"fmt"
"slices"
"testing"

"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/taproot-assets/itest"
"github.com/lightninglabs/taproot-assets/proof"
"github.com/lightninglabs/taproot-assets/taprpc"
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntest/port"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
Expand Down Expand Up @@ -100,3 +104,105 @@ func testCustomChannelsFee(ctx context.Context,
"min_relay_fee: ", tooLowFeeRateAmount.FeePerKWeight())
require.ErrorContains(t.t, err, errFeeRateTooLow)
}

// testCustomChannelsCoopCloseFeeBaseline is a regression test for lnd
// cooperative close fee estimation with auxiliary close outputs. Closing at
// relay floor must still succeed once the aux outputs are included in the
// initial fee baseline.
func testCustomChannelsCoopCloseFeeBaseline(ctx context.Context,
net *itest.IntegratedNetworkHarness, t *ccHarnessTest) {

lndArgs := slices.Clone(lndArgsTemplate)
tapdArgs := slices.Clone(tapdArgsTemplate)

// We use Charlie as the proof courier. But in order for Charlie to
// also use itself, we need to define its port upfront.
charliePort := port.NextAvailablePort()
tapdArgs = append(tapdArgs, fmt.Sprintf(
"--proofcourieraddr=%s://%s",
proof.UniverseRpcCourierType,
fmt.Sprintf(node.ListenerFormat, charliePort),
))

charlieLndArgs := slices.Clone(lndArgs)
charlieLndArgs = append(charlieLndArgs, fmt.Sprintf(
"--rpclisten=127.0.0.1:%d", charliePort,
))

charlie := net.NewNode("Charlie", charlieLndArgs, tapdArgs)
dave := net.NewNode("Dave", lndArgs, tapdArgs)

nodes := []*itest.IntegratedNode{charlie, dave}
connectAllNodes(t.t, net, nodes)
fundAllNodes(t.t, net, nodes)

// Mint an asset on Charlie and sync Dave to Charlie as the universe.
mintedAssets := itest.MintAssetsConfirmBatch(
t.t, net.Miner, asTapd(charlie),
[]*mintrpc.MintAssetRequest{
{
Asset: ccItestAsset,
},
},
)
cents := mintedAssets[0]
assetID := cents.AssetGenesis.AssetId

t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount)
syncUniverses(t.t, charlie, dave)
t.Logf("Universes synced between all nodes, distributing assets...")

const (
openFeeRateSatPerVbyte = 5
closeFeeRateSatPerVbyte = 1
)

net.FeeService.SetMinRelayFeerate(
chainfee.SatPerVByte(closeFeeRateSatPerVbyte).FeePerKVByte(),
)

assetFundResp, err := asTapd(charlie).FundChannel(
ctx, &tchrpc.FundChannelRequest{
AssetAmount: fundingAmount,
AssetId: assetID,
PeerPubkey: dave.PubKey[:],
FeeRateSatPerVbyte: openFeeRateSatPerVbyte,
PushSat: 0,
},
)
require.NoError(t.t, err)

mineBlocks(t, net, 6, 1)

assertAssetChan(
t.t, charlie, dave, fundingAmount, []*taprpc.Asset{cents},
)

chanPoint := &lnrpc.ChannelPoint{
OutputIndex: uint32(assetFundResp.OutputIndex),
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
FundingTxidStr: assetFundResp.Txid,
},
}

feeBaselineCoOpCloseBalanceCheck := func(t *testing.T, _, _ *itest.IntegratedNode,
closeTx *wire.MsgTx, closeUpdate *lnrpc.ChannelCloseUpdate,
_ [][]byte, _ []byte, _ *itest.IntegratedNode) {

require.NotNil(t, closeUpdate.LocalCloseOutput)
require.Len(t, closeUpdate.AdditionalOutputs, 1)

localAuxOut := closeUpdate.AdditionalOutputs[0]
require.True(t, localAuxOut.IsLocal)

auxTxOut, _ := findTxOut(t, closeTx, localAuxOut.PkScript)
require.LessOrEqual(t, auxTxOut.Value, int64(1000))
Comment thread
darioAnongba marked this conversation as resolved.

_, _ = findTxOut(t, closeTx, closeUpdate.LocalCloseOutput.PkScript)
}

closeAssetChannelWithFeeAndAssert(
t, net, charlie, dave, chanPoint, closeFeeRateSatPerVbyte,
[][]byte{assetID}, nil, charlie, feeBaselineCoOpCloseBalanceCheck,
)
}
114 changes: 114 additions & 0 deletions itest/custom_channels/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1456,6 +1456,120 @@
assertClosedChannelAssetData(t.t, remote, chanPoint)
}

// closeAssetChannelWithFeeAndAssert closes an asset channel at the given fee
// rate and asserts the final balances.
func closeAssetChannelWithFeeAndAssert(t *ccHarnessTest,
net *itest.IntegratedNetworkHarness,
local, remote *itest.IntegratedNode,
chanPoint *lnrpc.ChannelPoint, feeRateSatPerVbyte uint64,
assetIDs [][]byte, groupKey []byte,
universeTap *itest.IntegratedNode,
balanceCheck coOpCloseBalanceCheck) {

t.t.Helper()

// Ensure the two parties are connected before attempting the close.
// Channel closes after other close operations can sometimes race with
// peer disconnection, causing "peer is offline" errors.
net.EnsureConnected(t.t, local, remote)

ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, wait.DefaultTimeout)
defer cancel()

closeReq := &lnrpc.CloseChannelRequest{
ChannelPoint: chanPoint,
SatPerVbyte: feeRateSatPerVbyte,
}
closeStream, err := local.CloseChannel(ctxb, closeReq)
Comment thread
darioAnongba marked this conversation as resolved.
require.NoError(t.t, err)

err = waitForClosePendingUpdate(t, net, closeStream)
require.NoError(t.t, err)

sendEvents, err := local.SubscribeSendEvents(
ctxt, &taprpc.SubscribeSendEventsRequest{},
)
require.NoError(t.t, err)

assertWaitingCloseChannelAssetData(t.t, local, chanPoint)
assertWaitingCloseChannelAssetData(t.t, remote, chanPoint)

mineBlocks(t, net, 1, 1)

closeUpdate, err := net.WaitForChannelClose(closeStream)
require.NoError(t.t, err)

closeTxid, err := chainhash.NewHash(closeUpdate.ClosingTxid)
require.NoError(t.t, err)

closeTransaction := net.Miner.GetRawTransaction(*closeTxid)
closeTx := closeTransaction.MsgTx()
t.Logf("Channel closed with txid: %v", closeTxid)

waitForSendEvent(t.t, sendEvents, tapfreighter.SendStateComplete)

balanceCheck(
t.t, local, remote, closeTx, closeUpdate, assetIDs, groupKey,
universeTap,
)

assertClosedChannelAssetData(t.t, local, chanPoint)
assertClosedChannelAssetData(t.t, remote, chanPoint)
}

// waitForClosePendingUpdate waits for the first close pending update on the
// close stream and ensures that the close transaction reaches the mempool.
func waitForClosePendingUpdate(t *ccHarnessTest,
net *itest.IntegratedNetworkHarness,
closeStream lnrpc.Lightning_CloseChannelClient) error {

t.t.Helper()

errChan := make(chan error, 1)
txidChan := make(chan *chainhash.Hash, 1)

go func() {
closeResp, err := closeStream.Recv()
if err != nil {
errChan <- fmt.Errorf("unable to recv from close stream: %w",

Check failure on line 1535 in itest/custom_channels/helpers.go

View workflow job for this annotation

GitHub Actions / Lint check

The line is 85 characters long, which exceeds the maximum of 80 characters. (lll)
err)
return
}

pendingClose, ok := closeResp.Update.(*lnrpc.CloseStatusUpdate_ClosePending)

Check failure on line 1540 in itest/custom_channels/helpers.go

View workflow job for this annotation

GitHub Actions / Lint check

The line is 92 characters long, which exceeds the maximum of 80 characters. (lll)
if !ok {
errChan <- fmt.Errorf("expected close pending update, "+
"instead got %v", closeResp)
return
}

closeTxid, err := chainhash.NewHash(
pendingClose.ClosePending.Txid,
)
if err != nil {
errChan <- fmt.Errorf("unable to decode closeTxid: %w",
err)
return
}

txidChan <- closeTxid
}()

select {
case err := <-errChan:
return err

case closeTxid := <-txidChan:
net.Miner.AssertTxInMempool(*closeTxid)
return nil

case <-time.After(wait.ChannelCloseTimeout):
return fmt.Errorf("timeout reached while waiting for close " +
"pending update")
}
}

// assertDefaultCoOpCloseBalance returns a default co-op close balance check
// function.
//
Expand Down
Loading