mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
3219 lines
90 KiB
Go
3219 lines
90 KiB
Go
package itest
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/davecgh/go-spew/spew"
|
|
taprootassets "github.com/lightninglabs/taproot-assets"
|
|
"github.com/lightninglabs/taproot-assets/asset"
|
|
tapfn "github.com/lightninglabs/taproot-assets/fn"
|
|
"github.com/lightninglabs/taproot-assets/itest"
|
|
"github.com/lightninglabs/taproot-assets/proof"
|
|
"github.com/lightninglabs/taproot-assets/rfq"
|
|
"github.com/lightninglabs/taproot-assets/rfqmath"
|
|
"github.com/lightninglabs/taproot-assets/rfqmsg"
|
|
"github.com/lightninglabs/taproot-assets/rpcutils"
|
|
"github.com/lightninglabs/taproot-assets/tapfreighter"
|
|
"github.com/lightninglabs/taproot-assets/taprpc"
|
|
"github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
|
|
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
|
|
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
|
|
tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
|
|
"github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc"
|
|
"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
|
|
"github.com/lightninglabs/taproot-assets/tapscript"
|
|
"github.com/lightningnetwork/lnd/fn"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
|
"github.com/lightningnetwork/lnd/lntest/rpc"
|
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/macaroons"
|
|
"github.com/lightningnetwork/lnd/record"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/maps"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"gopkg.in/macaroon.v2"
|
|
)
|
|
|
|
// PaymentTimeout is the default payment timeout we use in our tests.
|
|
const (
|
|
PaymentTimeout = 12 * time.Second
|
|
DefaultPushSat int64 = 1062
|
|
)
|
|
|
|
// nolint: lll
|
|
var (
|
|
failureNoBalance = lnrpc.PaymentFailureReason_FAILURE_REASON_INSUFFICIENT_BALANCE
|
|
failureNoRoute = lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE
|
|
failureIncorrectDetails = lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS
|
|
failureTimeout = lnrpc.PaymentFailureReason_FAILURE_REASON_TIMEOUT
|
|
failureNone = lnrpc.PaymentFailureReason_FAILURE_REASON_NONE
|
|
)
|
|
|
|
// createTestAssetNetwork sends asset funds from Charlie to Dave and Erin, so
|
|
// they can fund asset channels with Yara and Fabia, respectively. So the asset
|
|
// channels created are Charlie->Dave, Dave->Yara, Erin->Fabia. The channels
|
|
// are then confirmed and balances asserted.
|
|
func createTestAssetNetwork(t *harnessTest, net *NetworkHarness, charlieTap,
|
|
daveTap, erinTap, fabiaTap, yaraTap, universeTap *tapClient,
|
|
mintedAsset *taprpc.Asset, assetSendAmount, charlieFundingAmount,
|
|
daveFundingAmount,
|
|
erinFundingAmount uint64, pushSat int64) (*lnrpc.ChannelPoint,
|
|
*lnrpc.ChannelPoint, *lnrpc.ChannelPoint) {
|
|
|
|
ctxb := context.Background()
|
|
assetID := mintedAsset.AssetGenesis.AssetId
|
|
var groupKey []byte
|
|
if mintedAsset.AssetGroup != nil {
|
|
groupKey = mintedAsset.AssetGroup.TweakedGroupKey
|
|
}
|
|
|
|
fundingScriptTree := tapscript.NewChannelFundingScriptTree()
|
|
fundingScriptKey := fundingScriptTree.TaprootKey
|
|
fundingScriptKeyBytes := fundingScriptKey.SerializeCompressed()
|
|
|
|
// We need to send some assets to Dave, so he can fund an asset channel
|
|
// with Yara.
|
|
daveAddr, err := daveTap.NewAddr(ctxb, &taprpc.NewAddrRequest{
|
|
Amt: assetSendAmount,
|
|
AssetId: assetID,
|
|
ProofCourierAddr: fmt.Sprintf(
|
|
"%s://%s", proof.UniverseRpcCourierType,
|
|
charlieTap.node.Cfg.LitAddr(),
|
|
),
|
|
})
|
|
require.NoError(t.t, err)
|
|
|
|
t.Logf("Sending %v asset units to Dave...", assetSendAmount)
|
|
|
|
// Send the assets to Dave.
|
|
itest.AssertAddrCreated(t.t, daveTap, mintedAsset, daveAddr)
|
|
sendResp, err := charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{
|
|
TapAddrs: []string{daveAddr.Encoded},
|
|
})
|
|
require.NoError(t.t, err)
|
|
itest.ConfirmAndAssertOutboundTransfer(
|
|
t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID,
|
|
[]uint64{mintedAsset.Amount - assetSendAmount, assetSendAmount},
|
|
0, 1,
|
|
)
|
|
itest.AssertNonInteractiveRecvComplete(t.t, daveTap, 1)
|
|
|
|
// We need to send some assets to Erin, so he can fund an asset channel
|
|
// with Fabia.
|
|
erinAddr, err := erinTap.NewAddr(ctxb, &taprpc.NewAddrRequest{
|
|
Amt: assetSendAmount,
|
|
AssetId: assetID,
|
|
ProofCourierAddr: fmt.Sprintf(
|
|
"%s://%s", proof.UniverseRpcCourierType,
|
|
charlieTap.node.Cfg.LitAddr(),
|
|
),
|
|
})
|
|
require.NoError(t.t, err)
|
|
|
|
t.Logf("Sending %v asset units to Erin...", assetSendAmount)
|
|
|
|
// Send the assets to Erin.
|
|
itest.AssertAddrCreated(t.t, erinTap, mintedAsset, erinAddr)
|
|
sendResp, err = charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{
|
|
TapAddrs: []string{erinAddr.Encoded},
|
|
})
|
|
require.NoError(t.t, err)
|
|
itest.ConfirmAndAssertOutboundTransfer(
|
|
t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID,
|
|
[]uint64{
|
|
mintedAsset.Amount - 2*assetSendAmount, assetSendAmount,
|
|
}, 1, 2,
|
|
)
|
|
itest.AssertNonInteractiveRecvComplete(t.t, erinTap, 1)
|
|
|
|
t.Logf("Opening asset channels...")
|
|
|
|
// The first channel we create has a push amount, so Charlie can receive
|
|
// payments immediately and not run into the channel reserve issue.
|
|
fundRespCD, err := charlieTap.FundChannel(
|
|
ctxb, &tchrpc.FundChannelRequest{
|
|
AssetAmount: charlieFundingAmount,
|
|
AssetId: assetID,
|
|
PeerPubkey: daveTap.node.PubKey[:],
|
|
FeeRateSatPerVbyte: 5,
|
|
PushSat: pushSat,
|
|
},
|
|
)
|
|
require.NoError(t.t, err)
|
|
t.Logf("Funded channel between Charlie and Dave: %v", fundRespCD)
|
|
|
|
fundRespDY, err := daveTap.FundChannel(
|
|
ctxb, &tchrpc.FundChannelRequest{
|
|
AssetAmount: daveFundingAmount,
|
|
AssetId: assetID,
|
|
PeerPubkey: yaraTap.node.PubKey[:],
|
|
FeeRateSatPerVbyte: 5,
|
|
},
|
|
)
|
|
require.NoError(t.t, err)
|
|
t.Logf("Funded channel between Dave and Yara: %v", fundRespDY)
|
|
|
|
fundRespEF, err := erinTap.FundChannel(
|
|
ctxb, &tchrpc.FundChannelRequest{
|
|
AssetAmount: erinFundingAmount,
|
|
AssetId: assetID,
|
|
PeerPubkey: fabiaTap.node.PubKey[:],
|
|
FeeRateSatPerVbyte: 5,
|
|
PushSat: pushSat,
|
|
},
|
|
)
|
|
require.NoError(t.t, err)
|
|
t.Logf("Funded channel between Erin and Fabia: %v", fundRespEF)
|
|
|
|
// Make sure the pending channel shows up in the list and has the
|
|
// custom records set as JSON.
|
|
assertPendingChannels(
|
|
t.t, charlieTap.node, mintedAsset, 1, charlieFundingAmount, 0,
|
|
)
|
|
assertPendingChannels(
|
|
t.t, daveTap.node, mintedAsset, 2, daveFundingAmount,
|
|
charlieFundingAmount,
|
|
)
|
|
assertPendingChannels(
|
|
t.t, erinTap.node, mintedAsset, 1, erinFundingAmount, 0,
|
|
)
|
|
|
|
// Now that we've looked at the pending channels, let's actually confirm
|
|
// all three of them.
|
|
mineBlocks(t, net, 6, 3)
|
|
|
|
// We'll be tracking the expected asset balances throughout the test, so
|
|
// we can assert it after each action.
|
|
charlieAssetBalance := mintedAsset.Amount - 2*assetSendAmount -
|
|
charlieFundingAmount
|
|
daveAssetBalance := assetSendAmount - daveFundingAmount
|
|
erinAssetBalance := assetSendAmount - erinFundingAmount
|
|
|
|
// Assert that we see the funding outputs in the wallet.
|
|
itest.AssertBalances(
|
|
t.t, charlieTap, charlieFundingAmount,
|
|
itest.WithAssetID(assetID),
|
|
itest.WithScriptKeyType(asset.ScriptKeyScriptPathChannel),
|
|
itest.WithNumUtxos(1),
|
|
itest.WithScriptKey(fundingScriptKeyBytes),
|
|
)
|
|
itest.AssertBalances(
|
|
t.t, daveTap, daveFundingAmount, itest.WithAssetID(assetID),
|
|
itest.WithScriptKeyType(asset.ScriptKeyScriptPathChannel),
|
|
itest.WithNumUtxos(1),
|
|
itest.WithScriptKey(fundingScriptKeyBytes),
|
|
)
|
|
itest.AssertBalances(
|
|
t.t, erinTap, erinFundingAmount, itest.WithAssetID(assetID),
|
|
itest.WithScriptKeyType(asset.ScriptKeyScriptPathChannel),
|
|
itest.WithNumUtxos(1),
|
|
itest.WithScriptKey(fundingScriptKeyBytes),
|
|
)
|
|
|
|
// After opening the channels, the asset balance of the funding nodes
|
|
// should have been decreased with the funding amount.
|
|
itest.AssertBalances(
|
|
t.t, charlieTap, charlieAssetBalance,
|
|
itest.WithAssetID(assetID),
|
|
)
|
|
itest.AssertBalances(
|
|
t.t, daveTap, daveAssetBalance, itest.WithAssetID(assetID),
|
|
)
|
|
itest.AssertBalances(
|
|
t.t, erinTap, erinAssetBalance, itest.WithAssetID(assetID),
|
|
)
|
|
|
|
// Assert that the proofs for both channels has been uploaded to the
|
|
// designated Universe server.
|
|
assertUniverseProofExists(
|
|
t.t, universeTap, assetID, groupKey, fundingScriptKeyBytes,
|
|
fmt.Sprintf("%v:%v", fundRespCD.Txid, fundRespCD.OutputIndex),
|
|
)
|
|
assertUniverseProofExists(
|
|
t.t, universeTap, assetID, groupKey, fundingScriptKeyBytes,
|
|
fmt.Sprintf("%v:%v", fundRespDY.Txid, fundRespDY.OutputIndex),
|
|
)
|
|
assertUniverseProofExists(
|
|
t.t, universeTap, assetID, groupKey, fundingScriptKeyBytes,
|
|
fmt.Sprintf("%v:%v", fundRespEF.Txid, fundRespEF.OutputIndex),
|
|
)
|
|
|
|
// Make sure the channel shows the correct asset information.
|
|
assertAssetChan(
|
|
t.t, charlieTap.node, daveTap.node, charlieFundingAmount,
|
|
[]*taprpc.Asset{mintedAsset},
|
|
)
|
|
assertAssetChan(
|
|
t.t, daveTap.node, yaraTap.node, daveFundingAmount,
|
|
[]*taprpc.Asset{mintedAsset},
|
|
)
|
|
assertAssetChan(
|
|
t.t, erinTap.node, fabiaTap.node, erinFundingAmount,
|
|
[]*taprpc.Asset{mintedAsset},
|
|
)
|
|
|
|
chanPointCD := &lnrpc.ChannelPoint{
|
|
OutputIndex: uint32(fundRespCD.OutputIndex),
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: fundRespCD.Txid,
|
|
},
|
|
}
|
|
chanPointDY := &lnrpc.ChannelPoint{
|
|
OutputIndex: uint32(fundRespDY.OutputIndex),
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: fundRespDY.Txid,
|
|
},
|
|
}
|
|
chanPointEF := &lnrpc.ChannelPoint{
|
|
OutputIndex: uint32(fundRespEF.OutputIndex),
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: fundRespEF.Txid,
|
|
},
|
|
}
|
|
|
|
return chanPointCD, chanPointDY, chanPointEF
|
|
}
|
|
|
|
// createTestAssetNetworkGroupKey sets up a test network with Charlie, Dave,
|
|
// Erin and Fabia and creates asset channels between Charlie->Dave and
|
|
// Erin-Fabia in a way that there are two equally sized asset pieces for each
|
|
// minted asset (currently limited to exactly two assets). The channels are then
|
|
// confirmed and balances asserted.
|
|
func createTestAssetNetworkGroupKey(ctx context.Context, t *harnessTest,
|
|
net *NetworkHarness, charlieTap, daveTap, erinTap, fabiaTap,
|
|
universeTap *tapClient, mintedAssets []*taprpc.Asset,
|
|
charlieFundingAmount, erinFundingAmount uint64,
|
|
pushSat int64) (*lnrpc.ChannelPoint, *lnrpc.ChannelPoint) {
|
|
|
|
var groupKey []byte
|
|
for _, mintedAsset := range mintedAssets {
|
|
require.NotNil(t.t, mintedAsset.AssetGroup)
|
|
|
|
if groupKey == nil {
|
|
groupKey = mintedAsset.AssetGroup.TweakedGroupKey
|
|
|
|
continue
|
|
}
|
|
|
|
require.Equal(
|
|
t.t, groupKey, mintedAsset.AssetGroup.TweakedGroupKey,
|
|
)
|
|
}
|
|
|
|
// We first do a transfer to Charlie by itself, so we get the correct
|
|
// asset pieces that we want for the channel funding.
|
|
sendAssetsAndAssert(
|
|
ctx, t, charlieTap, charlieTap, universeTap, mintedAssets[0],
|
|
charlieFundingAmount/2, 0, 1, 0,
|
|
)
|
|
sendAssetsAndAssert(
|
|
ctx, t, charlieTap, charlieTap, universeTap, mintedAssets[1],
|
|
charlieFundingAmount/2, 1, 2, 0,
|
|
)
|
|
|
|
// We need to send some assets to Erin, so he can fund an asset channel
|
|
// with Fabia.
|
|
sendAssetsAndAssert(
|
|
ctx, t, erinTap, charlieTap, universeTap, mintedAssets[0],
|
|
erinFundingAmount/2, 2, 1, charlieFundingAmount/2,
|
|
)
|
|
sendAssetsAndAssert(
|
|
ctx, t, erinTap, charlieTap, universeTap, mintedAssets[1],
|
|
erinFundingAmount/2, 3, 2, charlieFundingAmount/2,
|
|
)
|
|
|
|
// Then we burn everything but a single asset piece. We do this to make
|
|
// sure that the channel funding code will select the correct asset
|
|
// UTXOs during the channel funding. Otherwise, it's super hard to
|
|
// predict what exactly goes into the channel funding transaction. And
|
|
// this way it's also easier to assert overall balances.
|
|
assetID1 := mintedAssets[0].AssetGenesis.AssetId
|
|
assetID2 := mintedAssets[1].AssetGenesis.AssetId
|
|
burnAmount1 := mintedAssets[0].Amount - charlieFundingAmount/2 -
|
|
erinFundingAmount/2 - 1
|
|
_, err := charlieTap.BurnAsset(ctx, &taprpc.BurnAssetRequest{
|
|
Asset: &taprpc.BurnAssetRequest_AssetId{
|
|
AssetId: assetID1,
|
|
},
|
|
AmountToBurn: burnAmount1,
|
|
ConfirmationText: taprootassets.AssetBurnConfirmationText,
|
|
})
|
|
require.NoError(t.t, err)
|
|
|
|
mineBlocks(t, net, 1, 1)
|
|
|
|
burnAmount2 := mintedAssets[1].Amount - charlieFundingAmount/2 -
|
|
erinFundingAmount/2 - 1
|
|
_, err = charlieTap.BurnAsset(ctx, &taprpc.BurnAssetRequest{
|
|
Asset: &taprpc.BurnAssetRequest_AssetId{
|
|
AssetId: assetID2,
|
|
},
|
|
AmountToBurn: burnAmount2,
|
|
ConfirmationText: taprootassets.AssetBurnConfirmationText,
|
|
})
|
|
require.NoError(t.t, err)
|
|
|
|
mineBlocks(t, net, 1, 1)
|
|
|
|
t.Logf("Opening asset channels...")
|
|
|
|
// The first channel we create has a push amount, so Charlie can receive
|
|
// payments immediately and not run into the channel reserve issue.
|
|
fundRespCD, err := charlieTap.FundChannel(
|
|
ctx, &tchrpc.FundChannelRequest{
|
|
AssetAmount: charlieFundingAmount,
|
|
GroupKey: groupKey,
|
|
PeerPubkey: daveTap.node.PubKey[:],
|
|
FeeRateSatPerVbyte: 5,
|
|
PushSat: pushSat,
|
|
},
|
|
)
|
|
require.NoError(t.t, err)
|
|
t.Logf("Funded channel between Charlie and Dave: %v", fundRespCD)
|
|
|
|
fundRespEF, err := erinTap.FundChannel(
|
|
ctx, &tchrpc.FundChannelRequest{
|
|
AssetAmount: erinFundingAmount,
|
|
GroupKey: groupKey,
|
|
PeerPubkey: fabiaTap.node.PubKey[:],
|
|
FeeRateSatPerVbyte: 5,
|
|
PushSat: pushSat,
|
|
},
|
|
)
|
|
require.NoError(t.t, err)
|
|
t.Logf("Funded channel between Erin and Fabia: %v", fundRespEF)
|
|
|
|
// Make sure the pending channel shows up in the list and has the
|
|
// custom records set as JSON.
|
|
assertPendingChannels(
|
|
t.t, charlieTap.node, mintedAssets[0], 1,
|
|
charlieFundingAmount/2, 0,
|
|
)
|
|
assertPendingChannels(
|
|
t.t, charlieTap.node, mintedAssets[1], 1,
|
|
charlieFundingAmount/2, 0,
|
|
)
|
|
assertPendingChannels(
|
|
t.t, erinTap.node, mintedAssets[0], 1, erinFundingAmount/2, 0,
|
|
)
|
|
assertPendingChannels(
|
|
t.t, erinTap.node, mintedAssets[1], 1, erinFundingAmount/2, 0,
|
|
)
|
|
|
|
// Now that we've looked at the pending channels, let's actually confirm
|
|
// all three of them.
|
|
mineBlocks(t, net, 6, 2)
|
|
|
|
var id1, id2 asset.ID
|
|
copy(id1[:], assetID1)
|
|
copy(id2[:], assetID2)
|
|
|
|
fundingTree1, err := tapscript.NewChannelFundingScriptTreeUniqueID(
|
|
id1,
|
|
)
|
|
require.NoError(t.t, err)
|
|
fundingScriptKey1 := fundingTree1.TaprootKey
|
|
fundingScriptKeyBytes1 := fundingScriptKey1.SerializeCompressed()
|
|
|
|
fundingTree2, err := tapscript.NewChannelFundingScriptTreeUniqueID(
|
|
id2,
|
|
)
|
|
require.NoError(t.t, err)
|
|
fundingScriptKey2 := fundingTree2.TaprootKey
|
|
fundingScriptKeyBytes2 := fundingScriptKey2.SerializeCompressed()
|
|
|
|
// We expect the funding outputs to be in the wallet.
|
|
itest.AssertBalances(
|
|
t.t, charlieTap, charlieFundingAmount/2,
|
|
itest.WithAssetID(assetID1),
|
|
itest.WithScriptKeyType(asset.ScriptKeyScriptPathChannel),
|
|
itest.WithNumUtxos(1),
|
|
itest.WithScriptKey(fundingScriptKeyBytes1),
|
|
)
|
|
itest.AssertBalances(
|
|
t.t, charlieTap, charlieFundingAmount/2,
|
|
itest.WithAssetID(assetID2),
|
|
itest.WithScriptKeyType(asset.ScriptKeyScriptPathChannel),
|
|
itest.WithNumUtxos(1),
|
|
itest.WithScriptKey(fundingScriptKeyBytes2),
|
|
)
|
|
itest.AssertBalances(
|
|
t.t, erinTap, erinFundingAmount/2, itest.WithAssetID(assetID1),
|
|
itest.WithScriptKeyType(asset.ScriptKeyScriptPathChannel),
|
|
itest.WithNumUtxos(1),
|
|
itest.WithScriptKey(fundingScriptKeyBytes1),
|
|
)
|
|
itest.AssertBalances(
|
|
t.t, erinTap, erinFundingAmount/2, itest.WithAssetID(assetID2),
|
|
itest.WithScriptKeyType(asset.ScriptKeyScriptPathChannel),
|
|
itest.WithNumUtxos(1),
|
|
itest.WithScriptKey(fundingScriptKeyBytes2),
|
|
)
|
|
|
|
// There should be no directly spendable asset balance remaining, except
|
|
// for the 1 asset left over from the burn on Charlie.
|
|
itest.AssertBalances(
|
|
t.t, charlieTap, 1, itest.WithAssetID(assetID1),
|
|
itest.WithNumUtxos(1),
|
|
)
|
|
itest.AssertBalances(
|
|
t.t, charlieTap, 1, itest.WithAssetID(assetID2),
|
|
itest.WithNumUtxos(1),
|
|
)
|
|
|
|
// Erin only has the funding output for the channel with Fabia.
|
|
itest.AssertBalances(t.t, erinTap, 0, itest.WithAssetID(assetID1))
|
|
itest.AssertBalances(t.t, erinTap, 0, itest.WithAssetID(assetID1))
|
|
|
|
// Assert that the proofs for both channels has been uploaded to the
|
|
// designated Universe server.
|
|
assertUniverseProofExists(
|
|
t.t, universeTap, assetID1, groupKey, fundingScriptKeyBytes1,
|
|
fmt.Sprintf("%v:%v", fundRespCD.Txid, fundRespCD.OutputIndex),
|
|
)
|
|
assertUniverseProofExists(
|
|
t.t, universeTap, assetID2, groupKey, fundingScriptKeyBytes2,
|
|
fmt.Sprintf("%v:%v", fundRespCD.Txid, fundRespCD.OutputIndex),
|
|
)
|
|
assertUniverseProofExists(
|
|
t.t, universeTap, assetID1, groupKey, fundingScriptKeyBytes1,
|
|
fmt.Sprintf("%v:%v", fundRespEF.Txid, fundRespEF.OutputIndex),
|
|
)
|
|
assertUniverseProofExists(
|
|
t.t, universeTap, assetID2, groupKey, fundingScriptKeyBytes2,
|
|
fmt.Sprintf("%v:%v", fundRespEF.Txid, fundRespEF.OutputIndex),
|
|
)
|
|
|
|
// Make sure the channel shows the correct asset information.
|
|
assertAssetChan(
|
|
t.t, charlieTap.node, daveTap.node, charlieFundingAmount,
|
|
mintedAssets,
|
|
)
|
|
assertAssetChan(
|
|
t.t, erinTap.node, fabiaTap.node, erinFundingAmount,
|
|
mintedAssets,
|
|
)
|
|
|
|
chanPointCD := &lnrpc.ChannelPoint{
|
|
OutputIndex: uint32(fundRespCD.OutputIndex),
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: fundRespCD.Txid,
|
|
},
|
|
}
|
|
chanPointEF := &lnrpc.ChannelPoint{
|
|
OutputIndex: uint32(fundRespEF.OutputIndex),
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: fundRespEF.Txid,
|
|
},
|
|
}
|
|
|
|
return chanPointCD, chanPointEF
|
|
}
|
|
|
|
// sendAssetsAndAssert sends the given amount of assets to the recipient and
|
|
// asserts that the transfer was successful. It also checks that the asset
|
|
// balance of the sender and recipient is as expected.
|
|
func sendAssetsAndAssert(ctx context.Context, t *harnessTest,
|
|
recipient, sender, universe *tapClient, mintedAsset *taprpc.Asset,
|
|
assetSendAmount uint64, idx, numTransfers int,
|
|
previousSentAmount uint64) {
|
|
|
|
assetID := mintedAsset.AssetGenesis.AssetId
|
|
recipientAddr, err := recipient.NewAddr(ctx, &taprpc.NewAddrRequest{
|
|
Amt: assetSendAmount,
|
|
AssetId: assetID,
|
|
ProofCourierAddr: fmt.Sprintf(
|
|
"%s://%s", proof.UniverseRpcCourierType,
|
|
universe.node.Cfg.LitAddr(),
|
|
),
|
|
})
|
|
require.NoError(t.t, err)
|
|
|
|
t.Logf("Sending %v asset units to %s...", assetSendAmount,
|
|
recipient.node.Cfg.Name)
|
|
|
|
// We assume that we sent the same size in a previous send.
|
|
totalSent := assetSendAmount + previousSentAmount
|
|
|
|
// Send the assets to recipient.
|
|
itest.AssertAddrCreated(
|
|
t.t, recipient, mintedAsset, recipientAddr,
|
|
)
|
|
sendResp, err := sender.SendAsset(ctx, &taprpc.SendAssetRequest{
|
|
TapAddrs: []string{recipientAddr.Encoded},
|
|
})
|
|
require.NoError(t.t, err)
|
|
itest.ConfirmAndAssertOutboundTransfer(
|
|
t.t, t.lndHarness.Miner.Client, sender, sendResp,
|
|
assetID,
|
|
[]uint64{mintedAsset.Amount - totalSent, assetSendAmount},
|
|
idx, idx+1,
|
|
)
|
|
itest.AssertNonInteractiveRecvComplete(t.t, recipient, numTransfers)
|
|
}
|
|
|
|
func locateAssetTransfers(t *testing.T, tapdClient *tapClient,
|
|
txid chainhash.Hash) *taprpc.AssetTransfer {
|
|
|
|
var transfer *taprpc.AssetTransfer
|
|
err := wait.NoError(func() error {
|
|
ctxb := context.Background()
|
|
forceCloseTransfer, err := tapdClient.ListTransfers(
|
|
ctxb, &taprpc.ListTransfersRequest{
|
|
AnchorTxid: txid.String(),
|
|
},
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to list %v transfers: %w",
|
|
tapdClient.node.Name(), err)
|
|
}
|
|
if len(forceCloseTransfer.Transfers) != 1 {
|
|
return fmt.Errorf("%v is expecting %d transfers, has "+
|
|
"%d", tapdClient.node.Name(), 1,
|
|
len(forceCloseTransfer.Transfers))
|
|
}
|
|
|
|
transfer = forceCloseTransfer.Transfers[0]
|
|
|
|
if transfer.AnchorTxBlockHash == nil {
|
|
return fmt.Errorf("missing anchor block hash, " +
|
|
"transfer not confirmed")
|
|
}
|
|
|
|
return nil
|
|
}, defaultTimeout)
|
|
require.NoError(t, err)
|
|
|
|
return transfer
|
|
}
|
|
|
|
func connectAllNodes(t *testing.T, net *NetworkHarness, nodes []*HarnessNode) {
|
|
for i, node := range nodes {
|
|
for j := i + 1; j < len(nodes); j++ {
|
|
peer := nodes[j]
|
|
net.ConnectNodesPerm(t, node, peer)
|
|
}
|
|
}
|
|
}
|
|
|
|
func fundAllNodes(t *testing.T, net *NetworkHarness, nodes []*HarnessNode) {
|
|
for _, node := range nodes {
|
|
net.SendCoins(t, btcutil.SatoshiPerBitcoin, node)
|
|
}
|
|
}
|
|
|
|
func syncUniverses(t *testing.T, universe *tapClient, nodes ...*HarnessNode) {
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
for _, node := range nodes {
|
|
nodeTapClient := newTapClient(t, node)
|
|
|
|
universeHostAddr := universe.node.Cfg.LitAddr()
|
|
t.Logf("Syncing node %v with universe %v", node.Cfg.Name,
|
|
universeHostAddr)
|
|
|
|
itest.SyncUniverses(
|
|
ctxt, t, nodeTapClient, universe, universeHostAddr,
|
|
defaultTimeout,
|
|
)
|
|
}
|
|
}
|
|
|
|
func assertUniverseProofExists(t *testing.T, universe *tapClient,
|
|
assetID, groupKey, scriptKey []byte, outpoint string) *taprpc.Asset {
|
|
|
|
t.Logf("Asserting proof outpoint=%v, script_key=%x, asset_id=%x, "+
|
|
"group_key=%x", outpoint, scriptKey, assetID, groupKey)
|
|
|
|
req := &universerpc.UniverseKey{
|
|
Id: &universerpc.ID{
|
|
ProofType: universerpc.ProofType_PROOF_TYPE_TRANSFER,
|
|
},
|
|
LeafKey: &universerpc.AssetKey{
|
|
Outpoint: &universerpc.AssetKey_OpStr{
|
|
OpStr: outpoint,
|
|
},
|
|
ScriptKey: &universerpc.AssetKey_ScriptKeyBytes{
|
|
ScriptKeyBytes: scriptKey,
|
|
},
|
|
},
|
|
}
|
|
|
|
switch {
|
|
case len(groupKey) > 0:
|
|
req.Id.Id = &universerpc.ID_GroupKey{
|
|
GroupKey: groupKey,
|
|
}
|
|
|
|
case len(assetID) > 0:
|
|
req.Id.Id = &universerpc.ID_AssetId{
|
|
AssetId: assetID,
|
|
}
|
|
|
|
default:
|
|
t.Fatalf("Need either asset ID or group key")
|
|
}
|
|
|
|
ctxb := context.Background()
|
|
var proofResp *universerpc.AssetProofResponse
|
|
err := wait.NoError(func() error {
|
|
var pErr error
|
|
proofResp, pErr = universe.QueryProof(ctxb, req)
|
|
return pErr
|
|
}, defaultTimeout)
|
|
require.NoError(
|
|
t, err, "%v: outpoint=%v, script_key=%x", err, outpoint,
|
|
scriptKey,
|
|
)
|
|
|
|
if len(groupKey) > 0 {
|
|
require.NotNil(t, proofResp.AssetLeaf.Asset.AssetGroup)
|
|
require.Equal(
|
|
t, proofResp.AssetLeaf.Asset.AssetGroup.TweakedGroupKey,
|
|
groupKey,
|
|
)
|
|
} else {
|
|
require.Equal(
|
|
t, proofResp.AssetLeaf.Asset.AssetGenesis.AssetId,
|
|
assetID,
|
|
)
|
|
}
|
|
|
|
a := proofResp.AssetLeaf.Asset
|
|
t.Logf("Proof found for scriptKey=%x, amount=%d", a.ScriptKey, a.Amount)
|
|
|
|
return a
|
|
}
|
|
|
|
func assertPendingChannels(t *testing.T, node *HarnessNode,
|
|
mintedAsset *taprpc.Asset, numChannels int, localSum,
|
|
remoteSum uint64) {
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
pendingChannelsResp, err := node.PendingChannels(
|
|
ctxt, &lnrpc.PendingChannelsRequest{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, pendingChannelsResp.PendingOpenChannels, numChannels)
|
|
|
|
pendingChan := pendingChannelsResp.PendingOpenChannels[0]
|
|
var pendingJSON rfqmsg.JsonAssetChannel
|
|
err = json.Unmarshal(
|
|
pendingChan.Channel.CustomChannelData, &pendingJSON,
|
|
)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(pendingJSON.FundingAssets), 1)
|
|
|
|
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.
|
|
var expectedDecimalDisplay uint8
|
|
if mintedAsset.DecimalDisplay != nil {
|
|
expectedDecimalDisplay = uint8(
|
|
mintedAsset.DecimalDisplay.DecimalDisplay,
|
|
)
|
|
}
|
|
|
|
require.Equal(
|
|
t, expectedDecimalDisplay,
|
|
pendingJSON.FundingAssets[0].DecimalDisplay,
|
|
)
|
|
|
|
// Check the balance of the pending channel.
|
|
assetID := mintedAsset.AssetGenesis.AssetId
|
|
pendingLocalBalance, pendingRemoteBalance, _, _ :=
|
|
getAssetChannelBalance(t, node, [][]byte{assetID}, true)
|
|
require.EqualValues(t, localSum, pendingLocalBalance)
|
|
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,
|
|
channelAssets []*taprpc.Asset) {
|
|
|
|
err := wait.NoError(func() error {
|
|
a, err := getChannelCustomData(src, dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, channelAsset := range channelAssets {
|
|
assetID := channelAsset.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)
|
|
}
|
|
|
|
// Check the decimal display of the channel funding blob. If no
|
|
// explicit value was set, we assume and expect the value of 0.
|
|
// We only need to check the first funding asset, since we
|
|
// enforce them to be the same.
|
|
var expectedDecimalDisplay uint8
|
|
if channelAssets[0].DecimalDisplay != nil {
|
|
expectedDecimalDisplay = uint8(
|
|
channelAssets[0].DecimalDisplay.DecimalDisplay,
|
|
)
|
|
}
|
|
|
|
if a.FundingAssets[0].DecimalDisplay != expectedDecimalDisplay {
|
|
return fmt.Errorf("expected decimal display %d, got %d",
|
|
expectedDecimalDisplay,
|
|
a.FundingAssets[0].DecimalDisplay)
|
|
}
|
|
|
|
return nil
|
|
}, defaultTimeout)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func assertChannelKnown(t *testing.T, node *HarnessNode,
|
|
chanPoint *lnrpc.ChannelPoint) {
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
txid, err := chainhash.NewHash(chanPoint.GetFundingTxidBytes())
|
|
require.NoError(t, err)
|
|
targetChanPoint := fmt.Sprintf(
|
|
"%v:%d", txid.String(), chanPoint.OutputIndex,
|
|
)
|
|
|
|
err = wait.NoError(func() error {
|
|
graphResp, err := node.DescribeGraph(
|
|
ctxt, &lnrpc.ChannelGraphRequest{},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
found := false
|
|
for _, edge := range graphResp.Edges {
|
|
if edge.ChanPoint == targetChanPoint {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return fmt.Errorf("channel %v not found",
|
|
targetChanPoint)
|
|
}
|
|
|
|
return nil
|
|
}, defaultTimeout)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func getChannelCustomData(src, dst *HarnessNode) (*rfqmsg.JsonAssetChannel,
|
|
error) {
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
srcDestChannels, err := src.ListChannels(
|
|
ctxt, &lnrpc.ListChannelsRequest{
|
|
Peer: dst.PubKey[:],
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
assetChannels := fn.Filter(func(c *lnrpc.Channel) bool {
|
|
return len(c.CustomChannelData) > 0
|
|
}, srcDestChannels.Channels)
|
|
|
|
if len(assetChannels) != 1 {
|
|
return nil, fmt.Errorf("expected 1 asset channel, got %d: %v",
|
|
len(assetChannels), spew.Sdump(assetChannels))
|
|
}
|
|
|
|
targetChan := assetChannels[0]
|
|
|
|
var assetData rfqmsg.JsonAssetChannel
|
|
err = json.Unmarshal(targetChan.CustomChannelData, &assetData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to unmarshal asset data: %w",
|
|
err)
|
|
}
|
|
|
|
if len(assetData.FundingAssets) == 0 {
|
|
return nil, fmt.Errorf("expected at least 1 asset, got %d",
|
|
len(assetData.FundingAssets))
|
|
}
|
|
|
|
return &assetData, nil
|
|
}
|
|
|
|
func getAssetChannelBalance(t *testing.T, node *HarnessNode, assetIDs [][]byte,
|
|
pending bool) (uint64, uint64, uint64, uint64) {
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
balance, err := node.ChannelBalance(
|
|
ctxt, &lnrpc.ChannelBalanceRequest{},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// In case there are no channels, the custom channel data will just be
|
|
// empty. Which means the total asset balance is zero.
|
|
if len(balance.CustomChannelData) == 0 {
|
|
return 0, 0, 0, 0
|
|
}
|
|
|
|
var assetBalance rfqmsg.JsonAssetChannelBalances
|
|
err = json.Unmarshal(balance.CustomChannelData, &assetBalance)
|
|
require.NoErrorf(t, err, "json: '%x'", balance.CustomChannelData)
|
|
|
|
balances := assetBalance.OpenChannels
|
|
if pending {
|
|
balances = assetBalance.PendingChannels
|
|
}
|
|
|
|
idMatch := func(assetIDString string) bool {
|
|
for _, groupedID := range assetIDs {
|
|
if assetIDString == hex.EncodeToString(groupedID) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
var localSum, remoteSum uint64
|
|
for assetIDString := range balances {
|
|
if !idMatch(assetIDString) {
|
|
continue
|
|
}
|
|
|
|
localSum += balances[assetIDString].LocalBalance
|
|
remoteSum += balances[assetIDString].RemoteBalance
|
|
}
|
|
|
|
return localSum, remoteSum, balance.LocalBalance.Sat,
|
|
balance.RemoteBalance.Sat
|
|
}
|
|
|
|
func fetchChannel(t *testing.T, node *HarnessNode,
|
|
chanPoint *lnrpc.ChannelPoint) *lnrpc.Channel {
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
channelResp, err := node.ListChannels(ctxt, &lnrpc.ListChannelsRequest{
|
|
ActiveOnly: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
chanFundingHash, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
|
require.NoError(t, err)
|
|
|
|
chanPointStr := fmt.Sprintf("%v:%v", chanFundingHash,
|
|
chanPoint.OutputIndex)
|
|
|
|
var targetChan *lnrpc.Channel
|
|
for _, channel := range channelResp.Channels {
|
|
if channel.ChannelPoint == chanPointStr {
|
|
targetChan = channel
|
|
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, targetChan)
|
|
|
|
return targetChan
|
|
}
|
|
|
|
func assertChannelSatBalance(t *testing.T, node *HarnessNode,
|
|
chanPoint *lnrpc.ChannelPoint, local, remote int64) {
|
|
|
|
targetChan := fetchChannel(t, node, chanPoint)
|
|
|
|
require.InDelta(t, local, targetChan.LocalBalance, 1)
|
|
require.InDelta(t, remote, targetChan.RemoteBalance, 1)
|
|
}
|
|
|
|
func assertChannelAssetBalance(t *testing.T, node *HarnessNode,
|
|
chanPoint *lnrpc.ChannelPoint, local, remote uint64) {
|
|
|
|
targetChan := fetchChannel(t, node, chanPoint)
|
|
|
|
var assetBalance rfqmsg.JsonAssetChannel
|
|
err := json.Unmarshal(targetChan.CustomChannelData, &assetBalance)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, assetBalance.FundingAssets, 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
|
|
// 1000 milli-satoshi base fee) to the given milli-satoshi amount.
|
|
func addRoutingFee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {
|
|
return amt + (amt / 1000_000) + 1000
|
|
}
|
|
|
|
func sendAssetKeySendPayment(t *testing.T, src, dst *HarnessNode, amt uint64,
|
|
assetID []byte, btcAmt fn.Option[int64], opts ...payOpt) {
|
|
|
|
cfg := defaultPayConfig()
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
srcTapd := newTapClient(t, src)
|
|
|
|
// Read out the custom preimage for the keysend payment.
|
|
var preimage lntypes.Preimage
|
|
_, err := rand.Read(preimage[:])
|
|
require.NoError(t, err)
|
|
|
|
hash := preimage.Hash()
|
|
|
|
// Set the preimage. If the user supplied a preimage with the data
|
|
// flag, the preimage that is set here will be overwritten later.
|
|
customRecords := make(map[uint64][]byte)
|
|
customRecords[record.KeySendType] = preimage[:]
|
|
|
|
sendReq := &routerrpc.SendPaymentRequest{
|
|
Dest: dst.PubKey[:],
|
|
Amt: btcAmt.UnwrapOr(500),
|
|
DestCustomRecords: customRecords,
|
|
PaymentHash: hash[:],
|
|
TimeoutSeconds: int32(PaymentTimeout.Seconds()),
|
|
MaxParts: cfg.maxShards,
|
|
}
|
|
|
|
request := &tchrpc.SendPaymentRequest{
|
|
AssetAmount: amt,
|
|
PaymentRequest: sendReq,
|
|
}
|
|
|
|
switch {
|
|
case len(cfg.groupKey) > 0:
|
|
request.GroupKey = cfg.groupKey
|
|
|
|
default:
|
|
request.AssetId = assetID
|
|
}
|
|
|
|
stream, err := srcTapd.SendPayment(ctxt, request)
|
|
require.NoError(t, err)
|
|
|
|
result, err := getAssetPaymentResult(stream, false)
|
|
require.NoError(t, err)
|
|
if result.Status == lnrpc.Payment_FAILED {
|
|
t.Logf("Failure reason: %v", result.FailureReason)
|
|
}
|
|
require.Equal(t, cfg.payStatus, result.Status)
|
|
require.Equal(t, cfg.failureReason, result.FailureReason)
|
|
}
|
|
|
|
func sendKeySendPayment(t *testing.T, src, dst *HarnessNode,
|
|
amt btcutil.Amount) {
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
// Read out the custom preimage for the keysend payment.
|
|
var preimage lntypes.Preimage
|
|
_, err := rand.Read(preimage[:])
|
|
require.NoError(t, err)
|
|
|
|
hash := preimage.Hash()
|
|
|
|
// Set the preimage. If the user supplied a preimage with the data
|
|
// flag, the preimage that is set here will be overwritten later.
|
|
customRecords := make(map[uint64][]byte)
|
|
customRecords[record.KeySendType] = preimage[:]
|
|
|
|
req := &routerrpc.SendPaymentRequest{
|
|
Dest: dst.PubKey[:],
|
|
Amt: int64(amt),
|
|
DestCustomRecords: customRecords,
|
|
PaymentHash: hash[:],
|
|
TimeoutSeconds: int32(PaymentTimeout.Seconds()),
|
|
}
|
|
|
|
stream, err := src.RouterClient.SendPaymentV2(ctxt, req)
|
|
require.NoError(t, err)
|
|
|
|
result, err := getFinalPaymentResult(stream)
|
|
require.NoError(t, err)
|
|
require.Equal(t, lnrpc.Payment_SUCCEEDED, result.Status)
|
|
}
|
|
|
|
func createAndPayNormalInvoiceWithBtc(t *testing.T, src, dst *HarnessNode,
|
|
amountSat btcutil.Amount) {
|
|
|
|
invoiceResp := createNormalInvoice(t, dst, amountSat)
|
|
|
|
payInvoiceWithSatoshi(t, src, invoiceResp)
|
|
}
|
|
|
|
func createNormalInvoice(t *testing.T, dst *HarnessNode,
|
|
amountSat btcutil.Amount) *lnrpc.AddInvoiceResponse {
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
expirySeconds := 10
|
|
invoiceResp, err := dst.AddInvoice(ctxt, &lnrpc.Invoice{
|
|
Value: int64(amountSat),
|
|
Memo: "normal invoice",
|
|
Expiry: int64(expirySeconds),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return invoiceResp
|
|
}
|
|
|
|
func createAndPayNormalInvoice(t *testing.T, src, rfqPeer, dst *HarnessNode,
|
|
amountSat btcutil.Amount, assetID []byte, opts ...payOpt) uint64 {
|
|
|
|
invoiceResp := createNormalInvoice(t, dst, amountSat)
|
|
numUnits, _ := payInvoiceWithAssets(
|
|
t, src, rfqPeer, invoiceResp.PaymentRequest, assetID, opts...,
|
|
)
|
|
|
|
return numUnits
|
|
}
|
|
|
|
func payInvoiceWithSatoshi(t *testing.T, payer *HarnessNode,
|
|
invoice *lnrpc.AddInvoiceResponse, opts ...payOpt) {
|
|
|
|
payPayReqWithSatoshi(t, payer, invoice.PaymentRequest, opts...)
|
|
}
|
|
|
|
func payPayReqWithSatoshi(t *testing.T, payer *HarnessNode, payReq string,
|
|
opts ...payOpt) {
|
|
|
|
cfg := defaultPayConfig()
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
sendReq := &routerrpc.SendPaymentRequest{
|
|
PaymentRequest: payReq,
|
|
TimeoutSeconds: int32(PaymentTimeout.Seconds()),
|
|
FeeLimitMsat: 1_000_000,
|
|
MaxParts: cfg.maxShards,
|
|
}
|
|
|
|
if cfg.smallShards {
|
|
sendReq.MaxShardSizeMsat = 80_000_000
|
|
}
|
|
|
|
stream, err := payer.RouterClient.SendPaymentV2(ctxt, sendReq)
|
|
require.NoError(t, err)
|
|
|
|
if cfg.payStatus == lnrpc.Payment_IN_FLIGHT {
|
|
t.Logf("Waiting for initial stream response...")
|
|
result, err := stream.Recv()
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, cfg.payStatus, result.Status)
|
|
|
|
return
|
|
}
|
|
|
|
result, err := getFinalPaymentResult(stream)
|
|
if cfg.errSubStr != "" {
|
|
require.ErrorContains(t, err, cfg.errSubStr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, cfg.payStatus, result.Status)
|
|
require.Equal(t, cfg.failureReason, result.FailureReason)
|
|
}
|
|
}
|
|
|
|
func payInvoiceWithSatoshiLastHop(t *testing.T, payer *HarnessNode,
|
|
invoice *lnrpc.AddInvoiceResponse, hops [][]byte, opts ...payOpt) {
|
|
|
|
cfg := defaultPayConfig()
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
decodedInvoice, err := payer.DecodePayReq(ctxt, &lnrpc.PayReqString{
|
|
PayReq: invoice.PaymentRequest,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
routeRes, err := payer.RouterClient.BuildRoute(
|
|
ctxb, &routerrpc.BuildRouteRequest{
|
|
AmtMsat: decodedInvoice.NumMsat,
|
|
PaymentAddr: invoice.PaymentAddr,
|
|
HopPubkeys: hops,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
res, err := payer.RouterClient.SendToRouteV2(
|
|
ctxt, &routerrpc.SendToRouteRequest{
|
|
PaymentHash: invoice.RHash,
|
|
Route: routeRes.Route,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
switch cfg.payStatus {
|
|
case lnrpc.Payment_FAILED:
|
|
require.NoError(t, err)
|
|
require.Equal(t, lnrpc.HTLCAttempt_FAILED, res.Status)
|
|
require.NotNil(t, res.Failure)
|
|
require.Nil(t, res.Preimage)
|
|
|
|
case lnrpc.Payment_SUCCEEDED:
|
|
require.NoError(t, err)
|
|
require.Equal(t, lnrpc.HTLCAttempt_SUCCEEDED, res.Status)
|
|
}
|
|
}
|
|
|
|
type payConfig struct {
|
|
smallShards bool
|
|
maxShards uint32
|
|
errSubStr string
|
|
allowOverpay bool
|
|
feeLimit lnwire.MilliSatoshi
|
|
destCustomRecords map[uint64][]byte
|
|
payStatus lnrpc.Payment_PaymentStatus
|
|
failureReason lnrpc.PaymentFailureReason
|
|
rfq fn.Option[rfqmsg.ID]
|
|
groupKey []byte
|
|
}
|
|
|
|
func defaultPayConfig() *payConfig {
|
|
return &payConfig{
|
|
smallShards: false,
|
|
errSubStr: "",
|
|
feeLimit: 1_000_000,
|
|
payStatus: lnrpc.Payment_SUCCEEDED,
|
|
failureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_NONE,
|
|
}
|
|
}
|
|
|
|
type payOpt func(*payConfig)
|
|
|
|
func withSmallShards() payOpt {
|
|
return func(c *payConfig) {
|
|
c.smallShards = true
|
|
}
|
|
}
|
|
|
|
func withMaxShards(maxShards uint32) payOpt {
|
|
return func(c *payConfig) {
|
|
c.maxShards = maxShards
|
|
}
|
|
}
|
|
|
|
func withPayErrSubStr(errSubStr string) payOpt {
|
|
return func(c *payConfig) {
|
|
c.errSubStr = errSubStr
|
|
}
|
|
}
|
|
|
|
func withFailure(status lnrpc.Payment_PaymentStatus,
|
|
reason lnrpc.PaymentFailureReason) payOpt {
|
|
|
|
return func(c *payConfig) {
|
|
c.payStatus = status
|
|
c.failureReason = reason
|
|
}
|
|
}
|
|
|
|
func withRFQ(rfqID rfqmsg.ID) payOpt {
|
|
return func(c *payConfig) {
|
|
c.rfq = fn.Some(rfqID)
|
|
}
|
|
}
|
|
|
|
func withFeeLimit(limit lnwire.MilliSatoshi) payOpt {
|
|
return func(c *payConfig) {
|
|
c.feeLimit = limit
|
|
}
|
|
}
|
|
|
|
func withDestCustomRecords(records map[uint64][]byte) payOpt {
|
|
return func(c *payConfig) {
|
|
c.destCustomRecords = records
|
|
}
|
|
}
|
|
|
|
func withAllowOverpay() payOpt {
|
|
return func(c *payConfig) {
|
|
c.allowOverpay = true
|
|
}
|
|
}
|
|
|
|
func withGroupKey(groupKey []byte) payOpt {
|
|
return func(c *payConfig) {
|
|
c.groupKey = groupKey
|
|
}
|
|
}
|
|
|
|
func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode,
|
|
payReq string, assetID []byte,
|
|
opts ...payOpt) (uint64, rfqmath.BigIntFixedPoint) {
|
|
|
|
cfg := defaultPayConfig()
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
payerTapd := newTapClient(t, payer)
|
|
|
|
decodedInvoice, err := payer.DecodePayReq(ctxt, &lnrpc.PayReqString{
|
|
PayReq: payReq,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
sendReq := &routerrpc.SendPaymentRequest{
|
|
PaymentRequest: payReq,
|
|
TimeoutSeconds: int32(PaymentTimeout.Seconds()),
|
|
FeeLimitMsat: int64(cfg.feeLimit),
|
|
DestCustomRecords: cfg.destCustomRecords,
|
|
MaxParts: cfg.maxShards,
|
|
}
|
|
|
|
if cfg.smallShards {
|
|
sendReq.MaxShardSizeMsat = 80_000_000
|
|
}
|
|
|
|
var rfqBytes []byte
|
|
cfg.rfq.WhenSome(func(i rfqmsg.ID) {
|
|
rfqBytes = make([]byte, len(i[:]))
|
|
copy(rfqBytes, i[:])
|
|
})
|
|
|
|
request := &tchrpc.SendPaymentRequest{
|
|
PeerPubkey: rfqPeer.PubKey[:],
|
|
PaymentRequest: sendReq,
|
|
RfqId: rfqBytes,
|
|
AllowOverpay: cfg.allowOverpay,
|
|
}
|
|
|
|
switch {
|
|
case len(cfg.groupKey) > 0:
|
|
request.GroupKey = cfg.groupKey
|
|
|
|
default:
|
|
request.AssetId = assetID
|
|
}
|
|
|
|
stream, err := payerTapd.SendPayment(ctxt, request)
|
|
require.NoError(t, err)
|
|
|
|
// If an error is returned by the RPC method (meaning the stream itself
|
|
// was established, no network or auth error), we expect the error to be
|
|
// returned on the first read on the stream.
|
|
if cfg.errSubStr != "" {
|
|
_, err := stream.Recv()
|
|
require.ErrorContains(t, err, cfg.errSubStr)
|
|
|
|
return 0, rfqmath.BigIntFixedPoint{}
|
|
}
|
|
|
|
var (
|
|
numUnits uint64
|
|
rateVal rfqmath.FixedPoint[rfqmath.BigInt]
|
|
)
|
|
if cfg.rfq.IsNone() {
|
|
// We want to receive the accepted quote message first, so we
|
|
// know how many assets we're going to pay.
|
|
quoteMsg, err := stream.Recv()
|
|
require.NoError(t, err)
|
|
acceptedQuote := quoteMsg.GetAcceptedSellOrder()
|
|
require.NotNil(t, acceptedQuote)
|
|
|
|
peerPubKey := acceptedQuote.Peer
|
|
require.Equal(t, peerPubKey, rfqPeer.PubKeyStr)
|
|
|
|
rpcRate := acceptedQuote.BidAssetRate
|
|
rate, err := rpcutils.UnmarshalRfqFixedPoint(rpcRate)
|
|
require.NoError(t, err)
|
|
|
|
rateVal = *rate
|
|
|
|
t.Logf("Got quote for %v asset units per BTC", rate)
|
|
|
|
amountMsat := lnwire.MilliSatoshi(decodedInvoice.NumMsat)
|
|
milliSatsFP := rfqmath.MilliSatoshiToUnits(amountMsat, *rate)
|
|
numUnits = milliSatsFP.ScaleTo(0).ToUint64()
|
|
msatPerUnit := float64(decodedInvoice.NumMsat) /
|
|
float64(numUnits)
|
|
t.Logf("Got quote for %v asset units at %3f msat/unit from "+
|
|
"peer %s with SCID %d", numUnits, msatPerUnit,
|
|
peerPubKey, acceptedQuote.Scid)
|
|
}
|
|
|
|
result, err := getAssetPaymentResult(
|
|
stream, cfg.payStatus == lnrpc.Payment_IN_FLIGHT,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, cfg.payStatus, result.Status)
|
|
require.Equal(t, cfg.failureReason, result.FailureReason)
|
|
|
|
return numUnits, rateVal
|
|
}
|
|
|
|
type invoiceConfig struct {
|
|
errSubStr string
|
|
groupKey []byte
|
|
routeHints []*lnrpc.RouteHint
|
|
}
|
|
|
|
func defaultInvoiceConfig() *invoiceConfig {
|
|
return &invoiceConfig{
|
|
errSubStr: "",
|
|
}
|
|
}
|
|
|
|
type invoiceOpt func(*invoiceConfig)
|
|
|
|
func withInvoiceErrSubStr(errSubStr string) invoiceOpt {
|
|
return func(c *invoiceConfig) {
|
|
c.errSubStr = errSubStr
|
|
}
|
|
}
|
|
|
|
func withInvGroupKey(groupKey []byte) invoiceOpt {
|
|
return func(c *invoiceConfig) {
|
|
c.groupKey = groupKey
|
|
}
|
|
}
|
|
|
|
func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode,
|
|
assetAmount uint64, assetID []byte,
|
|
opts ...invoiceOpt) *lnrpc.AddInvoiceResponse {
|
|
|
|
cfg := defaultInvoiceConfig()
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
timeoutSeconds := int64(rfq.DefaultInvoiceExpiry.Seconds())
|
|
|
|
t.Logf("Asking peer %x for quote to buy assets to receive for "+
|
|
"invoice over %d units; waiting up to %ds",
|
|
dstRfqPeer.PubKey[:], assetAmount, timeoutSeconds)
|
|
|
|
dstTapd := newTapClient(t, dst)
|
|
|
|
request := &tchrpc.AddInvoiceRequest{
|
|
GroupKey: cfg.groupKey,
|
|
AssetAmount: assetAmount,
|
|
PeerPubkey: dstRfqPeer.PubKey[:],
|
|
InvoiceRequest: &lnrpc.Invoice{
|
|
Memo: fmt.Sprintf("this is an asset invoice for "+
|
|
"%d units", assetAmount),
|
|
Expiry: timeoutSeconds,
|
|
},
|
|
}
|
|
|
|
switch {
|
|
case len(cfg.groupKey) > 0:
|
|
request.GroupKey = cfg.groupKey
|
|
|
|
default:
|
|
request.AssetId = assetID
|
|
}
|
|
|
|
resp, err := dstTapd.AddInvoice(ctxt, request)
|
|
if cfg.errSubStr != "" {
|
|
require.ErrorContains(t, err, cfg.errSubStr)
|
|
|
|
return nil
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
decodedInvoice, err := dst.DecodePayReq(ctxt, &lnrpc.PayReqString{
|
|
PayReq: resp.InvoiceResult.PaymentRequest,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
rpcRate := resp.AcceptedBuyQuote.AskAssetRate
|
|
rate, err := rpcutils.UnmarshalRfqFixedPoint(rpcRate)
|
|
require.NoError(t, err)
|
|
|
|
t.Logf("Got quote for %v asset units per BTC", rate)
|
|
|
|
assetUnits := rfqmath.NewBigIntFixedPoint(assetAmount, 0)
|
|
numMSats := rfqmath.UnitsToMilliSatoshi(assetUnits, *rate)
|
|
mSatPerUnit := float64(decodedInvoice.NumMsat) / float64(assetAmount)
|
|
|
|
require.EqualValues(t, numMSats, decodedInvoice.NumMsat)
|
|
|
|
t.Logf("Got quote for %d mSats at %3f msat/unit from peer %x with "+
|
|
"SCID %d", decodedInvoice.NumMsat, mSatPerUnit,
|
|
dstRfqPeer.PubKey[:], resp.AcceptedBuyQuote.Scid)
|
|
|
|
return resp.InvoiceResult
|
|
}
|
|
|
|
// assertInvoiceHtlcAssets makes sure the invoice with the given hash shows the
|
|
// individual HTLCs that arrived for it and that they show the correct asset
|
|
// amounts for the given ID when decoded.
|
|
func assertInvoiceHtlcAssets(t *testing.T, node *HarnessNode,
|
|
addedInvoice *lnrpc.AddInvoiceResponse, assetID []byte, groupID []byte,
|
|
assetAmount uint64) {
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
invoice, err := node.InvoicesClient.LookupInvoiceV2(
|
|
ctxt, &invoicesrpc.LookupInvoiceMsg{
|
|
InvoiceRef: &invoicesrpc.LookupInvoiceMsg_PaymentAddr{
|
|
PaymentAddr: addedInvoice.PaymentAddr,
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, invoice.Htlcs)
|
|
|
|
t.Logf("Asset invoice: %v", toProtoJSON(t, invoice))
|
|
|
|
var targetID string
|
|
switch {
|
|
case len(groupID) > 0:
|
|
targetID = hex.EncodeToString(groupID)
|
|
|
|
case len(assetID) > 0:
|
|
targetID = hex.EncodeToString(assetID)
|
|
}
|
|
|
|
var totalAssetAmount uint64
|
|
for _, htlc := range invoice.Htlcs {
|
|
require.NotEmpty(t, htlc.CustomChannelData)
|
|
|
|
jsonHtlc := &rfqmsg.JsonHtlc{}
|
|
err := json.Unmarshal(htlc.CustomChannelData, jsonHtlc)
|
|
require.NoError(t, err)
|
|
|
|
for _, balance := range jsonHtlc.Balances {
|
|
if balance.AssetID != targetID {
|
|
continue
|
|
}
|
|
|
|
totalAssetAmount += balance.Amount
|
|
}
|
|
}
|
|
|
|
// Due to rounding we allow up to 1 unit of error.
|
|
require.InDelta(t, assetAmount, totalAssetAmount, 1)
|
|
}
|
|
|
|
// assertPaymentHtlcAssets makes sure the payment with the given hash shows the
|
|
// individual HTLCs that arrived for it and that they show the correct asset
|
|
// amounts for the given ID when decoded.
|
|
func assertPaymentHtlcAssets(t *testing.T, node *HarnessNode, payHash []byte,
|
|
assetID []byte, groupID []byte, assetAmount uint64) {
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
stream, err := node.RouterClient.TrackPaymentV2(
|
|
ctxt, &routerrpc.TrackPaymentRequest{
|
|
PaymentHash: payHash,
|
|
NoInflightUpdates: true,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
payment, err := stream.Recv()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, payment)
|
|
require.NotEmpty(t, payment.Htlcs)
|
|
|
|
t.Logf("Asset payment: %v", toProtoJSON(t, payment))
|
|
|
|
var targetID string
|
|
switch {
|
|
case len(groupID) > 0:
|
|
targetID = hex.EncodeToString(groupID)
|
|
|
|
case len(assetID) > 0:
|
|
targetID = hex.EncodeToString(assetID)
|
|
}
|
|
|
|
var totalAssetAmount uint64
|
|
for _, htlc := range payment.Htlcs {
|
|
require.NotNil(t, htlc.Route)
|
|
require.NotEmpty(t, htlc.Route.CustomChannelData)
|
|
|
|
jsonHtlc := &rfqmsg.JsonHtlc{}
|
|
err := json.Unmarshal(htlc.Route.CustomChannelData, jsonHtlc)
|
|
require.NoError(t, err)
|
|
|
|
for _, balance := range jsonHtlc.Balances {
|
|
if balance.AssetID != targetID {
|
|
continue
|
|
}
|
|
|
|
totalAssetAmount += balance.Amount
|
|
}
|
|
}
|
|
|
|
// Due to rounding we allow up to 1 unit of error.
|
|
require.InDelta(t, assetAmount, totalAssetAmount, 1)
|
|
}
|
|
|
|
type assetHodlInvoice struct {
|
|
preimage lntypes.Preimage
|
|
payReq string
|
|
}
|
|
|
|
func createAssetHodlInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode,
|
|
assetAmount uint64, assetID []byte,
|
|
opts ...invoiceOpt) assetHodlInvoice {
|
|
|
|
cfg := defaultInvoiceConfig()
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
timeoutSeconds := int64(rfq.DefaultInvoiceExpiry.Seconds())
|
|
|
|
t.Logf("Asking peer %x for quote to buy assets to receive for "+
|
|
"invoice for %d units; waiting up to %ds",
|
|
dstRfqPeer.PubKey[:], assetAmount, timeoutSeconds)
|
|
|
|
dstTapd := newTapClient(t, dst)
|
|
|
|
// As this is a hodl invoice, we'll also need to create a preimage
|
|
// external to lnd.
|
|
var preimage lntypes.Preimage
|
|
_, err := rand.Read(preimage[:])
|
|
require.NoError(t, err)
|
|
|
|
payHash := preimage.Hash()
|
|
request := &tchrpc.AddInvoiceRequest{
|
|
AssetAmount: assetAmount,
|
|
PeerPubkey: dstRfqPeer.PubKey[:],
|
|
InvoiceRequest: &lnrpc.Invoice{
|
|
Memo: fmt.Sprintf("this is an asset invoice for "+
|
|
"%d units", assetAmount),
|
|
Expiry: timeoutSeconds,
|
|
},
|
|
HodlInvoice: &tchrpc.HodlInvoice{
|
|
PaymentHash: payHash[:],
|
|
},
|
|
}
|
|
|
|
switch {
|
|
case len(cfg.groupKey) > 0:
|
|
request.GroupKey = cfg.groupKey
|
|
|
|
default:
|
|
request.AssetId = assetID
|
|
}
|
|
|
|
resp, err := dstTapd.AddInvoice(ctxt, request)
|
|
require.NoError(t, err)
|
|
|
|
decodedInvoice, err := dst.DecodePayReq(ctxt, &lnrpc.PayReqString{
|
|
PayReq: resp.InvoiceResult.PaymentRequest,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
rpcRate := resp.AcceptedBuyQuote.AskAssetRate
|
|
rate, err := rpcutils.UnmarshalRfqFixedPoint(rpcRate)
|
|
require.NoError(t, err)
|
|
|
|
assetUnits := rfqmath.NewBigIntFixedPoint(assetAmount, 0)
|
|
numMSats := rfqmath.UnitsToMilliSatoshi(assetUnits, *rate)
|
|
mSatPerUnit := float64(decodedInvoice.NumMsat) / float64(assetAmount)
|
|
|
|
require.EqualValues(t, uint64(numMSats), uint64(decodedInvoice.NumMsat))
|
|
|
|
t.Logf("Got quote for %d msat at %v msat/unit from peer %x with SCID "+
|
|
"%d", decodedInvoice.NumMsat, mSatPerUnit, dstRfqPeer.PubKey[:],
|
|
resp.AcceptedBuyQuote.Scid)
|
|
|
|
return assetHodlInvoice{
|
|
preimage: preimage,
|
|
payReq: resp.InvoiceResult.PaymentRequest,
|
|
}
|
|
}
|
|
|
|
func waitForSendEvent(t *testing.T,
|
|
sendEvents taprpc.TaprootAssets_SubscribeSendEventsClient,
|
|
expectedState tapfreighter.SendState) {
|
|
|
|
t.Helper()
|
|
|
|
for {
|
|
sendEvent, err := sendEvents.Recv()
|
|
require.NoError(t, err)
|
|
|
|
t.Logf("Received send event: %v", sendEvent.SendState)
|
|
if sendEvent.SendState == expectedState.String() {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// coOpCloseBalanceCheck is a function type that can be passed into
|
|
// closeAssetChannelAndAsset to asset the final balance of the closing
|
|
// transaction.
|
|
type coOpCloseBalanceCheck func(t *testing.T, local, remote *HarnessNode,
|
|
closeTx *wire.MsgTx, closeUpdate *lnrpc.ChannelCloseUpdate,
|
|
assetIDs [][]byte, groupKey []byte, universeTap *tapClient)
|
|
|
|
// noOpCoOpCloseBalanceCheck is a no-op implementation of the co-op close
|
|
// balance check that can be used in tests.
|
|
func noOpCoOpCloseBalanceCheck(_ *testing.T, _, _ *HarnessNode, _ *wire.MsgTx,
|
|
_ *lnrpc.ChannelCloseUpdate, _ [][]byte, _ []byte, _ *tapClient) {
|
|
|
|
// This is a no-op function.
|
|
}
|
|
|
|
// closeAssetChannelAndAssert closes the channel between the local and remote
|
|
// node and asserts the final balances of the closing transaction.
|
|
func closeAssetChannelAndAssert(t *harnessTest, net *NetworkHarness,
|
|
local, remote *HarnessNode, chanPoint *lnrpc.ChannelPoint,
|
|
assetIDs [][]byte, groupKey []byte, universeTap *tapClient,
|
|
balanceCheck coOpCloseBalanceCheck) {
|
|
|
|
t.t.Helper()
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
closeStream, _, err := t.lndHarness.CloseChannel(
|
|
local, chanPoint, false,
|
|
)
|
|
require.NoError(t.t, err)
|
|
|
|
localTapd := newTapClient(t.t, local)
|
|
sendEvents, err := localTapd.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 := t.lndHarness.WaitForChannelClose(closeStream)
|
|
require.NoError(t.t, err)
|
|
|
|
closeTxid, err := chainhash.NewHash(closeUpdate.ClosingTxid)
|
|
require.NoError(t.t, err)
|
|
|
|
closeTransaction := t.lndHarness.Miner.GetRawTransaction(*closeTxid)
|
|
closeTx := closeTransaction.MsgTx()
|
|
t.Logf("Channel closed with txid: %v", closeTxid)
|
|
t.Logf("Close transaction: %v", spew.Sdump(closeTx))
|
|
|
|
waitForSendEvent(t.t, sendEvents, tapfreighter.SendStateComplete)
|
|
|
|
// Check the final balance of the closing transaction.
|
|
balanceCheck(
|
|
t.t, local, remote, closeTx, closeUpdate, assetIDs, groupKey,
|
|
universeTap,
|
|
)
|
|
|
|
assertClosedChannelAssetData(t.t, local, chanPoint)
|
|
assertClosedChannelAssetData(t.t, remote, chanPoint)
|
|
}
|
|
|
|
// assertDefaultCoOpCloseBalance returns a default implementation of the co-op
|
|
// close balance check that can be used in tests. It assumes the initiator has
|
|
// both an asset and BTC balance left, while the responder's balance can be
|
|
// specified with the boolean variables.
|
|
func assertDefaultCoOpCloseBalance(remoteBtcBalance,
|
|
remoteAssetBalance bool) coOpCloseBalanceCheck {
|
|
|
|
return func(t *testing.T, local, remote *HarnessNode,
|
|
closeTx *wire.MsgTx, closeUpdate *lnrpc.ChannelCloseUpdate,
|
|
assetIDs [][]byte, groupKey []byte, universeTap *tapClient) {
|
|
|
|
defaultCoOpCloseBalanceCheck(
|
|
t, local, remote, closeTx, closeUpdate, assetIDs,
|
|
groupKey, universeTap, remoteBtcBalance,
|
|
remoteAssetBalance,
|
|
)
|
|
}
|
|
}
|
|
|
|
// defaultCoOpCloseBalanceCheck is a default implementation of the co-op close
|
|
// balance check that can be used in tests. It assumes the initiator has both
|
|
// an asset and BTC balance left, while the responder's balance can be specified
|
|
// with the boolean variables.
|
|
func defaultCoOpCloseBalanceCheck(t *testing.T, local, remote *HarnessNode,
|
|
closeTx *wire.MsgTx, closeUpdate *lnrpc.ChannelCloseUpdate,
|
|
assetIDs [][]byte, groupKey []byte, universeTap *tapClient,
|
|
remoteBtcBalance, remoteAssetBalance bool) {
|
|
|
|
// With the channel closed, we'll now assert that the co-op close
|
|
// transaction was inserted into the local universe.
|
|
//
|
|
// We expect that at most four outputs exist: one for the local asset
|
|
// output, one for the remote asset output, one for the remote BTC
|
|
// channel balance and one for the remote BTC channel balance.
|
|
//
|
|
// Those outputs are only present if the respective party has a
|
|
// non-dust balance.
|
|
numOutputs := 2
|
|
additionalOutputs := 1
|
|
if remoteBtcBalance {
|
|
numOutputs++
|
|
}
|
|
if remoteAssetBalance {
|
|
numOutputs++
|
|
additionalOutputs++
|
|
}
|
|
|
|
closeTxid := closeTx.TxHash()
|
|
require.Len(t, closeTx.TxOut, numOutputs)
|
|
|
|
outIdx := 0
|
|
dummyAmt := int64(1000)
|
|
require.LessOrEqual(t, closeTx.TxOut[outIdx].Value, dummyAmt)
|
|
|
|
if remoteAssetBalance {
|
|
outIdx++
|
|
require.LessOrEqual(t, closeTx.TxOut[outIdx].Value, dummyAmt)
|
|
}
|
|
|
|
// We also require there to be at most two additional outputs, one for
|
|
// each of the asset outputs with balance.
|
|
require.Len(t, closeUpdate.AdditionalOutputs, additionalOutputs)
|
|
|
|
var remoteCloseOut *lnrpc.CloseOutput
|
|
if remoteBtcBalance {
|
|
// The remote node has received a couple of HTLCs with an above
|
|
// dust value, so it should also have accumulated a non-dust
|
|
// balance, even after subtracting 1k sats for the asset output.
|
|
remoteCloseOut = closeUpdate.RemoteCloseOutput
|
|
require.NotNil(t, remoteCloseOut)
|
|
|
|
outIdx++
|
|
require.EqualValues(
|
|
t, remoteCloseOut.AmountSat-dummyAmt,
|
|
closeTx.TxOut[outIdx].Value,
|
|
)
|
|
} else if remoteAssetBalance {
|
|
// The remote node has received a couple of HTLCs but not enough
|
|
// to go above dust. So it should still have an asset balance
|
|
// that we can verify.
|
|
remoteCloseOut = closeUpdate.RemoteCloseOutput
|
|
require.NotNil(t, remoteCloseOut)
|
|
}
|
|
|
|
// The local node should have received the local BTC balance minus the
|
|
// TX fees and 1k sats for the asset output.
|
|
localCloseOut := closeUpdate.LocalCloseOutput
|
|
require.NotNil(t, localCloseOut)
|
|
outIdx++
|
|
require.Greater(
|
|
t, closeTx.TxOut[outIdx].Value,
|
|
localCloseOut.AmountSat-dummyAmt,
|
|
)
|
|
|
|
// Find out which of the additional outputs is the local one and which
|
|
// is the remote.
|
|
localAuxOut := closeUpdate.AdditionalOutputs[0]
|
|
|
|
var remoteAuxOut *lnrpc.CloseOutput
|
|
if remoteAssetBalance {
|
|
remoteAuxOut = closeUpdate.AdditionalOutputs[1]
|
|
}
|
|
if !localAuxOut.IsLocal && remoteAuxOut != nil {
|
|
localAuxOut, remoteAuxOut = remoteAuxOut, localAuxOut
|
|
}
|
|
|
|
// The first two transaction outputs should be the additional outputs
|
|
// as identified by the pk scripts in the close update.
|
|
localAssetIndex, remoteAssetIndex := 1, 0
|
|
if bytes.Equal(closeTx.TxOut[0].PkScript, localAuxOut.PkScript) {
|
|
localAssetIndex, remoteAssetIndex = 0, 1
|
|
}
|
|
|
|
if remoteAuxOut != nil {
|
|
require.Equal(
|
|
t, remoteAuxOut.PkScript,
|
|
closeTx.TxOut[remoteAssetIndex].PkScript,
|
|
)
|
|
}
|
|
|
|
require.Equal(
|
|
t, localAuxOut.PkScript,
|
|
closeTx.TxOut[localAssetIndex].PkScript,
|
|
)
|
|
|
|
// We now verify the arrival of the local balance asset proof at the
|
|
// universe server.
|
|
var localAssetCloseOut rfqmsg.JsonCloseOutput
|
|
err := json.Unmarshal(
|
|
localCloseOut.CustomChannelData, &localAssetCloseOut,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
assetIDStrings := fn.Map(hex.EncodeToString, assetIDs)
|
|
for assetIDStr, scriptKeyStr := range localAssetCloseOut.ScriptKeys {
|
|
scriptKeyBytes, err := hex.DecodeString(scriptKeyStr)
|
|
require.NoError(t, err)
|
|
|
|
require.Contains(t, assetIDStrings, assetIDStr)
|
|
|
|
assetID, err := hex.DecodeString(assetIDStr)
|
|
require.NoError(t, err)
|
|
|
|
a := assertUniverseProofExists(
|
|
t, universeTap, assetID, groupKey, scriptKeyBytes,
|
|
fmt.Sprintf("%v:%v", closeTxid, localAssetIndex),
|
|
)
|
|
|
|
localTapd := newTapClient(t, local)
|
|
itest.AssertBalances(
|
|
t, localTapd, a.Amount, itest.WithAssetID(assetID),
|
|
itest.WithScriptKeyType(asset.ScriptKeyBip86),
|
|
itest.WithScriptKey(scriptKeyBytes),
|
|
)
|
|
}
|
|
|
|
// If there is no remote asset balance, we're done.
|
|
if !remoteAssetBalance {
|
|
return
|
|
}
|
|
|
|
// At this point the remote close output should be defined, otherwise
|
|
// something went wrong.
|
|
require.NotNil(t, remoteCloseOut)
|
|
|
|
// And then we verify the arrival of the remote balance asset proof at
|
|
// the universe server as well.
|
|
var remoteAssetCloseOut rfqmsg.JsonCloseOutput
|
|
err = json.Unmarshal(
|
|
remoteCloseOut.CustomChannelData, &remoteAssetCloseOut,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
for assetIDStr, scriptKeyStr := range remoteAssetCloseOut.ScriptKeys {
|
|
scriptKeyBytes, err := hex.DecodeString(scriptKeyStr)
|
|
require.NoError(t, err)
|
|
|
|
require.Contains(t, assetIDStrings, assetIDStr)
|
|
|
|
assetID, err := hex.DecodeString(assetIDStr)
|
|
require.NoError(t, err)
|
|
|
|
a := assertUniverseProofExists(
|
|
t, universeTap, assetID, groupKey, scriptKeyBytes,
|
|
fmt.Sprintf("%v:%v", closeTxid, remoteAssetIndex),
|
|
)
|
|
|
|
remoteTapd := newTapClient(t, remote)
|
|
itest.AssertBalances(
|
|
t, remoteTapd, a.Amount, itest.WithAssetID(assetID),
|
|
itest.WithScriptKeyType(asset.ScriptKeyBip86),
|
|
itest.WithScriptKey(scriptKeyBytes),
|
|
)
|
|
}
|
|
}
|
|
|
|
// initiatorZeroAssetBalanceCoOpBalanceCheck is a co-op close balance check
|
|
// function that can be used when the initiator has a zero asset balance.
|
|
func initiatorZeroAssetBalanceCoOpBalanceCheck(t *testing.T, _,
|
|
remote *HarnessNode, closeTx *wire.MsgTx,
|
|
closeUpdate *lnrpc.ChannelCloseUpdate, assetIDs [][]byte,
|
|
groupKey []byte, universeTap *tapClient) {
|
|
|
|
// With the channel closed, we'll now assert that the co-op close
|
|
// transaction was inserted into the local universe.
|
|
//
|
|
// Since the initiator has a zero asset balance, we expect that at most
|
|
// three outputs exist: one for the remote asset output, one for the
|
|
// remote BTC channel balance and one for the initiator's BTC channel
|
|
// balance (which cannot be zero or below dust due to the mandatory
|
|
// channel reserve).
|
|
numOutputs := 3
|
|
|
|
closeTxid := closeTx.TxHash()
|
|
require.Len(t, closeTx.TxOut, numOutputs)
|
|
|
|
// We assume that the local node has a non-zero BTC balance left.
|
|
localOut, _ := closeTxOut(t, closeTx, closeUpdate, true)
|
|
require.Greater(t, localOut.Value, int64(1000))
|
|
|
|
// We also require there to be exactly one additional output, which is
|
|
// the remote asset output.
|
|
require.Len(t, closeUpdate.AdditionalOutputs, 1)
|
|
assetTxOut, assetOutputIndex := findTxOut(
|
|
t, closeTx, closeUpdate.AdditionalOutputs[0].PkScript,
|
|
)
|
|
require.LessOrEqual(t, assetTxOut.Value, int64(1000))
|
|
|
|
// The remote node has received a couple of HTLCs with an above
|
|
// dust value, so it should also have accumulated a non-dust
|
|
// balance, even after subtracting 1k sats for the asset output.
|
|
remoteCloseOut := closeUpdate.RemoteCloseOutput
|
|
require.NotNil(t, remoteCloseOut)
|
|
|
|
// Find out which of the additional outputs is the local one and which
|
|
// is the remote.
|
|
remoteAuxOut := closeUpdate.AdditionalOutputs[0]
|
|
require.False(t, remoteAuxOut.IsLocal)
|
|
|
|
// And then we verify the arrival of the remote balance asset proof at
|
|
// the universe server as well.
|
|
var remoteAssetCloseOut rfqmsg.JsonCloseOutput
|
|
err := json.Unmarshal(
|
|
remoteCloseOut.CustomChannelData, &remoteAssetCloseOut,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
assetIDStrings := fn.Map(hex.EncodeToString, assetIDs)
|
|
for assetIDStr, scriptKeyStr := range remoteAssetCloseOut.ScriptKeys {
|
|
scriptKeyBytes, err := hex.DecodeString(scriptKeyStr)
|
|
require.NoError(t, err)
|
|
|
|
require.Contains(t, assetIDStrings, assetIDStr)
|
|
|
|
assetID, err := hex.DecodeString(assetIDStr)
|
|
require.NoError(t, err)
|
|
|
|
a := assertUniverseProofExists(
|
|
t, universeTap, assetID, groupKey, scriptKeyBytes,
|
|
fmt.Sprintf("%v:%v", closeTxid, assetOutputIndex),
|
|
)
|
|
|
|
remoteTapd := newTapClient(t, remote)
|
|
itest.AssertBalances(
|
|
t, remoteTapd, a.Amount, itest.WithAssetID(assetID),
|
|
itest.WithScriptKeyType(asset.ScriptKeyBip86),
|
|
itest.WithScriptKey(scriptKeyBytes),
|
|
)
|
|
}
|
|
}
|
|
|
|
// closeTxOut returns either the local or remote output from the close
|
|
// transaction, based on the information given in the close update.
|
|
func closeTxOut(t *testing.T, closeTx *wire.MsgTx,
|
|
closeUpdate *lnrpc.ChannelCloseUpdate, local bool) (*wire.TxOut, int) {
|
|
|
|
var targetPkScript []byte
|
|
if local {
|
|
require.NotNil(t, closeUpdate.LocalCloseOutput)
|
|
targetPkScript = closeUpdate.LocalCloseOutput.PkScript
|
|
} else {
|
|
require.NotNil(t, closeUpdate.RemoteCloseOutput)
|
|
targetPkScript = closeUpdate.RemoteCloseOutput.PkScript
|
|
}
|
|
|
|
return findTxOut(t, closeTx, targetPkScript)
|
|
}
|
|
|
|
// findTxOut returns the transaction output with the target pk script from the
|
|
// given transaction.
|
|
func findTxOut(t *testing.T, tx *wire.MsgTx, targetPkScript []byte) (
|
|
*wire.TxOut, int) {
|
|
|
|
for i, txOut := range tx.TxOut {
|
|
if bytes.Equal(txOut.PkScript, targetPkScript) {
|
|
return txOut, i
|
|
}
|
|
}
|
|
|
|
t.Fatalf("close output (targetPkScript=%x) not found in close "+
|
|
"transaction", targetPkScript)
|
|
|
|
return &wire.TxOut{}, 0
|
|
}
|
|
|
|
type tapClient struct {
|
|
node *HarnessNode
|
|
lnd *rpc.HarnessRPC
|
|
taprpc.TaprootAssetsClient
|
|
assetwalletrpc.AssetWalletClient
|
|
tapdevrpc.TapDevClient
|
|
mintrpc.MintClient
|
|
rfqrpc.RfqClient
|
|
tchrpc.TaprootAssetChannelsClient
|
|
universerpc.UniverseClient
|
|
}
|
|
|
|
func newTapClient(t *testing.T, node *HarnessNode) *tapClient {
|
|
cfg := node.Cfg
|
|
superMacFile := bakeSuperMacaroon(t, cfg, getLiTMacFromFile, false)
|
|
|
|
t.Cleanup(func() {
|
|
require.NoError(t, os.Remove(superMacFile))
|
|
})
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
rawConn, err := connectRPCWithMac(
|
|
ctxt, cfg.LitAddr(), cfg.LitTLSCertPath, superMacFile,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
_ = rawConn.Close()
|
|
})
|
|
|
|
assetsClient := taprpc.NewTaprootAssetsClient(rawConn)
|
|
assetWalletClient := assetwalletrpc.NewAssetWalletClient(rawConn)
|
|
devClient := tapdevrpc.NewTapDevClient(rawConn)
|
|
mintMintClient := mintrpc.NewMintClient(rawConn)
|
|
rfqClient := rfqrpc.NewRfqClient(rawConn)
|
|
tchClient := tchrpc.NewTaprootAssetChannelsClient(rawConn)
|
|
universeClient := universerpc.NewUniverseClient(rawConn)
|
|
|
|
return &tapClient{
|
|
node: node,
|
|
TaprootAssetsClient: assetsClient,
|
|
AssetWalletClient: assetWalletClient,
|
|
TapDevClient: devClient,
|
|
MintClient: mintMintClient,
|
|
RfqClient: rfqClient,
|
|
TaprootAssetChannelsClient: tchClient,
|
|
UniverseClient: universeClient,
|
|
}
|
|
}
|
|
|
|
func connectRPCWithMac(ctx context.Context, hostPort, tlsCertPath,
|
|
macFilePath string) (*grpc.ClientConn, error) {
|
|
|
|
tlsCreds, err := credentials.NewClientTLSFromFile(tlsCertPath, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
opts := []grpc.DialOption{
|
|
grpc.WithBlock(),
|
|
grpc.WithTransportCredentials(tlsCreds),
|
|
}
|
|
|
|
macOption, err := readMacaroon(macFilePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
opts = append(opts, macOption)
|
|
|
|
return grpc.DialContext(ctx, hostPort, opts...)
|
|
}
|
|
|
|
func spendableBalance(client *tapClient, assetID,
|
|
groupKey []byte) (uint64, error) {
|
|
|
|
ctxb := context.Background()
|
|
ctxt, cancel := context.WithTimeout(ctxb, shortTimeout)
|
|
defer cancel()
|
|
|
|
utxos, err := client.ListUtxos(ctxt, &taprpc.ListUtxosRequest{})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
assets := tapfn.FlatMap(
|
|
maps.Values(utxos.ManagedUtxos),
|
|
func(utxo *taprpc.ManagedUtxo) []*taprpc.Asset {
|
|
return utxo.Assets
|
|
},
|
|
)
|
|
|
|
relevantAssets := fn.Filter(func(utxo *taprpc.Asset) bool {
|
|
if len(groupKey) > 0 {
|
|
return bytes.Equal(
|
|
utxo.AssetGroup.TweakedGroupKey, groupKey,
|
|
)
|
|
}
|
|
|
|
return bytes.Equal(utxo.AssetGenesis.AssetId, assetID)
|
|
}, assets)
|
|
|
|
var assetSum uint64
|
|
for _, a := range relevantAssets {
|
|
if a.ScriptKeyIsLocal {
|
|
assetSum += a.Amount
|
|
}
|
|
}
|
|
|
|
return assetSum, nil
|
|
}
|
|
|
|
// assertSpendableBalance differs from assertAssetBalance in that it asserts
|
|
// that the entire balance is spendable. We consider something spendable if we
|
|
// have a local script key for it.
|
|
func assertSpendableBalance(t *testing.T, client *tapClient, assetID,
|
|
groupKey []byte, expectedBalance uint64) {
|
|
|
|
t.Helper()
|
|
|
|
err := wait.NoError(func() error {
|
|
assetSum, err := spendableBalance(client, assetID, groupKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if assetSum != expectedBalance {
|
|
return fmt.Errorf("expected balance %d, got %d",
|
|
expectedBalance, assetSum)
|
|
}
|
|
|
|
return nil
|
|
}, shortTimeout)
|
|
if err != nil {
|
|
ctxb := context.Background()
|
|
listResp, err2 := client.ListAssets(
|
|
ctxb, &taprpc.ListAssetRequest{},
|
|
)
|
|
require.NoError(t, err2)
|
|
|
|
t.Logf("Failed to assert expected balance of %d, current "+
|
|
"assets: %v", expectedBalance, toProtoJSON(t, listResp))
|
|
|
|
utxos, err2 := client.ListUtxos(
|
|
ctxb, &taprpc.ListUtxosRequest{},
|
|
)
|
|
require.NoError(t, err2)
|
|
|
|
t.Logf("Current UTXOs: %v", toProtoJSON(t, utxos))
|
|
|
|
t.Fatalf("Failed to assert balance: %v", err)
|
|
}
|
|
}
|
|
|
|
func logBalance(t *testing.T, nodes []*HarnessNode, assetID []byte,
|
|
occasion string) {
|
|
|
|
t.Helper()
|
|
|
|
time.Sleep(time.Millisecond * 250)
|
|
|
|
for _, node := range nodes {
|
|
local, remote, localSat, remoteSat := getAssetChannelBalance(
|
|
t, node, [][]byte{assetID}, false,
|
|
)
|
|
|
|
t.Logf("%-7s balance: local=%-9d remote=%-9d, localSat=%-9d, "+
|
|
"remoteSat=%-9d (%v)", node.Cfg.Name, local, remote,
|
|
localSat, remoteSat, occasion)
|
|
}
|
|
}
|
|
|
|
func logBalanceGroup(t *testing.T, nodes []*HarnessNode, assetIDs [][]byte,
|
|
occasion string) {
|
|
|
|
t.Helper()
|
|
|
|
time.Sleep(time.Millisecond * 250)
|
|
|
|
for _, node := range nodes {
|
|
local, remote, localSat, remoteSat := getAssetChannelBalance(
|
|
t, node, assetIDs, false,
|
|
)
|
|
|
|
t.Logf("%-7s balance: local=%-9d remote=%-9d, localSat=%-9d, "+
|
|
"remoteSat=%-9d (%v)", node.Cfg.Name, local, remote,
|
|
localSat, remoteSat, occasion)
|
|
}
|
|
}
|
|
|
|
// readMacaroon tries to read the macaroon file at the specified path and create
|
|
// gRPC dial options from it.
|
|
func readMacaroon(macPath string) (grpc.DialOption, error) {
|
|
// Load the specified macaroon file.
|
|
macBytes, err := os.ReadFile(macPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read macaroon path : %w", err)
|
|
}
|
|
|
|
return macFromBytes(macBytes)
|
|
}
|
|
|
|
// macFromBytes returns a macaroon from the given byte slice.
|
|
func macFromBytes(macBytes []byte) (grpc.DialOption, error) {
|
|
mac := &macaroon.Macaroon{}
|
|
if err := mac.UnmarshalBinary(macBytes); err != nil {
|
|
return nil, fmt.Errorf("unable to decode macaroon: %w", err)
|
|
}
|
|
|
|
// Now we append the macaroon credentials to the dial options.
|
|
cred, err := macaroons.NewMacaroonCredential(mac)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating macaroon credential: %w",
|
|
err)
|
|
}
|
|
return grpc.WithPerRPCCredentials(cred), nil
|
|
}
|
|
|
|
func assertMinNumHtlcs(t *testing.T, node *HarnessNode, expected int) {
|
|
t.Helper()
|
|
|
|
ctxb := context.Background()
|
|
|
|
err := wait.NoError(func() error {
|
|
listChansRequest := &lnrpc.ListChannelsRequest{}
|
|
listChansResp, err := node.ListChannels(ctxb, listChansRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var numHtlcs int
|
|
for _, channel := range listChansResp.Channels {
|
|
numHtlcs += len(channel.PendingHtlcs)
|
|
}
|
|
|
|
if numHtlcs < expected {
|
|
return fmt.Errorf("expected %v HTLCs, got %v, %v",
|
|
expected, numHtlcs,
|
|
toProtoJSON(t, listChansResp))
|
|
}
|
|
|
|
return nil
|
|
}, defaultTimeout)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
type subscribeEventsClient = routerrpc.Router_SubscribeHtlcEventsClient
|
|
|
|
type htlcEventConfig struct {
|
|
timeout time.Duration
|
|
numEvents int
|
|
withLinkFailure bool
|
|
withForwardFailure bool
|
|
withFailureDetail routerrpc.FailureDetail
|
|
}
|
|
|
|
func defaultHtlcEventConfig() *htlcEventConfig {
|
|
return &htlcEventConfig{
|
|
timeout: defaultTimeout,
|
|
}
|
|
}
|
|
|
|
type htlcEventOpt func(*htlcEventConfig)
|
|
|
|
func withTimeout(timeout time.Duration) htlcEventOpt {
|
|
return func(config *htlcEventConfig) {
|
|
config.timeout = timeout
|
|
}
|
|
}
|
|
|
|
func withNumEvents(numEvents int) htlcEventOpt {
|
|
return func(config *htlcEventConfig) {
|
|
config.numEvents = numEvents
|
|
}
|
|
}
|
|
|
|
func withLinkFailure(detail routerrpc.FailureDetail) htlcEventOpt {
|
|
return func(config *htlcEventConfig) {
|
|
config.withLinkFailure = true
|
|
config.withFailureDetail = detail
|
|
}
|
|
}
|
|
|
|
func withForwardFailure() htlcEventOpt {
|
|
return func(config *htlcEventConfig) {
|
|
config.withForwardFailure = true
|
|
}
|
|
}
|
|
|
|
func assertHtlcEvents(t *testing.T, c subscribeEventsClient,
|
|
opts ...htlcEventOpt) {
|
|
|
|
t.Helper()
|
|
|
|
cfg := defaultHtlcEventConfig()
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
timeout := time.After(cfg.timeout)
|
|
events := make(chan *routerrpc.HtlcEvent)
|
|
|
|
go func() {
|
|
defer close(events)
|
|
|
|
for {
|
|
evt, err := c.Recv()
|
|
if err != nil {
|
|
t.Logf("Received HTLC event error: %v", err)
|
|
return
|
|
}
|
|
|
|
select {
|
|
case events <- evt:
|
|
case <-timeout:
|
|
t.Logf("Htlc event receive timeout")
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
var numEvents int
|
|
for {
|
|
type (
|
|
linkFailEvent = *routerrpc.HtlcEvent_LinkFailEvent
|
|
forwardFailEvent = *routerrpc.HtlcEvent_ForwardFailEvent
|
|
)
|
|
|
|
select {
|
|
case evt, ok := <-events:
|
|
if !ok {
|
|
t.Fatalf("Htlc event stream closed")
|
|
return
|
|
}
|
|
|
|
if cfg.withLinkFailure {
|
|
linkEvent, ok := evt.Event.(linkFailEvent)
|
|
if !ok {
|
|
// We only count link failure events.
|
|
continue
|
|
}
|
|
|
|
if linkEvent.LinkFailEvent.FailureDetail !=
|
|
cfg.withFailureDetail {
|
|
|
|
continue
|
|
}
|
|
}
|
|
|
|
if cfg.withForwardFailure {
|
|
_, ok := evt.Event.(forwardFailEvent)
|
|
if !ok {
|
|
// We only count link failure events.
|
|
continue
|
|
}
|
|
}
|
|
|
|
numEvents++
|
|
|
|
if numEvents == cfg.numEvents {
|
|
return
|
|
}
|
|
|
|
case <-timeout:
|
|
t.Fatalf("Htlc event receive timeout")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertNumHtlcs(t *testing.T, node *HarnessNode, expected int) {
|
|
t.Helper()
|
|
|
|
ctxb := context.Background()
|
|
|
|
err := wait.NoError(func() error {
|
|
listChansRequest := &lnrpc.ListChannelsRequest{}
|
|
listChansResp, err := node.ListChannels(ctxb, listChansRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var numHtlcs int
|
|
for _, channel := range listChansResp.Channels {
|
|
numHtlcs += len(channel.PendingHtlcs)
|
|
}
|
|
|
|
if numHtlcs != expected {
|
|
return fmt.Errorf("expected %v HTLCs, got %v, %v",
|
|
expected, numHtlcs,
|
|
spew.Sdump(toProtoJSON(t, listChansResp)))
|
|
}
|
|
|
|
return nil
|
|
}, defaultTimeout)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
type forceCloseExpiryInfo struct {
|
|
currentHeight uint32
|
|
csvDelay uint32
|
|
|
|
cltvDelays map[lntypes.Hash]uint32
|
|
|
|
localAssetBalance uint64
|
|
remoteAssetBalance uint64
|
|
|
|
t *testing.T
|
|
|
|
node *HarnessNode
|
|
}
|
|
|
|
func (f *forceCloseExpiryInfo) blockTillExpiry(hash lntypes.Hash) uint32 {
|
|
ctxb := context.Background()
|
|
nodeInfo, err := f.node.GetInfo(ctxb, &lnrpc.GetInfoRequest{})
|
|
require.NoError(f.t, err)
|
|
|
|
cltv, ok := f.cltvDelays[hash]
|
|
require.True(f.t, ok)
|
|
|
|
f.t.Logf("current_height=%v, expiry=%v, mining %v blocks",
|
|
nodeInfo.BlockHeight, cltv, cltv-nodeInfo.BlockHeight)
|
|
|
|
return cltv - nodeInfo.BlockHeight
|
|
}
|
|
|
|
func newCloseExpiryInfo(t *testing.T, node *HarnessNode) forceCloseExpiryInfo {
|
|
ctxb := context.Background()
|
|
|
|
listChansRequest := &lnrpc.ListChannelsRequest{}
|
|
listChansResp, err := node.ListChannels(ctxb, listChansRequest)
|
|
require.NoError(t, err)
|
|
|
|
mainChan := listChansResp.Channels[0]
|
|
|
|
nodeInfo, err := node.GetInfo(ctxb, &lnrpc.GetInfoRequest{})
|
|
require.NoError(t, err)
|
|
|
|
cltvs := make(map[lntypes.Hash]uint32)
|
|
for _, htlc := range mainChan.PendingHtlcs {
|
|
var payHash lntypes.Hash
|
|
copy(payHash[:], htlc.HashLock)
|
|
cltvs[payHash] = htlc.ExpirationHeight
|
|
}
|
|
|
|
var assetData rfqmsg.JsonAssetChannel
|
|
err = json.Unmarshal(mainChan.CustomChannelData, &assetData)
|
|
require.NoError(t, err)
|
|
|
|
return forceCloseExpiryInfo{
|
|
csvDelay: mainChan.CsvDelay,
|
|
currentHeight: nodeInfo.BlockHeight,
|
|
cltvDelays: cltvs,
|
|
localAssetBalance: assetData.LocalBalance,
|
|
remoteAssetBalance: assetData.RemoteBalance,
|
|
t: t,
|
|
node: node,
|
|
}
|
|
}
|
|
|
|
// AssertHLTCNotActive asserts the node doesn't have a pending HTLC in the
|
|
// given channel, which mean either the HTLC never exists, or it was pending
|
|
// and now settled. Returns the HTLC if found and active.
|
|
func assertHTLCNotActive(t *testing.T, hn *HarnessNode,
|
|
cp *lnrpc.ChannelPoint, payHash []byte) *lnrpc.HTLC {
|
|
|
|
var result *lnrpc.HTLC
|
|
target := hex.EncodeToString(payHash)
|
|
|
|
err := wait.NoError(func() error {
|
|
// We require the RPC call to be succeeded and won't wait for
|
|
// it as it's an unexpected behavior.
|
|
ch := fetchChannel(t, hn, cp)
|
|
|
|
// Check all payment hashes active for this channel.
|
|
for _, htlc := range ch.PendingHtlcs {
|
|
h := hex.EncodeToString(htlc.HashLock)
|
|
|
|
// Break if found the htlc.
|
|
if h == target {
|
|
result = htlc
|
|
break
|
|
}
|
|
}
|
|
|
|
// If we've found nothing, we're done.
|
|
if result == nil {
|
|
return nil
|
|
}
|
|
|
|
// Otherwise return an error.
|
|
return fmt.Errorf("node [%s:%x] still has: the payHash %x",
|
|
hn.Name(), hn.PubKey[:], payHash)
|
|
}, defaultTimeout)
|
|
require.NoError(t, err, "timeout checking pending HTLC")
|
|
|
|
return result
|
|
}
|
|
|
|
func assertInvoiceState(t *testing.T, hn *HarnessNode, payAddr []byte,
|
|
expectedState lnrpc.Invoice_InvoiceState) {
|
|
|
|
msg := &invoicesrpc.LookupInvoiceMsg{
|
|
InvoiceRef: &invoicesrpc.LookupInvoiceMsg_PaymentAddr{
|
|
PaymentAddr: payAddr,
|
|
},
|
|
}
|
|
|
|
err := wait.NoError(func() error {
|
|
invoice, err := hn.InvoicesClient.LookupInvoiceV2(
|
|
context.Background(), msg,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if invoice.State == expectedState {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("%s: invoice with payment address %x not "+
|
|
"in state %s", hn.Name(), payAddr, expectedState)
|
|
}, 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) == 0 {
|
|
return fmt.Errorf("expected at least 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)
|
|
}
|
|
|
|
func findForceCloseTransfer(t *testing.T, node1, node2 *tapClient,
|
|
closeTxid *chainhash.Hash) *taprpc.ListTransfersResponse {
|
|
|
|
var (
|
|
ctxb = context.Background()
|
|
result *taprpc.ListTransfersResponse
|
|
err error
|
|
)
|
|
fErr := wait.NoError(func() error {
|
|
result, err = node1.ListTransfers(
|
|
ctxb, &taprpc.ListTransfersRequest{
|
|
AnchorTxid: closeTxid.String(),
|
|
},
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to list node1 transfers: %w",
|
|
err)
|
|
}
|
|
if len(result.Transfers) != 1 {
|
|
return fmt.Errorf("node1 is missing force close " +
|
|
"transfer")
|
|
}
|
|
|
|
forceCloseTransfer2, err := node2.ListTransfers(
|
|
ctxb, &taprpc.ListTransfersRequest{
|
|
AnchorTxid: closeTxid.String(),
|
|
},
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to list node2 transfers: %w",
|
|
err)
|
|
}
|
|
if len(forceCloseTransfer2.Transfers) != 1 {
|
|
return fmt.Errorf("node2 is missing force close " +
|
|
"transfer")
|
|
}
|
|
|
|
return nil
|
|
}, defaultTimeout)
|
|
require.NoError(t, fErr)
|
|
|
|
return result
|
|
}
|
|
|
|
// assertForceCloseSweeps asserts that the force close sweeps are initiated
|
|
// correctly.
|
|
func assertForceCloseSweeps(ctx context.Context, net *NetworkHarness,
|
|
t *harnessTest, alice, bob *HarnessNode, chanPoint *lnrpc.ChannelPoint,
|
|
aliceStartAmount uint64, assetInvoiceAmt, assetsPerMPPShard int,
|
|
assetID, groupKey []byte, aliceHodlInvoices,
|
|
bobHodlInvoices []assetHodlInvoice, mpp bool) (uint64, uint64) {
|
|
|
|
aliceTap := newTapClient(t.t, alice)
|
|
bobTap := newTapClient(t.t, bob)
|
|
|
|
// At this point, both sides should have 4 (or +4 with MPP) HTLCs
|
|
// active.
|
|
numHtlcs := 4
|
|
numAdditionalShards := assetInvoiceAmt / assetsPerMPPShard
|
|
if mpp {
|
|
numHtlcs += numAdditionalShards * 2
|
|
}
|
|
t.Logf("Asserting both Alice and Bob have %d HTLCs...", numHtlcs)
|
|
assertNumHtlcs(t.t, alice, numHtlcs)
|
|
assertNumHtlcs(t.t, bob, numHtlcs)
|
|
|
|
// Before we force close, we'll grab the current height, the CSV delay
|
|
// needed, and also the absolute timeout of the set of active HTLCs.
|
|
closeExpiryInfo := newCloseExpiryInfo(t.t, alice)
|
|
|
|
// With all of the HTLCs established, we'll now force close the channel
|
|
// with Alice.
|
|
t.Logf("Force close by Alice w/ HTLCs...")
|
|
_, closeTxid, err := net.CloseChannel(alice, chanPoint, true)
|
|
require.NoError(t.t, err)
|
|
|
|
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, chanPoint)
|
|
|
|
// Next, we'll mine a block which should start the clock ticking on the
|
|
// relative timeout for the Alice, and Bob.
|
|
//
|
|
// After this next block, both of them can start to sweep.
|
|
//
|
|
// For Alice, she'll go to the second level, revealing her preimage in
|
|
// the process. She'll then need to wait for the relative timeout to
|
|
// expire before she can sweep her output.
|
|
//
|
|
// For Bob, since the remote party (Alice) closed, he can try to sweep
|
|
// right away after initial confirmation.
|
|
mineBlocks(t, net, 1, 1)
|
|
|
|
// After force closing, Bob should now have a transfer that tracks the
|
|
// force closed commitment transaction.
|
|
locateAssetTransfers(t.t, bobTap, *closeTxid)
|
|
|
|
t.Logf("Settling Bob's hodl invoice")
|
|
|
|
// It should then go to "pending force closed".
|
|
assertPendingForceCloseChannelAssetData(t.t, alice, chanPoint)
|
|
|
|
// At this point, the commitment transaction has been mined, and we have
|
|
// 4 total HTLCs on Alice's commitment transaction:
|
|
//
|
|
// * 2x outgoing HTLCs from Alice to Bob
|
|
// * 2x incoming HTLCs from Bob to Alice (+2 with MPP)
|
|
//
|
|
// We'll leave half the HTLCs timeout, while pulling the other half.
|
|
// To start, we'll signal Bob to settle one of his incoming HTLCs on
|
|
// Alice's commitment transaction. For him, this is a remote success
|
|
// spend, so there's no CSV delay other than the 1 CSV (carve out), and
|
|
// he can spend directly from the commitment transaction.
|
|
_, err = bob.InvoicesClient.SettleInvoice(
|
|
ctx, &invoicesrpc.SettleInvoiceMsg{
|
|
Preimage: bobHodlInvoices[0].preimage[:],
|
|
},
|
|
)
|
|
require.NoError(t.t, err)
|
|
|
|
// We'll pause here for Bob to extend the sweep request to the sweeper.
|
|
assertSweepExists(
|
|
t.t, bob,
|
|
walletrpc.WitnessType_TAPROOT_HTLC_ACCEPTED_REMOTE_SUCCESS,
|
|
)
|
|
|
|
bobSweepTx1, err := waitForNTxsInMempool(
|
|
net.Miner.Client, 1, shortTimeout,
|
|
)
|
|
require.NoError(t.t, err)
|
|
|
|
// Next, we'll mine an additional block, this should allow Bob to sweep
|
|
// both his commitment output, and the incoming HTLC that we just
|
|
// settled above.
|
|
mineBlocks(t, net, 1, 1)
|
|
|
|
// At this point, we should have the next sweep transaction in the
|
|
// mempool: Bob's incoming HTLC sweep directly off the commitment
|
|
// transaction.
|
|
bobSweepTx2, err := waitForNTxsInMempool(
|
|
net.Miner.Client, 1, shortTimeout,
|
|
)
|
|
require.NoError(t.t, err)
|
|
|
|
// We'll now mine the next block, which should confirm Bob's HTLC sweep
|
|
// transaction.
|
|
mineBlocks(t, net, 1, 1)
|
|
|
|
bobSweepTransfer1 := locateAssetTransfers(t.t, bobTap, *bobSweepTx1[0])
|
|
bobSweepTransfer2 := locateAssetTransfers(t.t, bobTap, *bobSweepTx2[0])
|
|
t.Logf("Bob's sweep transfer 1: %v",
|
|
toProtoJSON(t.t, bobSweepTransfer1))
|
|
t.Logf("Bob's sweep transfer 2: %v",
|
|
toProtoJSON(t.t, bobSweepTransfer2))
|
|
|
|
t.Logf("Confirming Bob's remote HTLC success sweep")
|
|
|
|
// Bob's balance should now reflect that he's gained the value of the
|
|
// HTLC, in addition to his settled balance. We need to subtract 1 from
|
|
// the final balance due to the rounding down of the asset amount during
|
|
// RFQ conversion.
|
|
bobExpectedBalance := closeExpiryInfo.remoteAssetBalance +
|
|
uint64(assetInvoiceAmt-1)
|
|
t.Logf("Expecting Bob's balance to be %d", bobExpectedBalance)
|
|
assertSpendableBalance(
|
|
t.t, bobTap, assetID, groupKey, bobExpectedBalance,
|
|
)
|
|
|
|
// With Bob's HTLC settled, we'll now have Alice do the same. For her,
|
|
// it'll be a 2nd level sweep, which requires an extra transaction.
|
|
//
|
|
// Before, we do that though, enough blocks have passed so Alice can now
|
|
// sweep her to-local output. So we'll mine an extra block, then assert
|
|
// that she's swept everything properly. With the way the sweeper works,
|
|
// we need to mine one extra block before the sweeper picks things up.
|
|
mineBlocks(t, net, 1, 0)
|
|
|
|
aliceSweepTx1, err := waitForNTxsInMempool(
|
|
net.Miner.Client, 1, shortTimeout,
|
|
)
|
|
require.NoError(t.t, err)
|
|
|
|
mineBlocks(t, net, 1, 1)
|
|
|
|
aliceSweepTransfer1 := locateAssetTransfers(
|
|
t.t, aliceTap, *aliceSweepTx1[0],
|
|
)
|
|
t.Logf("Alice's sweep transfer 1: %v",
|
|
toProtoJSON(t.t, aliceSweepTransfer1))
|
|
|
|
t.Logf("Confirming Alice's to-local sweep")
|
|
|
|
// With this extra block mined, Alice's settled balance should be the
|
|
// starting balance, minus the 2 HTLCs, plus her settled balance.
|
|
aliceExpectedBalance := aliceStartAmount
|
|
aliceExpectedBalance += closeExpiryInfo.localAssetBalance
|
|
assertSpendableBalance(
|
|
t.t, aliceTap, assetID, groupKey, aliceExpectedBalance,
|
|
)
|
|
|
|
t.Logf("Settling Alice's hodl invoice")
|
|
|
|
// With her commitment output swept above, we'll now settle one of
|
|
// Alice's incoming HTLCs.
|
|
_, err = alice.InvoicesClient.SettleInvoice(
|
|
ctx, &invoicesrpc.SettleInvoiceMsg{
|
|
Preimage: aliceHodlInvoices[0].preimage[:],
|
|
},
|
|
)
|
|
require.NoError(t.t, err)
|
|
|
|
// We'll pause here for Alice to extend the sweep request to the
|
|
// sweeper.
|
|
assertSweepExists(
|
|
t.t, alice,
|
|
walletrpc.WitnessType_TAPROOT_HTLC_ACCEPTED_LOCAL_SUCCESS,
|
|
)
|
|
|
|
// We'll now mine a block, which should trigger Alice's broadcast of the
|
|
// second level sweep transaction.
|
|
sweepBlocks := mineBlocks(t, net, 1, 0)
|
|
|
|
// If the block mined above didn't also mine our sweep, then we'll mine
|
|
// one final block which will confirm Alice's sweep transaction.
|
|
if len(sweepBlocks[0].Transactions) == 1 {
|
|
sweepTx, err := waitForNTxsInMempool(
|
|
net.Miner.Client, 1, shortTimeout,
|
|
)
|
|
require.NoError(t.t, err)
|
|
|
|
// With the sweep transaction in the mempool, we'll mine a block
|
|
// to confirm the sweep.
|
|
mineBlocks(t, net, 1, 1)
|
|
|
|
aliceSweepTransfer := locateAssetTransfers(
|
|
t.t, aliceTap, *sweepTx[0],
|
|
)
|
|
t.Logf("Alice's first-level sweep transfer: %v",
|
|
toProtoJSON(t.t, aliceSweepTransfer))
|
|
} else {
|
|
sweepTx := sweepBlocks[0].Transactions[1]
|
|
aliceSweepTransfer := locateAssetTransfers(
|
|
t.t, aliceTap, sweepTx.TxHash(),
|
|
)
|
|
t.Logf("Alice's first-level sweep transfer: %v",
|
|
toProtoJSON(t.t, aliceSweepTransfer))
|
|
}
|
|
|
|
t.Logf("Confirming Alice's second level remote HTLC success sweep")
|
|
|
|
// Next, we'll mine enough blocks to trigger the CSV expiry so Alice can
|
|
// sweep the HTLC into her wallet.
|
|
mineBlocks(t, net, closeExpiryInfo.csvDelay, 0)
|
|
|
|
// We'll pause here and wait until the sweeper recognizes that we've
|
|
// offered the second level sweep transaction.
|
|
assertSweepExists(
|
|
t.t, alice,
|
|
//nolint: lll
|
|
walletrpc.WitnessType_TAPROOT_HTLC_ACCEPTED_SUCCESS_SECOND_LEVEL,
|
|
)
|
|
|
|
t.Logf("Confirming Alice's local HTLC success sweep")
|
|
|
|
// Now that we know the sweep was offered, we'll mine an extra block to
|
|
// actually trigger a sweeper broadcast. Due to an internal block race
|
|
// condition, the sweep transaction may have already been
|
|
// published+mined. If so, we don't need to mine the extra block.
|
|
sweepBlocks = mineBlocks(t, net, 1, 0)
|
|
|
|
// If the block mined above didn't also mine our sweep, then we'll mine
|
|
// one final block which will confirm Alice's sweep transaction.
|
|
if len(sweepBlocks[0].Transactions) == 1 {
|
|
sweepTx, err := waitForNTxsInMempool(
|
|
net.Miner.Client, 1, shortTimeout,
|
|
)
|
|
require.NoError(t.t, err)
|
|
|
|
mineBlocks(t, net, 1, 1)
|
|
|
|
aliceSweepTransfer := locateAssetTransfers(
|
|
t.t, aliceTap, *sweepTx[0],
|
|
)
|
|
t.Logf("Alice's second-level sweep transfer: %v",
|
|
toProtoJSON(t.t, aliceSweepTransfer))
|
|
} else {
|
|
sweepTx := sweepBlocks[0].Transactions[1]
|
|
aliceSweepTransfer := locateAssetTransfers(
|
|
t.t, aliceTap, sweepTx.TxHash(),
|
|
)
|
|
t.Logf("Alice's second-level sweep transfer: %v",
|
|
toProtoJSON(t.t, aliceSweepTransfer))
|
|
}
|
|
|
|
// With the sweep transaction confirmed, Alice's balance should have
|
|
// incremented by the amt of the HTLC.
|
|
aliceExpectedBalance += uint64(assetInvoiceAmt - 1)
|
|
assertSpendableBalance(
|
|
t.t, aliceTap, assetID, groupKey, aliceExpectedBalance,
|
|
)
|
|
|
|
t.Logf("Mining enough blocks to time out the remaining HTLCs")
|
|
|
|
// At this point, we've swept two HTLCs: one from the remote commit, and
|
|
// one via the second layer. We'll now mine the remaining amount of
|
|
// blocks to time out the HTLCs.
|
|
blockToMine := closeExpiryInfo.blockTillExpiry(
|
|
aliceHodlInvoices[1].preimage.Hash(),
|
|
)
|
|
mineBlocks(t, net, blockToMine, 0)
|
|
|
|
// 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,
|
|
)
|
|
assertSweepExists(
|
|
t.t, bob,
|
|
walletrpc.WitnessType_TAPROOT_HTLC_OFFERED_REMOTE_TIMEOUT,
|
|
)
|
|
|
|
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)
|
|
assertSpendableBalance(
|
|
t.t, bobTap, assetID, groupKey, bobExpectedBalance,
|
|
)
|
|
|
|
t.Logf("Mining extra blocks for Alice's CSV to expire on 2nd level txn")
|
|
|
|
// Next, we'll mine 4 additional blocks to Alice's CSV delay expires for
|
|
// the second level timeout output.
|
|
mineBlocks(t, net, closeExpiryInfo.csvDelay, 0)
|
|
|
|
// Wait for Alice to extend the second level output to the sweeper
|
|
// before we mine the next block to the sweeper.
|
|
assertSweepExists(
|
|
t.t, alice,
|
|
walletrpc.WitnessType_TAPROOT_HTLC_OFFERED_TIMEOUT_SECOND_LEVEL,
|
|
)
|
|
|
|
t.Logf("Confirming Alice's final timeout sweep")
|
|
|
|
// With the way the sweeper works, we'll now need to mine an extra block
|
|
// to trigger the sweep.
|
|
sweepBlocks = mineBlocks(t, net, 1, 0)
|
|
|
|
// If the block mined above didn't also mine our sweep, then we'll mine
|
|
// one final block which will confirm Alice's sweep transaction.
|
|
if len(sweepBlocks[0].Transactions) == 1 {
|
|
sweepTx, err := waitForNTxsInMempool(
|
|
net.Miner.Client, 1, shortTimeout,
|
|
)
|
|
require.NoError(t.t, err)
|
|
|
|
// We'll mine one final block which will confirm Alice's sweep
|
|
// transaction.
|
|
mineBlocks(t, net, 1, 1)
|
|
|
|
aliceSweepTransfer := locateAssetTransfers(
|
|
t.t, aliceTap, *sweepTx[0],
|
|
)
|
|
t.Logf("Alice's final timeout sweep transfer: %v",
|
|
toProtoJSON(t.t, aliceSweepTransfer))
|
|
} else {
|
|
sweepTx := sweepBlocks[0].Transactions[1]
|
|
aliceSweepTransfer := locateAssetTransfers(
|
|
t.t, aliceTap, sweepTx.TxHash(),
|
|
)
|
|
t.Logf("Alice's final timeout sweep transfer: %v",
|
|
toProtoJSON(t.t, aliceSweepTransfer))
|
|
}
|
|
|
|
return aliceExpectedBalance, bobExpectedBalance
|
|
}
|