diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4e79ffb1..5a4091dd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -304,13 +304,11 @@ jobs: matrix: include: - name: bbolt - args: itest-parallel-no-backward-compat icase=terminal dbbackend=bbolt tranches=4 + args: itest-parallel icase=terminal dbbackend=bbolt tranches=4 - name: sqlite - args: itest-parallel-no-backward-compat icase=terminal dbbackend=sqlite tranches=4 + args: itest-parallel icase=terminal dbbackend=sqlite tranches=4 - name: postgres - args: itest-parallel-no-backward-compat icase=terminal dbbackend=postgres tranches=4 - - name: custom-channels - args: itest-parallel icase=custom_channels tranches=4 + args: itest-parallel icase=terminal dbbackend=postgres tranches=4 steps: - name: git checkout diff --git a/Makefile b/Makefile index 94a6166c..66a86b07 100644 --- a/Makefile +++ b/Makefile @@ -24,12 +24,6 @@ PUBLIC_URL := # versions are checked against this version. GO_VERSION = 1.25.5 -# LITD_COMPAT_VERSIONS is a space-separated list of litd versions that are -# installed before running the integration tests which include backward -# compatibility tests. The list of versions must be in sync with any version -# used in the backwardCompat map in itest/litd_test_list_on_test.go. -LITD_COMPAT_VERSIONS = v0.14.1-alpha v0.15.0-alpha - LOOP_COMMIT := $(shell cat go.mod | \ grep $(LOOP_PKG) | \ head -n1 | \ @@ -243,10 +237,6 @@ build-itest-binary: @$(call print, "Building itest binary.") CGO_ENABLED=0 $(GOTEST) -v ./itest -tags="$(DEV_TAGS) $(ITEST_TAGS)" -c -o itest/itest.test -install-backward-compat-versions: - @$(call print, "Installing old versions of litd for backward compatibility tests.") - scripts/install-backward-compat-versions.sh '$(LITD_COMPAT_VERSIONS)' - run-itest-only: @$(call print, "Building itest binary.") CGO_ENABLED=0 $(GOBUILD) -tags="$(ITEST_TAGS)" -o itest/litd-itest -ldflags "$(ITEST_LDFLAGS)" $(PKG)/cmd/litd @@ -256,22 +246,15 @@ run-itest-only: rm -rf itest/*.log itest/.logs*; date scripts/itest_part.sh $(ITEST_FLAGS) -itest-only: build-itest install-backward-compat-versions run-itest-only +itest-only: build-itest run-itest-only itest: app-build itest-only -itest-no-backward-compat: app-build build-itest run-itest-only - -itest-parallel: app-build build-itest install-backward-compat-versions build-itest-binary +itest-parallel: app-build build-itest build-itest-binary @$(call print, "Running integration tests in parallel.") rm -rf itest/*.log itest/.logs*; date scripts/itest_parallel.sh $(ITEST_PARALLELISM) $(NUM_ITEST_TRANCHES) $(SHUFFLE_SEED) $(TEST_FLAGS) $(ITEST_FLAGS) -itest-parallel-no-backward-compat: app-build build-itest build-itest-binary - @$(call print, "Running integration tests in parallel (no backward compat binaries).") - rm -rf itest/*.log itest/.logs*; date - scripts/itest_parallel.sh $(ITEST_PARALLELISM) $(NUM_ITEST_TRANCHES) $(SHUFFLE_SEED) $(TEST_FLAGS) $(ITEST_FLAGS) - # ============= # FLAKE HUNTING # ============= @@ -364,5 +347,5 @@ flakehunter-unit: go-install go-install-noui go-install-cli app-build release go-release \ docker-release docker-tools scratch check unit unit-cover unit-race \ clean-itest build-itest build-itest-binary itest-only itest \ - itest-parallel itest-parallel-no-backward-compat flake-unit fmt lint \ + itest-parallel flake-unit fmt lint \ mod mod-check list rpc protos protos-check rpc-js-compile clean diff --git a/go.mod b/go.mod index b7dcb5b2..8f411df2 100644 --- a/go.mod +++ b/go.mod @@ -108,7 +108,6 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/goccy/go-yaml v1.15.23 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/protobuf v1.5.4 // indirect diff --git a/go.sum b/go.sum index 9d31f1cc..6e5b69f2 100644 --- a/go.sum +++ b/go.sum @@ -846,8 +846,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-yaml v1.15.23 h1:WS0GAX1uNPDLUvLkNU2vXq6oTnsmfVFocjQ/4qA48qo= -github.com/goccy/go-yaml v1.15.23/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= diff --git a/itest/assets_test.go b/itest/assets_test.go deleted file mode 100644 index 7ff573ab..00000000 --- a/itest/assets_test.go +++ /dev/null @@ -1,3770 +0,0 @@ -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/authmailboxrpc" - "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" - "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:ll -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 -) - -// itestNode is a wrapper around a lnd/tapd node. -type itestNode struct { - Lnd *HarnessNode - Tapd *tapClient -} - -// multiRfqNodes contains all the itest nodes that are required to set up the -// multi RFQ network topology. -type multiRfqNodes struct { - charlie, dave, erin, fabia, yara, george itestNode - universeTap *tapClient -} - -// createTestMultiRFQAssetNetwork creates a lightning network topology which -// consists of both bitcoin and asset channels. It focuses on the property of -// having multiple channels available on both the sender and receiver side. -// The George node is using a way different oracle that provides asset rates -// that fall outside of the configured tolerance bounds, leading to RFQ -// negotiation failures. -// -// The topology we are going for looks like the following: -// -// /---[sats]--> Erin --[assets]--\ -// / \ -// / \ -// / \ -// -// Charlie -----[sats]--> Dave --[assets]--> Fabia -// -// \ / / -// \ / / -// \---[sats]--> Yara --[assets]-----/ / -// \ / -// \ / -// \--[sats]-> George --[assets]-/ -func createTestMultiRFQAssetNetwork(t *harnessTest, net *NetworkHarness, - nodes multiRfqNodes, mintedAsset *taprpc.Asset, assetSendAmount, - fundingAmount uint64, pushSat int64) (*lnrpc.ChannelPoint, - *lnrpc.ChannelPoint, *lnrpc.ChannelPoint) { - - charlie, charlieTap := nodes.charlie.Lnd, nodes.charlie.Tapd - dave, daveTap := nodes.dave.Lnd, nodes.dave.Tapd - erin, erinTap := nodes.erin.Lnd, nodes.erin.Tapd - _, fabiaTap := nodes.fabia.Lnd, nodes.fabia.Tapd - yara, yaraTap := nodes.yara.Lnd, nodes.yara.Tapd - george, georgeTap := nodes.george.Lnd, nodes.george.Tapd - universeTap := nodes.universeTap - - // Let's open the normal sats channels between Charlie and the routing - // peers. - _ = openChannelAndAssert( - t, net, charlie, erin, lntest.OpenChannelParams{ - Amt: 10_000_000, - SatPerVByte: 5, - }, - ) - - _ = openChannelAndAssert( - t, net, charlie, dave, lntest.OpenChannelParams{ - Amt: 10_000_000, - SatPerVByte: 5, - }, - ) - - _ = openChannelAndAssert( - t, net, charlie, yara, lntest.OpenChannelParams{ - Amt: 10_000_000, - SatPerVByte: 5, - }, - ) - - _ = openChannelAndAssert( - t, net, charlie, george, lntest.OpenChannelParams{ - Amt: 10_000_000, - SatPerVByte: 5, - }, - ) - - ctxb := context.Background() - assetID := mintedAsset.AssetGenesis.AssetId - var groupKey []byte - if mintedAsset.AssetGroup != nil { - groupKey = mintedAsset.AssetGroup.TweakedGroupKey - } - - fundingScriptTree := tapscript.NewChannelFundingScriptTree() - fundingScriptKey := fundingScriptTree.TaprootKey - fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() - - // We need to send some assets to Dave, so he can fund an asset channel - // with Fabia. - 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) - - // We need to send some assets to Yara, so he can fund an asset channel - // with Fabia. - yaraAddr, err := yaraTap.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 Yara...", assetSendAmount) - - // Send the assets to Yara. - itest.AssertAddrCreated(t.t, yaraTap, mintedAsset, yaraAddr) - sendResp, err = charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ - TapAddrs: []string{yaraAddr.Encoded}, - }) - require.NoError(t.t, err) - itest.ConfirmAndAssertOutboundTransfer( - t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, - []uint64{ - mintedAsset.Amount - 3*assetSendAmount, assetSendAmount, - }, 2, 3, - ) - itest.AssertNonInteractiveRecvComplete(t.t, yaraTap, 1) - - // We need to send some assets to George, so he can fund an asset - // channel with Fabia. - georgeAddr, err := georgeTap.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 George...", assetSendAmount) - - // Send the assets to George. - itest.AssertAddrCreated(t.t, georgeTap, mintedAsset, georgeAddr) - sendResp, err = charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ - TapAddrs: []string{georgeAddr.Encoded}, - }) - require.NoError(t.t, err) - itest.ConfirmAndAssertOutboundTransfer( - t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, - []uint64{ - mintedAsset.Amount - 4*assetSendAmount, assetSendAmount, - }, 3, 4, - ) - itest.AssertNonInteractiveRecvComplete(t.t, georgeTap, 1) - - // We fund the Dave->Fabia channel. - fundRespDF, err := daveTap.FundChannel( - ctxb, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetID, - PeerPubkey: fabiaTap.node.PubKey[:], - FeeRateSatPerVbyte: 5, - PushSat: pushSat, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded channel between Dave and Fabia: %v", fundRespDF) - - // We fund the Erin->Fabia channel. - fundRespEF, err := erinTap.FundChannel( - ctxb, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - 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) - - // We fund the Yara->Fabia channel. - fundRespYF, err := yaraTap.FundChannel( - ctxb, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetID, - PeerPubkey: fabiaTap.node.PubKey[:], - FeeRateSatPerVbyte: 5, - PushSat: pushSat, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded channel between Yara and Fabia: %v", fundRespYF) - - // We fund the George->Fabia channel. - fundRespGF, err := georgeTap.FundChannel( - ctxb, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetID, - PeerPubkey: fabiaTap.node.PubKey[:], - FeeRateSatPerVbyte: 5, - PushSat: pushSat, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded channel between George and Fabia: %v", fundRespGF) - - // Make sure the pending channel shows up in the list and has the - // custom records set as JSON. - assertPendingChannels( - t.t, daveTap.node, mintedAsset, 1, fundingAmount, 0, - ) - assertPendingChannels( - t.t, erinTap.node, mintedAsset, 1, fundingAmount, 0, - ) - assertPendingChannels( - t.t, yaraTap.node, mintedAsset, 1, fundingAmount, 0, - ) - assertPendingChannels( - t.t, georgeTap.node, mintedAsset, 1, fundingAmount, 0, - ) - - // Now that we've looked at the pending channels, let's actually confirm - // all three of them. - mineBlocks(t, net, 6, 4) - - // We'll be tracking the expected asset balances throughout the test, so - // we can assert it after each action. - charlieAssetBalance := mintedAsset.Amount - 4*assetSendAmount - daveAssetBalance := assetSendAmount - fundingAmount - erinAssetBalance := assetSendAmount - fundingAmount - yaraAssetBalance := assetSendAmount - fundingAmount - georgeAssetBalance := assetSendAmount - fundingAmount - - itest.AssertBalances( - t.t, charlieTap, charlieAssetBalance, - itest.WithAssetID(assetID), itest.WithNumUtxos(1), - ) - - itest.AssertBalances( - t.t, daveTap, daveAssetBalance, itest.WithAssetID(assetID), - ) - - itest.AssertBalances( - t.t, erinTap, erinAssetBalance, itest.WithAssetID(assetID), - ) - - itest.AssertBalances( - t.t, yaraTap, yaraAssetBalance, itest.WithAssetID(assetID), - ) - - itest.AssertBalances( - t.t, georgeTap, georgeAssetBalance, itest.WithAssetID(assetID), - ) - - // Assert that the proofs for both channels has been uploaded to the - // designated Universe server. - assertUniverseProofExists( - t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, - fmt.Sprintf("%v:%v", fundRespDF.Txid, fundRespDF.OutputIndex), - ) - assertUniverseProofExists( - t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, - fmt.Sprintf("%v:%v", fundRespEF.Txid, fundRespEF.OutputIndex), - ) - assertUniverseProofExists( - t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, - fmt.Sprintf("%v:%v", fundRespYF.Txid, fundRespYF.OutputIndex), - ) - assertUniverseProofExists( - t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, - fmt.Sprintf("%v:%v", fundRespGF.Txid, fundRespGF.OutputIndex), - ) - - return nil, nil, nil -} - -// 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. - assertBalance( - t.t, charlieTap, charlieFundingAmount, - itest.WithAssetID(assetID), - itest.WithScriptKeyType(asset.ScriptKeyScriptPathChannel), - itest.WithNumUtxos(1), - itest.WithScriptKey(fundingScriptKeyBytes), - ) - assertBalance( - t.t, daveTap, daveFundingAmount, itest.WithAssetID(assetID), - itest.WithScriptKeyType(asset.ScriptKeyScriptPathChannel), - itest.WithNumUtxos(1), - itest.WithScriptKey(fundingScriptKeyBytes), - ) - assertBalance( - 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. - assertBalance( - t.t, charlieTap, charlieAssetBalance, - itest.WithAssetID(assetID), - ) - assertBalance( - t.t, daveTap, daveAssetBalance, itest.WithAssetID(assetID), - ) - assertBalance( - 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 -} - -// assertBalance is a thin wrapper around itest.AssertBalances that skips the -// balance check for old versions during the backward compatibility test. -func assertBalance(t *testing.T, client *tapClient, balance uint64, - opts ...itest.BalanceOption) { - - if client.node.Cfg.SkipBalanceChecks { - return - } - - itest.AssertBalances(t, client, balance, opts...) -} - -// 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] - pendingJSON, err := parseChannelData( - node, pendingChan.Channel.CustomChannelData, - ) - 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] - - assetData, err := parseChannelData(src, targetChan.CustomChannelData) - 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) { - - assertChannelAssetBalanceWithDelta( - t, node, chanPoint, local, remote, 1, - ) -} - -func assertChannelAssetBalanceWithDelta(t *testing.T, node *HarnessNode, - chanPoint *lnrpc.ChannelPoint, local, remote uint64, delta float64) { - - targetChan := fetchChannel(t, node, chanPoint) - - assetBalance, err := parseChannelData( - node, targetChan.CustomChannelData, - ) - require.NoError(t, err) - - require.Len(t, assetBalance.FundingAssets, 1) - - require.InDelta(t, local, assetBalance.LocalBalance, delta) - require.InDelta(t, remote, assetBalance.RemoteBalance, delta) -} - -func channelAssetBalance(t *testing.T, node *HarnessNode, - chanPoint *lnrpc.ChannelPoint) (uint64, uint64) { - - targetChan := fetchChannel(t, node, chanPoint) - - assetBalance, err := parseChannelData( - node, targetChan.CustomChannelData, - ) - require.NoError(t, err) - - require.GreaterOrEqual(t, len(assetBalance.FundingAssets), 1) - - return assetBalance.LocalBalance, assetBalance.RemoteBalance -} - -// 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, - OutgoingChanIds: cfg.outgoingChanIDs, - AllowSelfPayment: cfg.allowSelfPayment, - } - - 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) - - // 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 - } - - tapPayment, err := getAssetPaymentResult(t, stream, false) - require.NoError(t, err) - - payment := tapPayment.lndPayment - if payment.Status == lnrpc.Payment_FAILED { - t.Logf("Failure reason: %v", payment.FailureReason) - } - require.Equal(t, cfg.payStatus, payment.Status) - require.Equal(t, cfg.failureReason, payment.FailureReason) -} - -func sendKeySendPayment(t *testing.T, src, dst *HarnessNode, - amt btcutil.Amount, opts ...payOpt) { - - cfg := defaultPayConfig() - for _, opt := range opts { - opt(cfg) - } - - 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[:] - - for key, value := range cfg.destCustomRecords { - customRecords[key] = value - } - - req := &routerrpc.SendPaymentRequest{ - Dest: dst.PubKey[:], - Amt: int64(amt), - DestCustomRecords: customRecords, - PaymentHash: hash[:], - TimeoutSeconds: int32(PaymentTimeout.Seconds()), - FeeLimitMsat: int64(cfg.feeLimit), - MaxParts: cfg.maxShards, - OutgoingChanIds: cfg.outgoingChanIDs, - AllowSelfPayment: cfg.allowSelfPayment, - RouteHints: cfg.routeHints, - } - - stream, err := src.RouterClient.SendPaymentV2(ctxt, req) - require.NoError(t, err) - - result, err := getPaymentResult(stream, false) - 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, - opts ...invoiceOpt) *lnrpc.AddInvoiceResponse { - - cfg := defaultInvoiceConfig() - for _, opt := range opts { - opt(cfg) - } - - 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), - RouteHints: cfg.routeHints, - }) - 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: int64(cfg.feeLimit), - MaxParts: cfg.maxShards, - OutgoingChanIds: cfg.outgoingChanIDs, - AllowSelfPayment: cfg.allowSelfPayment, - } - - if cfg.smallShards { - sendReq.MaxShardSizeMsat = 80_000_000 - } - - stream, err := payer.RouterClient.SendPaymentV2(ctxt, sendReq) - require.NoError(t, err) - - result, err := getPaymentResult( - stream, cfg.payStatus == lnrpc.Payment_IN_FLIGHT, - ) - 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 - outgoingChanIDs []uint64 - allowSelfPayment bool - routeHints []*lnrpc.RouteHint -} - -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 withOutgoingChanIDs(ids []uint64) payOpt { - return func(c *payConfig) { - c.outgoingChanIDs = ids - } -} - -func withAllowSelfPayment() payOpt { - return func(c *payConfig) { - c.allowSelfPayment = true - } -} - -func withPayRouteHints(hints []*lnrpc.RouteHint) payOpt { - return func(c *payConfig) { - c.routeHints = hints - } -} - -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, - OutgoingChanIds: cfg.outgoingChanIDs, - AllowSelfPayment: cfg.allowSelfPayment, - } - - 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[:]) - }) - - var peerPubKey []byte - if rfqPeer != nil { - peerPubKey = rfqPeer.PubKey[:] - } - - request := &tchrpc.SendPaymentRequest{ - PeerPubkey: peerPubKey, - 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 stream. - if cfg.errSubStr != "" { - msg, err := stream.Recv() - - // On errors we still get an empty set of RFQs as a response. - if msg.GetAcceptedSellOrders() != nil { - _, err = stream.Recv() - } - - require.ErrorContains(t, err, cfg.errSubStr) - - return 0, rfqmath.BigIntFixedPoint{} - } - - var ( - numUnits uint64 - rateVal rfqmath.FixedPoint[rfqmath.BigInt] - ) - - tapPayment, err := getAssetPaymentResult( - t, stream, cfg.payStatus == lnrpc.Payment_IN_FLIGHT, - ) - require.NoError(t, err) - - payment := tapPayment.lndPayment - require.Equal(t, cfg.payStatus, payment.Status) - require.Equal(t, cfg.failureReason, payment.FailureReason) - - amountMsat := lnwire.MilliSatoshi(decodedInvoice.NumMsat) - - rateVal = tapPayment.assetRate - milliSatsFP := rfqmath.MilliSatoshiToUnits(amountMsat, rateVal) - numUnits = milliSatsFP.ScaleTo(0).ToUint64() - - return numUnits, rateVal -} - -type invoiceConfig struct { - errSubStr string - groupKey []byte - msats lnwire.MilliSatoshi - 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 withMsatAmount(amt uint64) invoiceOpt { - return func(c *invoiceConfig) { - c.msats = lnwire.MilliSatoshi(amt) - } -} - -func withRouteHints(hints []*lnrpc.RouteHint) invoiceOpt { - return func(c *invoiceConfig) { - c.routeHints = hints - } -} - -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()) - - var peerPubKey []byte - if dstRfqPeer != nil { - peerPubKey = dstRfqPeer.PubKey[:] - - 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{ - AssetAmount: assetAmount, - PeerPubkey: peerPubKey, - InvoiceRequest: &lnrpc.Invoice{ - Memo: fmt.Sprintf("this is an asset invoice for "+ - "%d units", assetAmount), - Expiry: timeoutSeconds, - ValueMsat: int64(cfg.msats), - RouteHints: cfg.routeHints, - }, - } - - 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) - - var mSatPerUnit float64 - - if cfg.msats > 0 { - require.EqualValues(t, decodedInvoice.NumMsat, cfg.msats) - units := rfqmath.MilliSatoshiToUnits(cfg.msats, *rate) - - mSatPerUnit = float64(cfg.msats) / float64(units.ToUint64()) - } else { - 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, - resp.AcceptedBuyQuote.Peer, 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()) - - var rfqPeer []byte - - if dstRfqPeer != nil { - rfqPeer = dstRfqPeer.PubKey[:] - } - - t.Logf("Asking peer %x for quote to buy assets to receive for "+ - "invoice for %d units; waiting up to %ds", - rfqPeer, 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: rfqPeer, - 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, rfqPeer, - 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) - - if !local.Cfg.OldChannelFormat { - assertWaitingCloseChannelAssetData(t.t, local, chanPoint) - } - if !remote.Cfg.OldChannelFormat { - 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, - ) - - if !local.Cfg.OldChannelFormat { - assertClosedChannelAssetData(t.t, local, chanPoint) - } - if !remote.Cfg.OldChannelFormat { - 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) { - - ctxb := context.Background() - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() - - // 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 local 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, - ) - - // Because we don't exactly know what asset IDs made it into the close - // transaction, we need to fetch the closed channel to find that out. - closedChans, err := local.ClosedChannels( - ctxt, &lnrpc.ClosedChannelsRequest{ - Cooperative: true, - }, - ) - require.NoError(t, err) - require.NotEmpty(t, closedChans.Channels) - - var closedJsonChannel *rfqmsg.JsonAssetChannel - for _, closedChan := range closedChans.Channels { - if closedChan.ClosingTxHash == closeTx.TxHash().String() { - closedJsonChannel, err = parseChannelData( - local, closedChan.CustomChannelData, - ) - require.NoError(t, err) - - break - } - } - require.NotNil(t, closedJsonChannel) - - // 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) - - // We only check for a proof if an asset of that asset ID was - // actually in the close output, which might not always be the - // case in grouped asset channels. - localAssetIDs := fn.NewSet[string](fn.Map( - func(t rfqmsg.JsonAssetTranche) string { - return t.AssetID - }, - closedJsonChannel.LocalAssets, - )...) - if !localAssetIDs.Contains(assetIDStr) { - continue - } - - 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) - - // We only check for a proof if an asset of that asset ID was - // actually in the close output, which might not always be the - // case in grouped asset channels. - remoteAssetIDs := fn.NewSet[string](fn.Map( - func(t rfqmsg.JsonAssetTranche) string { - return t.AssetID - }, - closedJsonChannel.RemoteAssets, - )...) - if !remoteAssetIDs.Contains(assetIDStr) { - continue - } - - 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 - authmailboxrpc.MailboxClient -} - -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) - mboxClient := authmailboxrpc.NewMailboxClient(rawConn) - - return &tapClient{ - node: node, - TaprootAssetsClient: assetsClient, - AssetWalletClient: assetWalletClient, - TapDevClient: devClient, - MintClient: mintMintClient, - RfqClient: rfqClient, - TaprootAssetChannelsClient: tchClient, - UniverseClient: universeClient, - MailboxClient: mboxClient, - } -} - -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 - } - - assetData, err := parseChannelData(node, mainChan.CustomChannelData) - 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) - } - - closeData, err := parseChannelData( - node, targetChan.CustomChannelData, - ) - 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) - - closeData, err := parseChannelData(node, closedChan.CustomChannelData) - 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. - // - // nolint:ll - assertSweepExists( - t.t, alice, - 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 { - // nolint:ll - assertSweepExists( - t.t, bob, - 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 -} - -// oldJsonAssetChanInfo is a struct that represents the old channel information -// of a single asset within a channel, as it looked for litd v0.14.1 and before. -type oldJsonAssetChanInfo struct { - AssetInfo rfqmsg.JsonAssetUtxo `json:"asset_utxo"` - Capacity uint64 `json:"capacity"` - LocalBalance uint64 `json:"local_balance"` - RemoteBalance uint64 `json:"remote_balance"` -} - -// oldJsonAssetChannel is a struct that represents the old channel information -// of all assets within a channel, as it looked for litd v0.14.1 and before. -type oldJsonAssetChannel struct { - Assets []oldJsonAssetChanInfo `json:"assets"` -} - -// parseChannelData parses the given channel data into a rfqmsg.JsonAssetChannel -// struct. It can deal with the old (litd v0.14.1 and before) and new (litd -// v0.15.0 and after) channel data formats. -func parseChannelData(node *HarnessNode, - data []byte) (*rfqmsg.JsonAssetChannel, error) { - - var closeData rfqmsg.JsonAssetChannel - if node.Cfg.OldChannelFormat { - var oldData oldJsonAssetChannel - err := json.Unmarshal(data, &oldData) - if err != nil { - return nil, fmt.Errorf("error unmarshalling old "+ - "channel data: %w", err) - } - - oldAsset := oldData.Assets[0] - closeData.FundingAssets = []rfqmsg.JsonAssetUtxo{ - oldAsset.AssetInfo, - } - closeData.Capacity = oldAsset.Capacity - closeData.LocalBalance = oldAsset.LocalBalance - closeData.RemoteBalance = oldAsset.RemoteBalance - - return &closeData, nil - } - - err := json.Unmarshal(data, &closeData) - if err != nil { - return nil, fmt.Errorf("error unmarshalling channel data: %w", - err) - } - - return &closeData, nil -} diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go deleted file mode 100644 index e27b6bc5..00000000 --- a/itest/litd_custom_channels_test.go +++ /dev/null @@ -1,5633 +0,0 @@ -package itest - -import ( - "context" - "crypto/rand" - "encoding/hex" - "fmt" - "math" - "math/big" - "slices" - "strconv" - "time" - - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/schnorr" - "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/wire" - "github.com/lightninglabs/taproot-assets/asset" - "github.com/lightninglabs/taproot-assets/itest" - "github.com/lightninglabs/taproot-assets/proof" - "github.com/lightninglabs/taproot-assets/rfqmath" - "github.com/lightninglabs/taproot-assets/rfqmsg" - "github.com/lightninglabs/taproot-assets/rpcutils" - "github.com/lightninglabs/taproot-assets/taprpc" - "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" - oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" - "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" - tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" - "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/lntest" - "github.com/lightningnetwork/lnd/lntest/node" - "github.com/lightningnetwork/lnd/lntest/port" - "github.com/lightningnetwork/lnd/lntest/wait" - "github.com/lightningnetwork/lnd/lntypes" - "github.com/lightningnetwork/lnd/lnwallet/chainfee" - "github.com/lightningnetwork/lnd/lnwire" - "github.com/stretchr/testify/require" -) - -var ( - dummyMetaData = &taprpc.AssetMeta{ - Data: []byte("some metadata"), - } - - itestAsset = &mintrpc.MintAsset{ - AssetType: taprpc.AssetType_NORMAL, - Name: "itest-asset-cents", - AssetMeta: dummyMetaData, - Amount: 1_000_000, - } - - shortTimeout = time.Second * 5 -) - -var ( - lndArgsTemplate = []string{ - "--trickledelay=50", - "--gossip.sub-batch-delay=5ms", - "--caches.rpc-graph-cache-duration=100ms", - "--default-remote-max-htlcs=483", - "--dust-threshold=5000000", - "--rpcmiddleware.enable", - "--protocol.anchors", - "--protocol.option-scid-alias", - "--protocol.zero-conf", - "--protocol.simple-taproot-chans", - "--protocol.simple-taproot-overlay-chans", - "--protocol.custom-message=17", - "--accept-keysend", - "--debuglevel=trace,GRPC=error,BTCN=info", - "--height-hint-cache-query-disable", - } - litdArgsTemplateNoOracle = []string{ - "--taproot-assets.allow-public-uni-proof-courier", - "--taproot-assets.universe.public-access=rw", - "--taproot-assets.universe.sync-all-assets", - "--taproot-assets.universerpccourier.skipinitdelay", - "--taproot-assets.universerpccourier.backoffresetwait=1s", - "--taproot-assets.universerpccourier.numtries=5", - "--taproot-assets.universerpccourier.initialbackoff=300ms", - "--taproot-assets.universerpccourier.maxbackoff=600ms", - "--taproot-assets.universerpccourier.skipinitdelay", - "--taproot-assets.universerpccourier.backoffresetwait=100ms", - "--taproot-assets.universerpccourier.initialbackoff=300ms", - "--taproot-assets.universerpccourier.maxbackoff=600ms", - "--taproot-assets.custodianproofretrievaldelay=500ms", - } - // nolint:ll - litdArgsTemplate = append(litdArgsTemplateNoOracle, []string{ - "--taproot-assets.experimental.rfq.priceoracletlsinsecure", - "--taproot-assets.experimental.rfq.priceoracleaddress=" + - "use_mock_price_oracle_service_promise_to_" + - "not_use_on_mainnet", - "--taproot-assets.experimental.rfq.mockoracleassetsperbtc=" + - "5820600", - "--taproot-assets.experimental.rfq.acceptpricedeviationppm=50000", - }...) - - // nolint:ll - litdArgsTemplateDiffOracle = append(litdArgsTemplateNoOracle, []string{ - "--taproot-assets.experimental.rfq.priceoracletlsinsecure", - "--taproot-assets.experimental.rfq.priceoracleaddress=" + - "use_mock_price_oracle_service_promise_to_" + - "not_use_on_mainnet", - "--taproot-assets.experimental.rfq.mockoracleassetsperbtc=" + - "8820600", - "--taproot-assets.experimental.rfq.acceptpricedeviationppm=50000", - }...) -) - -const ( - fundingAmount = 50_000 - startAmount = fundingAmount * 2 -) - -// testCustomChannelsLarge tests that we can create a network with custom -// channels and send large asset payments over them. -func testCustomChannelsLarge(_ context.Context, net *NetworkHarness, - t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - // The topology we are going for looks like the following: - // - // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia - // | - // | - // [assets] - // | - // v - // Yara - // - // With [assets] being a custom channel and [sats] being a normal, BTC - // only channel. - // All 5 nodes need to be full litd nodes running in integrated mode - // with tapd included. We also need specific flags to be enabled, so we - // create 5 completely new nodes, ignoring the two default nodes that - // are created by the harness. - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - fabia, err := net.NewNode( - t.t, "Fabia", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - yara, err := net.NewNode( - t.t, "Yara", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - // Create the normal channel between Dave and Erin. - t.Logf("Opening normal channel between Dave and Erin...") - channelOp := openChannelAndAssert( - t, net, dave, erin, lntest.OpenChannelParams{ - Amt: 10_000_000, - SatPerVByte: 5, - }, - ) - defer closeChannelAndAssert(t, net, dave, channelOp, false) - - // This is the only public channel, we need everyone to be aware of it. - assertChannelKnown(t.t, charlie, channelOp) - assertChannelKnown(t.t, fabia, channelOp) - - universeTap := newTapClient(t.t, charlie) - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - erinTap := newTapClient(t.t, erin) - fabiaTap := newTapClient(t.t, fabia) - yaraTap := newTapClient(t.t, yara) - - // Mint an asset on Charlie and sync all nodes to Charlie as the - // universe. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - - t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) - t.Logf("Universes synced between all nodes, distributing assets...") - - const ( - daveFundingAmount = uint64(400_000) - erinFundingAmount = uint64(200_000) - ) - charlieFundingAmount := cents.Amount - uint64(2*400_000) - - chanPointCD, _, _ := createTestAssetNetwork( - t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, - universeTap, cents, 400_000, charlieFundingAmount, - daveFundingAmount, erinFundingAmount, DefaultPushSat, - ) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) - - // Print initial channel balances. - logBalance(t.t, nodes, assetID, "initial") - - // Try larger invoice payments, first from Charlie to Fabia, then half - // of the amount back in the other direction. - const fabiaInvoiceAssetAmount = 20_000 - invoiceResp := createAssetInvoice( - t.t, erin, fabia, fabiaInvoiceAssetAmount, assetID, - ) - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, - ) - logBalance(t.t, nodes, assetID, "after invoice") - - invoiceResp2 := createAssetInvoice( - t.t, dave, charlie, fabiaInvoiceAssetAmount/2, assetID, - ) - - // Sleep for a second to make sure the balances fully propagated before - // we make the payment. Otherwise, we'll make an RFQ order with a max - // amount of zero. - time.Sleep(time.Second * 1) - - payInvoiceWithAssets( - t.t, fabia, erin, invoiceResp2.PaymentRequest, assetID, - ) - logBalance(t.t, nodes, assetID, "after invoice 2") - - // Now we send a large invoice from Charlie to Dave. - const largeInvoiceAmount = 100_000 - invoiceResp3 := createAssetInvoice( - t.t, charlie, dave, largeInvoiceAmount, assetID, - ) - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp3.PaymentRequest, assetID, - ) - logBalance(t.t, nodes, assetID, "after invoice 3") - - // Make sure the invoice on the receiver side and the payment on the - // sender side show the individual HTLCs that arrived for it and that - // they show the correct asset amounts when decoded. - assertInvoiceHtlcAssets( - t.t, dave, invoiceResp3, assetID, nil, largeInvoiceAmount, - ) - assertPaymentHtlcAssets( - t.t, charlie, invoiceResp3.RHash, assetID, nil, - largeInvoiceAmount, - ) - - // We keysend the rest, so that all the balance is on Dave's side. - charlieRemainingBalance := charlieFundingAmount - largeInvoiceAmount - - fabiaInvoiceAssetAmount/2 - sendAssetKeySendPayment( - t.t, charlie, dave, charlieRemainingBalance, - assetID, fn.None[int64](), - ) - logBalance(t.t, nodes, assetID, "after keysend") - - // And now we close the channel to test how things look if all the - // balance is on the non-initiator (recipient) side. - t.Logf("Closing Charlie -> Dave channel") - closeAssetChannelAndAssert( - t, net, charlie, dave, chanPointCD, [][]byte{assetID}, nil, - universeTap, initiatorZeroAssetBalanceCoOpBalanceCheck, - ) -} - -// testCustomChannels tests that we can create a network with custom channels -// and send asset payments over them. -func testCustomChannels(ctx context.Context, net *NetworkHarness, - t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - // The topology we are going for looks like the following: - // - // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia - // | - // | - // [assets] - // | - // v - // Yara - // - // With [assets] being a custom channel and [sats] being a normal, BTC - // only channel. - // All 5 nodes need to be full litd nodes running in integrated mode - // with tapd included. We also need specific flags to be enabled, so we - // create 5 completely new nodes, ignoring the two default nodes that - // are created by the harness. - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - fabia, err := net.NewNode( - t.t, "Fabia", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - yara, err := net.NewNode(t.t, "Yara", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - // Create the normal channel between Dave and Erin. - t.Logf("Opening normal channel between Dave and Erin...") - channelOp := openChannelAndAssert( - t, net, dave, erin, lntest.OpenChannelParams{ - Amt: 5_000_000, - SatPerVByte: 5, - }, - ) - defer closeChannelAndAssert(t, net, dave, channelOp, false) - - // This is the only public channel, we need everyone to be aware of it. - assertChannelKnown(t.t, charlie, channelOp) - assertChannelKnown(t.t, fabia, channelOp) - - universeTap := newTapClient(t.t, charlie) - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - erinTap := newTapClient(t.t, erin) - fabiaTap := newTapClient(t.t, fabia) - yaraTap := newTapClient(t.t, yara) - - // Mint an asset on Charlie and sync all nodes to Charlie as the - // universe. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - fundingScriptTree := tapscript.NewChannelFundingScriptTree() - fundingScriptKey := fundingScriptTree.TaprootKey - fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() - - t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) - t.Logf("Universes synced between all nodes, distributing assets...") - - const ( - daveFundingAmount = uint64(startAmount) - erinFundingAmount = uint64(fundingAmount) - ) - charlieFundingAmount := cents.Amount - 2*startAmount - - chanPointCD, chanPointDY, chanPointEF := createTestAssetNetwork( - t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, - universeTap, cents, startAmount, charlieFundingAmount, - daveFundingAmount, erinFundingAmount, DefaultPushSat, - ) - - // We'll be tracking the expected asset balances throughout the test, so - // we can assert it after each action. - charlieAssetBalance := charlieFundingAmount - daveAssetBalance := uint64(startAmount) - erinAssetBalance := uint64(startAmount) - fabiaAssetBalance := uint64(0) - yaraAssetBalance := uint64(0) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) - - // Print initial channel balances. - logBalance(t.t, nodes, assetID, "initial") - - // ------------ - // Test case 1: Send a direct keysend payment from Charlie to Dave, - // sending the whole balance. - // ------------ - keySendAmount := charlieFundingAmount - sendAssetKeySendPayment( - t.t, charlie, dave, charlieFundingAmount, assetID, - fn.None[int64](), - ) - logBalance(t.t, nodes, assetID, "after keysend") - - charlieAssetBalance -= keySendAmount - daveAssetBalance += keySendAmount - - // We should be able to send 1000 assets back immediately, because - // there is enough on-chain balance on Dave's side to be able to create - // an HTLC. We use an invoice to execute another code path. - const charlieInvoiceAmount = 1_000 - invoiceResp := createAssetInvoice( - t.t, dave, charlie, charlieInvoiceAmount, assetID, - ) - payInvoiceWithAssets( - t.t, dave, charlie, invoiceResp.PaymentRequest, assetID, - withSmallShards(), - ) - logBalance(t.t, nodes, assetID, "after invoice back") - - // Make sure the invoice on the receiver side and the payment on the - // sender side show the individual HTLCs that arrived for it and that - // they show the correct asset amounts when decoded. - assertInvoiceHtlcAssets( - t.t, charlie, invoiceResp, assetID, nil, charlieInvoiceAmount, - ) - assertPaymentHtlcAssets( - t.t, dave, invoiceResp.RHash, assetID, nil, - charlieInvoiceAmount, - ) - - charlieAssetBalance += charlieInvoiceAmount - daveAssetBalance -= charlieInvoiceAmount - - // We should also be able to do a non-asset (BTC only) keysend payment - // from Charlie to Dave. This'll also replenish the BTC balance of - // Dave, making it possible to send another asset HTLC below, sending - // all assets back to Charlie (so we have enough balance for further - // tests). - sendKeySendPayment(t.t, charlie, dave, 2000) - logBalance(t.t, nodes, assetID, "after BTC only keysend") - - // Let's keysend the rest of the balance back to Charlie. - sendAssetKeySendPayment( - t.t, dave, charlie, charlieFundingAmount-charlieInvoiceAmount, - assetID, fn.None[int64](), - ) - logBalance(t.t, nodes, assetID, "after keysend back") - - charlieAssetBalance += charlieFundingAmount - charlieInvoiceAmount - daveAssetBalance -= charlieFundingAmount - charlieInvoiceAmount - - // ------------ - // Test case 2: Pay a normal invoice from Dave by Charlie, making it - // a direct channel invoice payment with no RFQ SCID present in the - // invoice. - // ------------ - createAndPayNormalInvoice( - t.t, charlie, dave, dave, 20_000, assetID, withSmallShards(), - withFailure(lnrpc.Payment_FAILED, failureIncorrectDetails), - ) - logBalance(t.t, nodes, assetID, "after invoice") - - // We should also be able to do a multi-hop BTC only payment, paying an - // invoice from Erin by Charlie. - createAndPayNormalInvoiceWithBtc(t.t, charlie, erin, 2000) - logBalance(t.t, nodes, assetID, "after BTC only invoice") - - // ------------ - // Test case 3: Pay an asset invoice from Dave by Charlie, making it - // a direct channel invoice payment with an RFQ SCID present in the - // invoice. - // ------------ - const daveInvoiceAssetAmount = 2_000 - invoiceResp = createAssetInvoice( - t.t, charlie, dave, daveInvoiceAssetAmount, assetID, - ) - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, - withSmallShards(), - ) - logBalance(t.t, nodes, assetID, "after invoice") - - charlieAssetBalance -= daveInvoiceAssetAmount - daveAssetBalance += daveInvoiceAssetAmount - - // ------------ - // Test case 3.5: Pay an asset invoice from Dave by Charlie with normal - // satoshi payment flow. We expect that payment to fail, since it's a - // direct channel payment and the invoice is for assets, not sats. So - // without a conversion, it is rejected by the receiver. - // ------------ - invoiceResp = createAssetInvoice( - t.t, charlie, dave, daveInvoiceAssetAmount, assetID, - ) - payInvoiceWithSatoshi( - t.t, charlie, invoiceResp, withFailure( - lnrpc.Payment_FAILED, failureIncorrectDetails, - ), - ) - logBalance(t.t, nodes, assetID, "after asset invoice paid with sats") - - // We don't need to update the asset balances of Charlie and Dave here - // as the invoice payment failed. - - // ------------ - // Test case 4: Pay a normal invoice from Erin by Charlie. - // ------------ - paidAssetAmount := createAndPayNormalInvoice( - t.t, charlie, dave, erin, 20_000, assetID, withSmallShards(), - ) - logBalance(t.t, nodes, assetID, "after invoice") - - charlieAssetBalance -= paidAssetAmount - daveAssetBalance += paidAssetAmount - - // ------------ - // Test case 5: Create an asset invoice on Fabia and pay it from - // Charlie. - // ------------ - - // First send some sats from Erin to Fabia, for Fabia to have some - // minimal sats liquidity on her end. - sendKeySendPayment(t.t, erin, fabia, 5000) - - logBalance(t.t, nodes, assetID, "after erin->fabia sats keysend") - - const fabiaInvoiceAssetAmount1 = 1000 - invoiceResp = createAssetInvoice( - t.t, erin, fabia, fabiaInvoiceAssetAmount1, assetID, - ) - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, - withSmallShards(), - ) - logBalance(t.t, nodes, assetID, "after invoice") - - charlieAssetBalance -= fabiaInvoiceAssetAmount1 - daveAssetBalance += fabiaInvoiceAssetAmount1 - erinAssetBalance -= fabiaInvoiceAssetAmount1 - fabiaAssetBalance += fabiaInvoiceAssetAmount1 - - // ------------ - // Test case 6: Create an asset invoice on Fabia and pay it with just - // BTC from Dave, making sure it ends up being a multipart payment (we - // set the maximum shard size to 80k sat and 15k asset units will be - // more than a single shard). - // ------------ - const fabiaInvoiceAssetAmount2 = 15_000 - invoiceResp = createAssetInvoice( - t.t, erin, fabia, fabiaInvoiceAssetAmount2, assetID, - ) - payInvoiceWithSatoshi(t.t, dave, invoiceResp) - logBalance(t.t, nodes, assetID, "after invoice") - - erinAssetBalance -= fabiaInvoiceAssetAmount2 - fabiaAssetBalance += fabiaInvoiceAssetAmount2 - - // ------------ - // Test case 7: Create an asset invoice on Fabia and pay it with assets - // from Charlie, making sure it ends up being a multipart payment as - // well, with the high amount of asset units to send and the hard coded - // 80k sat max shard size. - // ------------ - const fabiaInvoiceAssetAmount3 = 10_000 - invoiceResp = createAssetInvoice( - t.t, erin, fabia, fabiaInvoiceAssetAmount3, assetID, - ) - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, - withSmallShards(), - ) - logBalance(t.t, nodes, assetID, "after invoice") - - charlieAssetBalance -= fabiaInvoiceAssetAmount3 - daveAssetBalance += fabiaInvoiceAssetAmount3 - erinAssetBalance -= fabiaInvoiceAssetAmount3 - fabiaAssetBalance += fabiaInvoiceAssetAmount3 - - // ------------ - // Test case 8: An invoice payment over two channels that are both asset - // channels. - // ------------ - logBalance(t.t, nodes, assetID, "before asset-to-asset") - - const yaraInvoiceAssetAmount1 = 1000 - invoiceResp = createAssetInvoice( - t.t, dave, yara, yaraInvoiceAssetAmount1, assetID, - ) - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, - withSmallShards(), - ) - logBalance(t.t, nodes, assetID, "after asset-to-asset") - - charlieAssetBalance -= yaraInvoiceAssetAmount1 - yaraAssetBalance += yaraInvoiceAssetAmount1 - - // ------------ - // Test case 8: Now we'll close each of the channels, starting with the - // Charlie -> Dave custom channel. - // ------------ - t.Logf("Closing Charlie -> Dave channel") - closeAssetChannelAndAssert( - t, net, charlie, dave, chanPointCD, [][]byte{assetID}, nil, - universeTap, assertDefaultCoOpCloseBalance(true, true), - ) - - t.Logf("Closing Dave -> Yara channel, close initiated by Yara") - closeAssetChannelAndAssert( - t, net, yara, dave, chanPointDY, [][]byte{assetID}, nil, - universeTap, assertDefaultCoOpCloseBalance(false, true), - ) - - t.Logf("Closing Erin -> Fabia channel") - closeAssetChannelAndAssert( - t, net, erin, fabia, chanPointEF, [][]byte{assetID}, nil, - universeTap, assertDefaultCoOpCloseBalance(true, true), - ) - - // We've been tracking the off-chain channel balances all this time, so - // now that we have the assets on-chain again, we can assert them. Due - // to rounding errors that happened when sending multiple shards with - // MPP, we need to do some slight adjustments. - charlieAssetBalance += 1 - erinAssetBalance += 3 - fabiaAssetBalance -= 3 - yaraAssetBalance -= 1 - assertBalance( - t.t, charlieTap, charlieAssetBalance, - itest.WithAssetID(assetID), - ) - assertBalance( - t.t, daveTap, daveAssetBalance, itest.WithAssetID(assetID), - ) - assertBalance( - t.t, erinTap, erinAssetBalance, itest.WithAssetID(assetID), - ) - assertBalance( - t.t, fabiaTap, fabiaAssetBalance, itest.WithAssetID(assetID), - ) - assertBalance( - t.t, yaraTap, yaraAssetBalance, itest.WithAssetID(assetID), - ) - - // ------------ - // Test case 10: We now open a new asset channel and close it again, to - // make sure that a non-existent remote balance is handled correctly. - t.Logf("Opening new asset channel between Charlie and Dave...") - fundRespCD, err := charlieTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetID, - PeerPubkey: dave.PubKey[:], - FeeRateSatPerVbyte: 5, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded second channel between Charlie and Dave: %v", fundRespCD) - - mineBlocks(t, net, 6, 1) - - // Assert that the proofs for both channels has been uploaded to the - // designated Universe server. - assertUniverseProofExists( - t.t, universeTap, assetID, nil, fundingScriptTreeBytes, - fmt.Sprintf("%v:%v", fundRespCD.Txid, fundRespCD.OutputIndex), - ) - assertAssetChan( - t.t, charlie, dave, fundingAmount, []*taprpc.Asset{cents}, - ) - - // And let's just close the channel again. - chanPointCD = &lnrpc.ChannelPoint{ - OutputIndex: uint32(fundRespCD.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: fundRespCD.Txid, - }, - } - - t.Logf("Closing Charlie -> Dave channel") - closeAssetChannelAndAssert( - t, net, charlie, dave, chanPointCD, [][]byte{assetID}, nil, - universeTap, assertDefaultCoOpCloseBalance(false, false), - ) - - // Charlie should still have four asset pieces, two with the same size. - assertBalance( - t.t, charlieTap, charlieAssetBalance, - itest.WithAssetID(assetID), itest.WithNumUtxos(2), - itest.WithScriptKeyType(asset.ScriptKeyBip86), - ) - - // Dave should have two outputs, one from the initial channel with Yara - // and one from the remaining amount of the channel with Charlie. - assertBalance( - t.t, daveTap, daveAssetBalance, itest.WithAssetID(assetID), - itest.WithNumUtxos(2), - itest.WithScriptKeyType(asset.ScriptKeyBip86), - ) - - // Fabia and Yara should all have a single output each, just what was - // left over from the initial channel. - assertBalance( - t.t, fabiaTap, fabiaAssetBalance, itest.WithAssetID(assetID), - itest.WithNumUtxos(1), - itest.WithScriptKeyType(asset.ScriptKeyBip86), - ) - assertBalance( - t.t, yaraTap, yaraAssetBalance, itest.WithAssetID(assetID), - itest.WithNumUtxos(1), - itest.WithScriptKeyType(asset.ScriptKeyBip86), - ) - - // Erin didn't use all of his assets when opening the channel, so he - // should have two outputs, the change from the channel opening and the - // remaining amount after closing the channel. - assertBalance( - t.t, erinTap, erinAssetBalance, itest.WithAssetID(assetID), - itest.WithNumUtxos(2), - itest.WithScriptKeyType(asset.ScriptKeyBip86), - ) - - // The asset balances should still remain unchanged. - assertBalance( - t.t, charlieTap, charlieAssetBalance, - itest.WithAssetID(assetID), - ) - assertBalance( - t.t, daveTap, daveAssetBalance, itest.WithAssetID(assetID), - ) - assertBalance( - t.t, erinTap, erinAssetBalance, itest.WithAssetID(assetID), - ) - assertBalance( - t.t, fabiaTap, fabiaAssetBalance, itest.WithAssetID(assetID), - ) -} - -// testCustomChannelsGroupedAsset tests that we can create a network with custom -// channels that use grouped assets and send asset payments over them. -func testCustomChannelsGroupedAsset(ctx context.Context, net *NetworkHarness, - t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - // The topology we are going for looks like the following: - // - // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia - // | - // | - // [assets] - // | - // v - // Yara - // - // With [assets] being a custom channel and [sats] being a normal, BTC - // only channel. - // All 5 nodes need to be full litd nodes running in integrated mode - // with tapd included. We also need specific flags to be enabled, so we - // create 5 completely new nodes, ignoring the two default nodes that - // are created by the harness. - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - fabia, err := net.NewNode( - t.t, "Fabia", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - yara, err := net.NewNode( - t.t, "Yara", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - // Create the normal channel between Dave and Erin. - t.Logf("Opening normal channel between Dave and Erin...") - channelOp := openChannelAndAssert( - t, net, dave, erin, lntest.OpenChannelParams{ - Amt: 5_000_000, - SatPerVByte: 5, - }, - ) - defer closeChannelAndAssert(t, net, dave, channelOp, false) - - // This is the only public channel, we need everyone to be aware of it. - assertChannelKnown(t.t, charlie, channelOp) - assertChannelKnown(t.t, fabia, channelOp) - - universeTap := newTapClient(t.t, charlie) - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - erinTap := newTapClient(t.t, erin) - fabiaTap := newTapClient(t.t, fabia) - yaraTap := newTapClient(t.t, yara) - - groupAssetReq := itest.CopyRequest(&mintrpc.MintAssetRequest{ - Asset: itestAsset, - }) - groupAssetReq.Asset.NewGroupedAsset = true - - // Mint an asset on Charlie and sync all nodes to Charlie as the - // universe. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{groupAssetReq}, - ) - - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - groupID := cents.GetAssetGroup().GetTweakedGroupKey() - groupKey, err := btcec.ParsePubKey(groupID) - require.NoError(t.t, err) - fundingScriptTree := tapscript.NewChannelFundingScriptTree() - fundingScriptKey := fundingScriptTree.TaprootKey - fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() - - t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) - t.Logf("Universes synced between all nodes, distributing assets...") - - const ( - daveFundingAmount = uint64(startAmount) - erinFundingAmount = uint64(fundingAmount) - ) - charlieFundingAmount := cents.Amount - 2*startAmount - - chanPointCD, chanPointDY, chanPointEF := createTestAssetNetwork( - t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, - universeTap, cents, startAmount, charlieFundingAmount, - daveFundingAmount, erinFundingAmount, DefaultPushSat, - ) - - // We'll be tracking the expected asset balances throughout the test, so - // we can assert it after each action. - charlieAssetBalance := charlieFundingAmount - daveAssetBalance := uint64(startAmount) - erinAssetBalance := uint64(startAmount) - fabiaAssetBalance := uint64(0) - yaraAssetBalance := uint64(0) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) - - // Print initial channel balances. - logBalance(t.t, nodes, assetID, "initial") - - // ------------ - // Test case 1: Send a direct keysend payment from Charlie to Dave. - // ------------ - const keySendAmount = 100 - sendAssetKeySendPayment( - t.t, charlie, dave, keySendAmount, nil, fn.None[int64](), - withGroupKey(groupID), - ) - logBalance(t.t, nodes, assetID, "after keysend") - - charlieAssetBalance -= keySendAmount - daveAssetBalance += keySendAmount - - // We should be able to send the 100 assets back immediately, because - // there is enough on-chain balance on Dave's side to be able to create - // an HTLC. - sendAssetKeySendPayment( - t.t, dave, charlie, keySendAmount, assetID, fn.None[int64](), - ) - logBalance(t.t, nodes, assetID, "after keysend back") - - charlieAssetBalance += keySendAmount - daveAssetBalance -= keySendAmount - - // We should also be able to do a non-asset (BTC only) keysend payment. - sendKeySendPayment(t.t, charlie, dave, 2000) - logBalance(t.t, nodes, assetID, "after BTC only keysend") - - // ------------ - // Test case 2: Pay a normal invoice from Dave by Charlie, making it - // a direct channel invoice payment with no RFQ SCID present in the - // invoice. - // ------------ - createAndPayNormalInvoice( - t.t, charlie, dave, dave, 20_000, nil, withSmallShards(), - withFailure(lnrpc.Payment_FAILED, failureIncorrectDetails), - withGroupKey(groupID), - ) - logBalance(t.t, nodes, assetID, "after failed invoice") - - // We should also be able to do a multi-hop BTC only payment, paying an - // invoice from Erin by Charlie. - createAndPayNormalInvoiceWithBtc(t.t, charlie, erin, 2000) - logBalance(t.t, nodes, assetID, "after BTC only invoice") - - // ------------ - // Test case 3: Pay an asset invoice from Dave by Charlie, making it - // a direct channel invoice payment with an RFQ SCID present in the - // invoice. - // ------------ - const daveInvoiceAssetAmount = 2_000 - invoiceResp := createAssetInvoice( - t.t, charlie, dave, daveInvoiceAssetAmount, nil, - withInvGroupKey(groupID), - ) - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, nil, - withSmallShards(), withGroupKey(groupID), - ) - logBalance(t.t, nodes, assetID, "after invoice") - - groupBytes := schnorr.SerializePubKey(groupKey) - - // Make sure the invoice on the receiver side and the payment on the - // sender side show the individual HTLCs that arrived for it and that - // they show the correct asset amounts when decoded. - assertInvoiceHtlcAssets( - t.t, dave, invoiceResp, nil, groupBytes, daveInvoiceAssetAmount, - ) - assertPaymentHtlcAssets( - t.t, charlie, invoiceResp.RHash, nil, groupBytes, - daveInvoiceAssetAmount, - ) - - charlieAssetBalance -= daveInvoiceAssetAmount - daveAssetBalance += daveInvoiceAssetAmount - - // ------------ - // Test case 4: Pay a normal invoice from Erin by Charlie. - // ------------ - paidAssetAmount := createAndPayNormalInvoice( - t.t, charlie, dave, erin, 20_000, nil, withSmallShards(), - withGroupKey(groupID), - ) - logBalance(t.t, nodes, assetID, "after invoice") - - charlieAssetBalance -= paidAssetAmount - daveAssetBalance += paidAssetAmount - - // ------------ - // Test case 5: Create an asset invoice on Fabia and pay it from - // Charlie. - // ------------ - - // First send some sats from Erin to Fabia, for Fabia to have some - // minimal sats liquidity on her end. - sendKeySendPayment(t.t, erin, fabia, 5000) - - logBalance(t.t, nodes, assetID, "after erin->fabia sats keysend") - - const fabiaInvoiceAssetAmount1 = 1000 - invoiceResp = createAssetInvoice( - t.t, erin, fabia, fabiaInvoiceAssetAmount1, nil, - withInvGroupKey(groupID), - ) - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, - withSmallShards(), - ) - logBalance(t.t, nodes, assetID, "after invoice") - - charlieAssetBalance -= fabiaInvoiceAssetAmount1 - daveAssetBalance += fabiaInvoiceAssetAmount1 - erinAssetBalance -= fabiaInvoiceAssetAmount1 - fabiaAssetBalance += fabiaInvoiceAssetAmount1 - - // ------------ - // Test case 6: Create an asset invoice on Fabia and pay it with just - // BTC from Dave, making sure it ends up being a multipart payment (we - // set the maximum shard size to 80k sat and 15k asset units will be - // more than a single shard). - // ------------ - const fabiaInvoiceAssetAmount2 = 15_000 - invoiceResp = createAssetInvoice( - t.t, erin, fabia, fabiaInvoiceAssetAmount2, assetID, - ) - payInvoiceWithSatoshi(t.t, dave, invoiceResp) - logBalance(t.t, nodes, assetID, "after invoice") - - erinAssetBalance -= fabiaInvoiceAssetAmount2 - fabiaAssetBalance += fabiaInvoiceAssetAmount2 - - // ------------ - // Test case 7: Create an asset invoice on Fabia and pay it with assets - // from Charlie, making sure it ends up being a multipart payment as - // well, with the high amount of asset units to send and the hard coded - // 80k sat max shard size. - // ------------ - const fabiaInvoiceAssetAmount3 = 10_000 - invoiceResp = createAssetInvoice( - t.t, erin, fabia, fabiaInvoiceAssetAmount3, assetID, - ) - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, nil, - withSmallShards(), withGroupKey(groupID), - ) - logBalance(t.t, nodes, assetID, "after invoice") - - charlieAssetBalance -= fabiaInvoiceAssetAmount3 - daveAssetBalance += fabiaInvoiceAssetAmount3 - erinAssetBalance -= fabiaInvoiceAssetAmount3 - fabiaAssetBalance += fabiaInvoiceAssetAmount3 - - // ------------ - // Test case 8: An invoice payment over two channels that are both asset - // channels. - // ------------ - logBalance(t.t, nodes, assetID, "before asset-to-asset") - - const yaraInvoiceAssetAmount1 = 1000 - invoiceResp = createAssetInvoice( - t.t, dave, yara, yaraInvoiceAssetAmount1, nil, - withInvGroupKey(groupID), - ) - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, - withSmallShards(), - ) - logBalance(t.t, nodes, assetID, "after asset-to-asset") - - charlieAssetBalance -= yaraInvoiceAssetAmount1 - yaraAssetBalance += yaraInvoiceAssetAmount1 - - // ------------ - // Test case 8: Now we'll close each of the channels, starting with the - // Charlie -> Dave custom channel. - // ------------ - t.Logf("Closing Charlie -> Dave channel") - closeAssetChannelAndAssert( - t, net, charlie, dave, chanPointCD, [][]byte{assetID}, groupID, - universeTap, assertDefaultCoOpCloseBalance(true, true), - ) - - t.Logf("Closing Dave -> Yara channel, close initiated by Yara") - closeAssetChannelAndAssert( - t, net, yara, dave, chanPointDY, [][]byte{assetID}, groupID, - universeTap, assertDefaultCoOpCloseBalance(false, true), - ) - - t.Logf("Closing Erin -> Fabia channel") - closeAssetChannelAndAssert( - t, net, erin, fabia, chanPointEF, [][]byte{assetID}, groupID, - universeTap, assertDefaultCoOpCloseBalance(true, true), - ) - - // We've been tracking the off-chain channel balances all this time, so - // now that we have the assets on-chain again, we can assert them. Due - // to rounding errors that happened when sending multiple shards with - // MPP, we need to do some slight adjustments. - charlieAssetBalance += 2 - daveAssetBalance -= 1 - erinAssetBalance += 3 - fabiaAssetBalance -= 3 - yaraAssetBalance -= 1 - 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), - ) - itest.AssertBalances( - t.t, fabiaTap, fabiaAssetBalance, itest.WithAssetID(assetID), - ) - itest.AssertBalances( - t.t, yaraTap, yaraAssetBalance, itest.WithAssetID(assetID), - ) - - // ------------ - // Test case 10: We now open a new asset channel and close it again, to - // make sure that a non-existent remote balance is handled correctly. - t.Logf("Opening new asset channel between Charlie and Dave...") - fundRespCD, err := charlieTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetID, - PeerPubkey: dave.PubKey[:], - FeeRateSatPerVbyte: 5, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded second channel between Charlie and Dave: %v", fundRespCD) - - mineBlocks(t, net, 6, 1) - - // Assert that the proofs for both channels has been uploaded to the - // designated Universe server. - assertUniverseProofExists( - t.t, universeTap, nil, groupID, fundingScriptTreeBytes, - fmt.Sprintf("%v:%v", fundRespCD.Txid, fundRespCD.OutputIndex), - ) - assertAssetChan( - t.t, charlie, dave, fundingAmount, []*taprpc.Asset{cents}, - ) - - // And let's just close the channel again. - chanPointCD = &lnrpc.ChannelPoint{ - OutputIndex: uint32(fundRespCD.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: fundRespCD.Txid, - }, - } - - t.Logf("Closing Charlie -> Dave channel") - closeAssetChannelAndAssert( - t, net, charlie, dave, chanPointCD, [][]byte{assetID}, groupID, - universeTap, assertDefaultCoOpCloseBalance(false, false), - ) - - // Charlie should have asset outputs: the leftover change from the - // channel funding, and the new close output. - itest.AssertBalances( - t.t, charlieTap, charlieAssetBalance, - itest.WithAssetID(assetID), itest.WithNumUtxos(2), - ) - - // The asset balances should still remain unchanged. - 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), - ) - itest.AssertBalances( - t.t, fabiaTap, fabiaAssetBalance, itest.WithAssetID(assetID), - ) -} - -// testCustomChannelsGroupTranchesForceClose tests that we can successfully open -// a custom channel with multiple pieces of a grouped asset. We then test that -// we can successfully co-op and force close such channels and sweep the -// remaining channel balances. -func testCustomChannelsGroupTranchesForceClose(ctx context.Context, - net *NetworkHarness, t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - // The topology we are going for looks like the following: - // - // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia - // - // With [assets] being a custom channel and [sats] being a normal, BTC - // only channel. - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - fabia, err := net.NewNode( - t.t, "Fabia", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave, erin, fabia} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - // Create the normal channel between Dave and Erin. - t.Logf("Opening normal channel between Dave and Erin...") - channelOp := openChannelAndAssert( - t, net, dave, erin, lntest.OpenChannelParams{ - Amt: 5_000_000, - SatPerVByte: 5, - }, - ) - defer closeChannelAndAssert(t, net, dave, channelOp, false) - - // This is the only public channel, we need everyone to be aware of it. - assertChannelKnown(t.t, charlie, channelOp) - assertChannelKnown(t.t, fabia, channelOp) - - universeTap := newTapClient(t.t, charlie) - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - erinTap := newTapClient(t.t, erin) - fabiaTap := newTapClient(t.t, fabia) - - groupAssetReq := itest.CopyRequest(&mintrpc.MintAssetRequest{ - Asset: itestAsset, - }) - groupAssetReq.Asset.NewGroupedAsset = true - - // Mint the asset tranches 1 and 2 on Charlie and sync all nodes to - // Charlie as the universe. - mintedAssetsT1 := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{groupAssetReq}, - ) - centsT1 := mintedAssetsT1[0] - assetID1 := centsT1.AssetGenesis.AssetId - groupKey := centsT1.GetAssetGroup().GetTweakedGroupKey() - - groupAssetReq = itest.CopyRequest(&mintrpc.MintAssetRequest{ - Asset: itestAsset, - }) - groupAssetReq.Asset.GroupedAsset = true - groupAssetReq.Asset.GroupKey = groupKey - groupAssetReq.Asset.Name = "itest-asset-cents-tranche-2" - - mintedAssetsT2 := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{groupAssetReq}, - ) - centsT2 := mintedAssetsT2[0] - assetID2 := centsT2.AssetGenesis.AssetId - - t.Logf("Minted lightning cents tranche 1 (%x) and 2 (%x) for group "+ - "key %x, syncing universes...", assetID1, assetID2, groupKey) - syncUniverses(t.t, charlieTap, dave, erin, fabia) - t.Logf("Universes synced between all nodes, distributing assets...") - - chanPointCD, chanPointEF := createTestAssetNetworkGroupKey( - ctx, t, net, charlieTap, daveTap, erinTap, fabiaTap, - universeTap, []*taprpc.Asset{centsT1, centsT2}, - fundingAmount, fundingAmount, DefaultPushSat, - ) - - t.Logf("Created channels %v and %v", chanPointCD, chanPointEF) - - // We now send some assets over the channels to test the functionality. - // Print initial channel balances. - groupIDs := [][]byte{assetID1, assetID2} - logBalanceGroup(t.t, nodes, groupIDs, "initial") - - // ------------ - // Test case 1: Send a few direct keysend payments from Charlie to Dave. - // We want to send at least 30k assets, so we use up one channel - // internal tranche of assets and should at least once have an HTLC - // that transports assets from two tranches. - // ------------ - const ( - keySendAmount = 5000 - keySendSatAmount = 5000 - numSends = 6 - totalFirstSend = keySendAmount * numSends - ) - for i := 0; i < numSends; i++ { - sendAssetKeySendPayment( - t.t, charlie, dave, keySendAmount, nil, - fn.None[int64](), withGroupKey(groupKey), - ) - } - - // With noop HTLCs implemented the sats balance of Dave will only - // increase up to the reserve amount. Let's make a direct non-asset - // keysend to make sure the sats balance is also enough. - sendKeySendPayment(t.t, charlie, dave, keySendSatAmount) - - logBalanceGroup(t.t, nodes, groupIDs, "after keysend Charlie->Dave") - - // ------------ - // Test case 2: Send a few direct keysend payments from Erin to Fabia. - // ------------ - for i := 0; i < numSends; i++ { - sendAssetKeySendPayment( - t.t, erin, fabia, keySendAmount, nil, - fn.None[int64](), withGroupKey(groupKey), - ) - } - logBalanceGroup(t.t, nodes, groupIDs, "after keysend Erin->Fabia") - - // We also assert that in a grouped channel with multiple grouped asset - // UTXOs we get a proper error if we try to do payments or create - // invoices while using a single asset ID. - sendAssetKeySendPayment( - t.t, erin, fabia, keySendAmount, assetID1, fn.None[int64](), - withPayErrSubStr( - "make sure to use group key for grouped asset channels", - ), - ) - createAssetInvoice( - t.t, charlie, dave, 100, assetID1, withInvoiceErrSubStr( - "make sure to use group key for grouped asset channels", - ), - ) - invoiceResp := createAssetInvoice( - t.t, charlie, dave, keySendAmount, nil, - withInvGroupKey(groupKey), - ) - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, assetID1, - withPayErrSubStr( - "make sure to use group key for grouped asset channels", - ), - ) - - // ------------ - // Test case 3: Co-op close the channel between Charlie and Dave. - // ------------ - t.Logf("Closing Charlie -> Dave channel") - closeAssetChannelAndAssert( - t, net, charlie, dave, chanPointCD, - [][]byte{assetID1, assetID2}, groupKey, universeTap, - assertDefaultCoOpCloseBalance(true, true), - ) - - assertSpendableBalance( - t.t, charlieTap, nil, groupKey, fundingAmount-totalFirstSend+2, - ) - assertSpendableBalance(t.t, daveTap, nil, groupKey, totalFirstSend) - - // ------------ - // Test case 4: Force close the channel between Erin and Fabia. - // ------------ - _, closeTxid, err := net.CloseChannel(erin, chanPointEF, true) - require.NoError(t.t, err) - - t.Logf("Channel force closed! Mining blocks, close_txid=%v", closeTxid) - - // Next, we'll mine a block to confirm the force close. - mineBlocks(t, net, 1, 1) - - // At this point, we should have the force close transaction in the set - // of transfers for both nodes. - forceCloseTransfer := findForceCloseTransfer( - t.t, erinTap, fabiaTap, closeTxid, - ) - t.Logf("Force close transfer: %v", toProtoJSON(t.t, forceCloseTransfer)) - - // Now that we have the transfer on disk, we'll also assert that the - // universe also has proof for both the relevant transfer outputs. - for _, transfer := range forceCloseTransfer.Transfers { - for _, transferOut := range transfer.Outputs { - assertUniverseProofExists( - t.t, universeTap, transferOut.AssetId, groupKey, - transferOut.ScriptKey, - transferOut.Anchor.Outpoint, - ) - } - } - - t.Logf("Universe proofs located!") - - // We should also have a new sweep transaction in the mempool. - fabiaSweepTxid, err := waitForNTxsInMempool( - net.Miner.Client, 1, shortTimeout, - ) - require.NoError(t.t, err) - - t.Logf("Fabia sweep txid: %v", fabiaSweepTxid) - - mineBlocks(t, net, 1, 1) - - // Fabia should have her sweep output confirmed now, and the assets - // should be back in her on-chain wallet and spendable. - assertSpendableBalance(t.t, fabiaTap, nil, groupKey, totalFirstSend) - - // Next, we'll mine three additional blocks to trigger the CSV delay - // for Erin. - mineBlocks(t, net, 4, 0) - - // We expect that Erin's sweep transaction has been broadcast. - erinSweepTxid, err := waitForNTxsInMempool( - net.Miner.Client, 1, shortTimeout, - ) - require.NoError(t.t, err) - - t.Logf("Erin sweep txid: %v", erinSweepTxid) - - // Now we'll mine a block to confirm Erin's sweep transaction. - mineBlocks(t, net, 1, 1) - - // Charlie should now have an asset transfer for his sweep transaction. - erinSweepTransfer := locateAssetTransfers( - t.t, erinTap, *erinSweepTxid[0], - ) - - t.Logf("Erin sweep transfer: %v", toProtoJSON(t.t, erinSweepTransfer)) - - assertSpendableBalance( - t.t, erinTap, nil, groupKey, fundingAmount-totalFirstSend, - ) -} - -// testCustomChannelsGroupTranchesHtlcForceClose tests that we can successfully -// open a custom channel with multiple pieces of a grouped asset, then force -// close it while having pending HTLCs. We then test that we can successfully -// sweep all balances from those HTLCs. -func testCustomChannelsGroupTranchesHtlcForceClose(ctx context.Context, - net *NetworkHarness, t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - // The topology we are going for looks like the following: - // - // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia - // - // With [assets] being a custom channel and [sats] being a normal, BTC - // only channel. - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - fabia, err := net.NewNode( - t.t, "Fabia", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave, erin, fabia} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - // Create the normal channel between Dave and Erin. - t.Logf("Opening normal channel between Dave and Erin...") - channelOp := openChannelAndAssert( - t, net, dave, erin, lntest.OpenChannelParams{ - Amt: 5_000_000, - SatPerVByte: 5, - }, - ) - defer closeChannelAndAssert(t, net, dave, channelOp, false) - - // This is the only public channel, we need everyone to be aware of it. - assertChannelKnown(t.t, charlie, channelOp) - assertChannelKnown(t.t, fabia, channelOp) - - universeTap := newTapClient(t.t, charlie) - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - erinTap := newTapClient(t.t, erin) - fabiaTap := newTapClient(t.t, fabia) - - groupAssetReq := itest.CopyRequest(&mintrpc.MintAssetRequest{ - Asset: itestAsset, - }) - groupAssetReq.Asset.NewGroupedAsset = true - - // Mint the asset tranches 1 and 2 on Charlie and sync all nodes to - // Charlie as the universe. - mintedAssetsT1 := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{groupAssetReq}, - ) - centsT1 := mintedAssetsT1[0] - assetID1 := centsT1.AssetGenesis.AssetId - groupKey := centsT1.GetAssetGroup().GetTweakedGroupKey() - - groupAssetReq = itest.CopyRequest(&mintrpc.MintAssetRequest{ - Asset: itestAsset, - }) - groupAssetReq.Asset.GroupedAsset = true - groupAssetReq.Asset.GroupKey = groupKey - groupAssetReq.Asset.Name = "itest-asset-cents-tranche-2" - - mintedAssetsT2 := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{groupAssetReq}, - ) - centsT2 := mintedAssetsT2[0] - assetID2 := centsT2.AssetGenesis.AssetId - - t.Logf("Minted lightning cents tranche 1 (%x) and 2 (%x) for group "+ - "key %x, syncing universes...", assetID1, assetID2, groupKey) - syncUniverses(t.t, charlieTap, dave, erin, fabia) - t.Logf("Universes synced between all nodes, distributing assets...") - - chanPointCD, chanPointEF := createTestAssetNetworkGroupKey( - ctx, t, net, charlieTap, daveTap, erinTap, fabiaTap, - universeTap, []*taprpc.Asset{centsT1, centsT2}, - fundingAmount, fundingAmount, DefaultPushSat, - ) - - t.Logf("Created channels %v and %v", chanPointCD, chanPointEF) - - // We now send some assets over the channels to test the functionality. - // Print initial channel balances. - groupIDs := [][]byte{assetID1, assetID2} - logBalanceGroup(t.t, nodes, groupIDs, "initial") - - // First, we'll send over some funds from Charlie to Dave, as we want - // Dave to be able to extend HTLCs in the other direction. - const ( - numPayments = 10 - keySendAmount = 2_500 - keySendSatAmount = 5_000 - ) - for i := 0; i < numPayments; i++ { - sendAssetKeySendPayment( - t.t, charlie, dave, keySendAmount, nil, - fn.None[int64](), withGroupKey(groupKey), - ) - } - - // With noop HTLCs implemented the sats balance of Dave will only - // increase up to the reserve amount. Let's make a direct non-asset - // keysend to make sure the sats balance is also enough. - sendKeySendPayment(t.t, charlie, dave, keySendSatAmount) - - // Now that both parties have some funds, we'll move onto the main test. - // - // We'll make 2 hodl invoice for each peer, so 4 total. From Charlie's - // PoV, he'll have 6 outgoing HTLCs, and two incoming HTLCs. - var ( - daveHodlInvoices []assetHodlInvoice - charlieHodlInvoices []assetHodlInvoice - - // The default oracle rate is 17_180 mSat/asset unit, so 10_000 - // will be equal to 171_800_000 mSat. When we use the mpp bool - // for the smallShards param of payInvoiceWithAssets, that - // means we'll split the payment into shards of 80_000_000 mSat - // max. So we'll get three shards per payment. - assetInvoiceAmt = 10_000 - assetsPerMPPShard = 4656 - ) - for i := 0; i < 2; i++ { - daveHodlInvoices = append( - daveHodlInvoices, createAssetHodlInvoice( - t.t, charlie, dave, uint64(assetInvoiceAmt), - nil, withInvGroupKey(groupKey), - ), - ) - charlieHodlInvoices = append( - charlieHodlInvoices, createAssetHodlInvoice( - t.t, dave, charlie, uint64(assetInvoiceAmt), - nil, withInvGroupKey(groupKey), - ), - ) - } - - // Now we'll have both Dave and Charlie pay each other's invoices. We - // only care that they're in flight at this point, as they won't be - // settled yet. - baseOpts := []payOpt{ - withGroupKey(groupKey), - withFailure( - lnrpc.Payment_IN_FLIGHT, - lnrpc.PaymentFailureReason_FAILURE_REASON_NONE, - ), - } - for _, charlieInvoice := range charlieHodlInvoices { - // For this direction, we also want to enforce MPP. - opts := append(slices.Clone(baseOpts), withSmallShards()) - payInvoiceWithAssets( - t.t, dave, charlie, charlieInvoice.payReq, nil, opts..., - ) - } - for _, daveInvoice := range daveHodlInvoices { - payInvoiceWithAssets( - t.t, charlie, dave, daveInvoice.payReq, nil, - baseOpts..., - ) - } - - // Make sure we can sweep all the HTLCs. - const charlieStartAmount = 2 - charlieExpectedBalance, _ := assertForceCloseSweeps( - ctx, net, t, charlie, dave, chanPointCD, charlieStartAmount, - assetInvoiceAmt, assetsPerMPPShard, nil, groupKey, - charlieHodlInvoices, daveHodlInvoices, true, - ) - - // Finally, we'll assert that Charlie's balance has been incremented by - // the timeout value. - charlieExpectedBalance += uint64(assetInvoiceAmt - 1) - t.Logf("Expecting Charlie's balance to be %d", charlieExpectedBalance) - assertSpendableBalance( - t.t, charlieTap, nil, groupKey, charlieExpectedBalance, - ) - - t.Logf("Sending all settled funds to Fabia") - - // As a final sanity check, both Charlie and Dave should be able to send - // their entire balances to Fabia, our 3rd party. - // - // We'll make two addrs for Fabia, one for Charlie, and one for Dave. - charlieSpendableBalanceAsset1, err := spendableBalance( - charlieTap, assetID1, nil, - ) - require.NoError(t.t, err) - charlieSpendableBalanceAsset2, err := spendableBalance( - charlieTap, assetID2, nil, - ) - require.NoError(t.t, err) - - t.Logf("Charlie's spendable balance asset 1: %d, asset 2: %d", - charlieSpendableBalanceAsset1, charlieSpendableBalanceAsset2) - - fabiaCourierAddr := fmt.Sprintf( - "%s://%s", proof.UniverseRpcCourierType, - fabiaTap.node.Cfg.LitAddr(), - ) - charlieAddr1, err := fabiaTap.NewAddr(ctx, &taprpc.NewAddrRequest{ - Amt: charlieSpendableBalanceAsset1, - AssetId: assetID1, - ProofCourierAddr: fabiaCourierAddr, - }) - require.NoError(t.t, err) - charlieAddr2, err := fabiaTap.NewAddr(ctx, &taprpc.NewAddrRequest{ - Amt: charlieSpendableBalanceAsset2, - AssetId: assetID2, - ProofCourierAddr: fabiaCourierAddr, - }) - require.NoError(t.t, err) - - daveSpendableBalanceAsset1, err := spendableBalance( - daveTap, assetID1, nil, - ) - require.NoError(t.t, err) - daveSpendableBalanceAsset2, err := spendableBalance( - daveTap, assetID2, nil, - ) - require.NoError(t.t, err) - - t.Logf("Daves's spendable balance asset 1: %d, asset 2: %d", - daveSpendableBalanceAsset1, daveSpendableBalanceAsset2) - - daveAddr1, err := fabiaTap.NewAddr(ctx, &taprpc.NewAddrRequest{ - Amt: daveSpendableBalanceAsset1, - AssetId: assetID1, - ProofCourierAddr: fabiaCourierAddr, - }) - require.NoError(t.t, err) - daveAddr2, err := fabiaTap.NewAddr(ctx, &taprpc.NewAddrRequest{ - Amt: daveSpendableBalanceAsset2, - AssetId: assetID2, - ProofCourierAddr: fabiaCourierAddr, - }) - require.NoError(t.t, err) - - _, err = charlieTap.SendAsset(ctx, &taprpc.SendAssetRequest{ - TapAddrs: []string{charlieAddr1.Encoded}, - }) - require.NoError(t.t, err) - mineBlocks(t, net, 1, 1) - - itest.AssertNonInteractiveRecvComplete(t.t, fabiaTap, 1) - - ctxb := context.Background() - charlieAssets, err := charlieTap.ListAssets( - ctxb, &taprpc.ListAssetRequest{ - IncludeSpent: true, - }, - ) - require.NoError(t.t, err) - charlieTransfers, err := charlieTap.ListTransfers( - ctxb, &taprpc.ListTransfersRequest{}, - ) - require.NoError(t.t, err) - - t.Logf("Charlie's assets: %v", toProtoJSON(t.t, charlieAssets)) - t.Logf("Charlie's transfers: %v", toProtoJSON(t.t, charlieTransfers)) - - _, err = charlieTap.SendAsset(ctx, &taprpc.SendAssetRequest{ - TapAddrs: []string{charlieAddr2.Encoded}, - }) - require.NoError(t.t, err) - mineBlocks(t, net, 1, 1) - - itest.AssertNonInteractiveRecvComplete(t.t, fabiaTap, 2) - - _, err = daveTap.SendAsset(ctx, &taprpc.SendAssetRequest{ - TapAddrs: []string{daveAddr1.Encoded}, - }) - require.NoError(t.t, err) - mineBlocks(t, net, 1, 1) - - itest.AssertNonInteractiveRecvComplete(t.t, fabiaTap, 3) - - _, err = daveTap.SendAsset(ctx, &taprpc.SendAssetRequest{ - TapAddrs: []string{daveAddr2.Encoded}, - }) - require.NoError(t.t, err) - mineBlocks(t, net, 1, 1) - - itest.AssertNonInteractiveRecvComplete(t.t, fabiaTap, 4) - - // Fabia's balance should now be the sum of Charlie's and Dave's - // balances. - fabiaExpectedBalance := uint64(50_002) - assertSpendableBalance( - t.t, fabiaTap, nil, groupKey, fabiaExpectedBalance, - ) -} - -// testCustomChannelsForceClose tests a force close scenario after both parties -// have an active asset balance. -func testCustomChannelsForceClose(ctx context.Context, net *NetworkHarness, - t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // Explicitly set the proof courier as Zane (now has no other role - // other than proof shuffling), otherwise a hashmail courier will be - // used. For the funding transaction, we're just posting it and don't - // expect a true receiver. - zane, err := net.NewNode( - t.t, "Zane", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - // For our litd args, make sure that they all seen Zane as the main - // Universe server. - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), - )) - - // For this simple test, we'll just have Carol -> Dave as an assets - // channel. - charlie, err := net.NewNode( - t.t, "Charlie", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - - // Next we'll connect all the nodes and also fund them with some coins. - nodes := []*HarnessNode{charlie, dave} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - universeTap := newTapClient(t.t, zane) - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - - // Now we'll make an asset for Charlie that we'll use in the test to - // open a channel. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - - t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, charlieTap, dave) - t.Logf("Universes synced between all nodes, distributing assets...") - - // Before we actually create the asset channel, we want to make sure - // that failed attempts of creating a channel (e.g. due to insufficient - // on-chain funds) are cleaned up properly on the recipient side. - // We do this by sending all of Charlie's coins to a burn address then - // just sending him 50k sats, which isn't enough to fund a channel. - _, err = charlie.LightningClient.SendCoins( - ctx, &lnrpc.SendCoinsRequest{ - Addr: burnAddr, - SendAll: true, - MinConfs: 0, - SpendUnconfirmed: true, - }, - ) - require.NoError(t.t, err) - net.SendCoins(t.t, 50_000, charlie) - - // The attempt should fail. But the recipient should receive the error, - // clean up the state and allow Charlie to try again after acquiring - // more funds. - _, err = charlieTap.FundChannel(ctx, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetID, - PeerPubkey: dave.PubKey[:], - FeeRateSatPerVbyte: 5, - }) - require.ErrorContains(t.t, err, "not enough witness outputs to create") - - // Now we'll fund the channel with the correct amount. - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, charlie) - - // Next we can open an asset channel from Charlie -> Dave, then kick - // off the main scenario. - t.Logf("Opening asset channels...") - assetFundResp, err := charlieTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetID, - PeerPubkey: dave.PubKey[:], - FeeRateSatPerVbyte: 5, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded channel between Charlie and Dave: %v", assetFundResp) - - // With the channel open, mine a block to confirm it. - mineBlocks(t, net, 6, 1) - - // A transfer for the funding transaction should be found in Charlie's - // DB. - fundingTxid, err := chainhash.NewHashFromStr(assetFundResp.Txid) - require.NoError(t.t, err) - assetFundingTransfer := locateAssetTransfers( - t.t, charlieTap, *fundingTxid, - ) - - t.Logf("Channel funding transfer: %v", - toProtoJSON(t.t, assetFundingTransfer)) - - // Charlie's balance should reflect that the funding asset is now - // excluded from balance reporting by tapd. - itest.AssertBalances( - t.t, charlieTap, itestAsset.Amount-fundingAmount, - itest.WithAssetID(assetID), - ) - - // Make sure that Charlie properly uploaded funding proof to the - // Universe server. - fundingScriptTree := tapscript.NewChannelFundingScriptTree() - fundingScriptKey := fundingScriptTree.TaprootKey - fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() - assertUniverseProofExists( - t.t, universeTap, assetID, nil, fundingScriptTreeBytes, - fmt.Sprintf( - "%v:%v", assetFundResp.Txid, assetFundResp.OutputIndex, - ), - ) - - // Make sure the channel shows the correct asset information. - assertAssetChan( - t.t, charlie, dave, fundingAmount, []*taprpc.Asset{cents}, - ) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) - - // We'll also have dave sync with Charlie+Zane to ensure he has the - // proof for the funding output. We sync the transfers as well so he - // has all the proofs needed. - mode := universerpc.UniverseSyncMode_SYNC_FULL - diff, err := daveTap.SyncUniverse(ctx, &universerpc.SyncRequest{ - UniverseHost: zane.Cfg.LitAddr(), - SyncMode: mode, - }) - require.NoError(t.t, err) - - t.Logf("Synced Dave w/ Zane, universe_diff=%v", toProtoJSON(t.t, diff)) - - // With the channel confirmed, we'll push over some keysend payments - // from Carol to Dave. We'll send over a bit more BTC each time so Dave - // will go to chain sweep his output (default fee rate is 50 sat/vb). - const ( - numPayments = 5 - keySendAmount = 100 - btcAmt = int64(5_000) - ) - for i := 0; i < numPayments; i++ { - sendAssetKeySendPayment( - t.t, charlie, dave, keySendAmount, assetID, - fn.Some(btcAmt), - ) - } - - logBalance(t.t, nodes, assetID, "after keysend") - - // With the payments sent, we'll now go on chain with a force close - // from Carol. - t.Logf("Force closing channel...") - charlieChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(assetFundResp.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: assetFundResp.Txid, - }, - } - _, closeTxid, err := net.CloseChannel(charlie, charlieChanPoint, true) - require.NoError(t.t, err) - - t.Logf("Channel closed! Mining blocks, close_txid=%v", closeTxid) - - // Next, we'll mine a block to confirm the force close. - mineBlocks(t, net, 1, 1) - - // At this point, we should have the force close transaction in the set - // of transfers for both nodes. - forceCloseTransfer := findForceCloseTransfer( - t.t, charlieTap, daveTap, closeTxid, - ) - t.Logf("Force close transfer: %v", toProtoJSON(t.t, forceCloseTransfer)) - - // Now that we have the transfer on disk, we'll also assert that the - // universe also has proof for both the relevant transfer outputs. - for _, transfer := range forceCloseTransfer.Transfers { - for _, transferOut := range transfer.Outputs { - assertUniverseProofExists( - t.t, universeTap, assetID, nil, - transferOut.ScriptKey, - transferOut.Anchor.Outpoint, - ) - } - } - - t.Logf("Universe proofs located!") - - // We should also have a new sweep transaction in the mempool. - daveSweepTxid, err := waitForNTxsInMempool( - net.Miner.Client, 1, shortTimeout, - ) - require.NoError(t.t, err) - - t.Logf("Dave sweep txid: %v", daveSweepTxid) - - // Next, we'll mine a block to confirm Dave's sweep transaction. - // This'll sweep his non-delay commitment output. - mineBlocks(t, net, 1, 1) - - // At this point, a transfer should have been created for Dave's sweep - // transaction. - daveSweepTransfer := locateAssetTransfers( - t.t, daveTap, *daveSweepTxid[0], - ) - - t.Logf("Dave sweep transfer: %v", toProtoJSON(t.t, daveSweepTransfer)) - - time.Sleep(time.Second * 1) - - // Next, we'll mine three additional blocks to trigger the CSV delay - // for Charlie. - mineBlocks(t, net, 4, 0) - - // We expect that Charlie's sweep transaction has been broadcast. - charlieSweepTxid, err := waitForNTxsInMempool( - net.Miner.Client, 1, shortTimeout, - ) - require.NoError(t.t, err) - - t.Logf("Charlie sweep txid: %v", charlieSweepTxid) - - // Now we'll mine a block to confirm Charlie's sweep transaction. - mineBlocks(t, net, 1, 0) - - // Charlie should now have an asset transfer for his sweep transaction. - charlieSweepTransfer := locateAssetTransfers( - t.t, charlieTap, *charlieSweepTxid[0], - ) - - t.Logf("Charlie sweep transfer: %v", toProtoJSON( - t.t, charlieSweepTransfer, - )) - - // Both sides should now reflect their updated asset balances. - daveBalance := uint64(numPayments * keySendAmount) - charlieBalance := itestAsset.Amount - daveBalance - itest.AssertBalances( - t.t, daveTap, daveBalance, itest.WithAssetID(assetID), - itest.WithNumUtxos(1), - ) - itest.AssertBalances( - t.t, charlieTap, charlieBalance, itest.WithAssetID(assetID), - itest.WithNumUtxos(2), - ) - - // We'll make sure Dave can spend his asset UTXO by sending it all but - // one unit to Zane (the universe). - assetSendAmount := daveBalance - 1 - zaneAddr, err := universeTap.NewAddr(ctx, &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 from Dave units to Zane...", assetSendAmount) - - // Send the assets to Zane. We expect Dave to have 3 transfers: the - // funding txn, their force close sweep, and now this new send. - itest.AssertAddrCreated(t.t, universeTap, cents, zaneAddr) - sendResp, err := daveTap.SendAsset(ctx, &taprpc.SendAssetRequest{ - TapAddrs: []string{zaneAddr.Encoded}, - }) - require.NoError(t.t, err) - itest.ConfirmAndAssertOutboundTransfer( - t.t, t.lndHarness.Miner.Client, daveTap, sendResp, assetID, - []uint64{1, assetSendAmount}, 2, 3, - ) - itest.AssertNonInteractiveRecvComplete(t.t, universeTap, 1) - - // And now we also send all assets but one from Charlie to the universe - // to make sure the time lock sweep output can also be spent correctly. - assetSendAmount = charlieBalance - 1 - zaneAddr2, err := universeTap.NewAddr(ctx, &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 from Charlie units to Zane...", - assetSendAmount) - - itest.AssertAddrCreated(t.t, universeTap, cents, zaneAddr2) - sendResp2, err := charlieTap.SendAsset(ctx, &taprpc.SendAssetRequest{ - TapAddrs: []string{zaneAddr2.Encoded}, - }) - require.NoError(t.t, err) - itest.ConfirmAndAssertOutboundTransfer( - t.t, t.lndHarness.Miner.Client, charlieTap, sendResp2, assetID, - []uint64{1, assetSendAmount}, 3, 4, - ) - itest.AssertNonInteractiveRecvComplete(t.t, universeTap, 2) -} - -// testCustomChannelsBreach tests a force close scenario that breaches an old -// state, after both parties have an active asset balance. -func testCustomChannelsBreach(ctx context.Context, net *NetworkHarness, - t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - // Charlie will be the breached party. We set --nolisten to ensure Dave - // won't be able to connect to him and trigger the channel protection - // logic automatically. We also can't have Charlie automatically - // reconnect too early, otherwise DLP would be initiated instead of the - // breach we want to provoke. - charlieFlags := append( - slices.Clone(lndArgs), "--nolisten", "--minbackoff=1h", - ) - - // For this simple test, we'll just have Carol -> Dave as an assets - // channel. - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", charlieFlags, false, true, charliePort, - litdArgs..., - ) - require.NoError(t.t, err) - - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - - // Next we'll connect all the nodes and also fund them with some coins. - nodes := []*HarnessNode{charlie, dave} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - universeTap := newTapClient(t.t, charlie) - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - - // Now we'll make an asset for Charlie that we'll use in the test to - // open a channel. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - - t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, charlieTap, dave) - t.Logf("Universes synced between all nodes, distributing assets...") - - // Next we can open an asset channel from Charlie -> Dave, then kick - // off the main scenario. - t.Logf("Opening asset channels...") - assetFundResp, err := charlieTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetID, - PeerPubkey: dave.PubKey[:], - FeeRateSatPerVbyte: 5, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded channel between Charlie and Dave: %v", assetFundResp) - - // With the channel open, mine a block to confirm it. - mineBlocks(t, net, 6, 1) - - // A transfer for the funding transaction should be found in Charlie's - // DB. - fundingTxid, err := chainhash.NewHashFromStr(assetFundResp.Txid) - require.NoError(t.t, err) - assetFundingTransfer := locateAssetTransfers( - t.t, charlieTap, *fundingTxid, - ) - - t.Logf("Channel funding transfer: %v", - toProtoJSON(t.t, assetFundingTransfer)) - - // Charlie's balance should reflect that the funding asset is now - // excluded from balance reporting by tapd. - itest.AssertBalances( - t.t, charlieTap, itestAsset.Amount-fundingAmount, - itest.WithAssetID(assetID), itest.WithNumUtxos(1), - ) - - // Make sure that Charlie properly uploaded funding proof to the - // Universe server. - fundingScriptTree := tapscript.NewChannelFundingScriptTree() - fundingScriptKey := fundingScriptTree.TaprootKey - fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() - assertUniverseProofExists( - t.t, universeTap, assetID, nil, fundingScriptTreeBytes, - fmt.Sprintf( - "%v:%v", assetFundResp.Txid, assetFundResp.OutputIndex, - ), - ) - - // Make sure the channel shows the correct asset information. - assertAssetChan( - t.t, charlie, dave, fundingAmount, []*taprpc.Asset{cents}, - ) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) - - // Next, we'll make keysend payments from Charlie to Dave. we'll use - // this to reach a state where both parties have funds in the channel. - const ( - numPayments = 5 - keySendAmount = 100 - btcAmt = int64(5_000) - ) - for i := 0; i < numPayments; i++ { - sendAssetKeySendPayment( - t.t, charlie, dave, keySendAmount, assetID, - fn.Some(btcAmt), - ) - } - - logBalance(t.t, nodes, assetID, "after keysend -- breach state") - - // Now we'll create an on disk snapshot that we'll use to restore back - // to as our breached state. - require.NoError(t.t, net.StopAndBackupDB(dave)) - connectAllNodes(t.t, net, nodes) - - // We'll send one more keysend payment now to revoke the state we were - // just at above. - sendAssetKeySendPayment( - t.t, charlie, dave, keySendAmount, assetID, fn.Some(btcAmt), - ) - logBalance(t.t, nodes, assetID, "after keysend -- final state") - - // With the final state achieved, we'll now restore Dave (who will be - // force closing) to that old state, the breach state. - require.NoError(t.t, net.StopAndRestoreDB(dave)) - - // With Dave restored, we'll now execute the force close. - t.Logf("Force close by Dave to breach...") - daveChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(assetFundResp.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: assetFundResp.Txid, - }, - } - _, breachTxid, err := net.CloseChannel(dave, daveChanPoint, true) - require.NoError(t.t, err) - - t.Logf("Channel closed! Mining blocks, close_txid=%v", breachTxid) - - // Next, we'll mine a block to confirm the breach transaction. - mineBlocks(t, net, 1, 1) - - // We should be able to find the transfer of the breach for both - // parties. - charlieBreachTransfer := locateAssetTransfers( - t.t, charlieTap, *breachTxid, - ) - daveBreachTransfer := locateAssetTransfers( - t.t, daveTap, *breachTxid, - ) - - t.Logf("Charlie breach transfer: %v", - toProtoJSON(t.t, charlieBreachTransfer)) - t.Logf("Dave breach transfer: %v", - toProtoJSON(t.t, daveBreachTransfer)) - - // With the breach transaction mined, Charlie should now have a - // transaction in the mempool sweeping the *both* commitment outputs. - charlieJusticeTxid, err := waitForNTxsInMempool( - net.Miner.Client, 1, time.Second*5, - ) - require.NoError(t.t, err) - - t.Logf("Charlie justice txid: %v", charlieJusticeTxid) - - // Next, we'll mine a block to confirm Charlie's justice transaction. - mineBlocks(t, net, 1, 1) - - // Charlie should now have a transfer for his justice transaction. - charlieJusticeTransfer := locateAssetTransfers( - t.t, charlieTap, *charlieJusticeTxid[0], - ) - - t.Logf("Charlie justice transfer: %v", - toProtoJSON(t.t, charlieJusticeTransfer)) - - // Charlie's balance should now be the same as before the breach - // attempt: the amount he minted at the very start. - charlieBalance := itestAsset.Amount - itest.AssertBalances( - t.t, charlieTap, charlieBalance, itest.WithAssetID(assetID), - itest.WithNumUtxos(3), - ) - - t.Logf("Charlie balance after breach: %d", charlieBalance) -} - -// testCustomChannelsV1Upgrade tests the upgrade path of a taproot assets -// channel. It upgrades one of the peers to a version that utilizes feature bits -// and new features over the channel, testing that backwards compatibility is -// maintained along the way. We also introduce a channel breach, right at the -// point before we switched over to the new features, to test that sweeping is -// done properly. -func testCustomChannelsV1Upgrade(ctx context.Context, net *NetworkHarness, - t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - zane, err := net.NewNode( - t.t, "Zane", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), - )) - - davePort := port.NextAvailablePort() - daveFlags := append( - slices.Clone(lndArgs), "--nolisten", "--minbackoff=1h", - ) - - // For this simple test, we'll just have Charlie -> Dave as an assets - // channel. - dave, err := net.NewNodeWithPort( - t.t, "Dave", daveFlags, false, true, davePort, - litdArgs..., - ) - require.NoError(t.t, err) - - charlie, err := net.NewNode( - t.t, "Charlie", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - // Next we'll connect all the nodes and also fund them with some coins. - nodes := []*HarnessNode{dave, charlie} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - universeTap := newTapClient(t.t, zane) - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - - // Now we'll make an asset for Charlie that we'll use in the test to - // open a channel. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - - t.Logf("Minted %d itest asset cents, syncing universes...", - cents.Amount) - - syncUniverses(t.t, charlieTap, dave) - t.Logf("Universes synced between all nodes, distributing assets...") - - // Next we can open an asset channel from Charlie -> Dave, then kick - // off the main scenario. - t.Logf("Opening asset channels...") - assetFundResp, err := charlieTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetID, - PeerPubkey: dave.PubKey[:], - FeeRateSatPerVbyte: 5, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded channel between Charlie and Dave: %v", assetFundResp) - - // With the channel open, mine 6 blocks to confirm it. - mineBlocks(t, net, 6, 1) - - // A transfer for the funding transaction should be found in Charlie's - // DB. - fundingTxid, err := chainhash.NewHashFromStr(assetFundResp.Txid) - require.NoError(t.t, err) - assetFundingTransfer := locateAssetTransfers( - t.t, charlieTap, *fundingTxid, - ) - - t.Logf("Channel funding transfer: %v", - toProtoJSON(t.t, assetFundingTransfer)) - - // Charlie's balance should reflect that the funding asset is now - // excluded from balance reporting by tapd. - itest.AssertBalances( - t.t, charlieTap, itestAsset.Amount-fundingAmount, - itest.WithAssetID(assetID), itest.WithNumUtxos(1), - ) - - // Make sure that Charlie properly uploaded funding proof to the - // Universe server. - fundingScriptTree := tapscript.NewChannelFundingScriptTree() - fundingScriptKey := fundingScriptTree.TaprootKey - fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() - assertUniverseProofExists( - t.t, universeTap, assetID, nil, fundingScriptTreeBytes, - fmt.Sprintf( - "%v:%v", assetFundResp.Txid, assetFundResp.OutputIndex, - ), - ) - - // Make sure the channel shows the correct asset information. - assertAssetChan( - t.t, charlie, dave, fundingAmount, []*taprpc.Asset{cents}, - ) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) - - logBalance(t.t, nodes, assetID, "start") - - // Let's dispatch 5 asset & 5 keysend payments from Charlie to Dave. At - // this point Charlie is running the old version of LiT. - for range 5 { - sendAssetKeySendPayment( - t.t, charlie, dave, 50, assetID, fn.None[int64](), - ) - sendKeySendPayment(t.t, charlie, dave, 1_000) - } - - logBalance(t.t, nodes, assetID, "before upgrade") - - // Let's assert that Charlie & Dave actually run different versions of - // taproot-assets. We expect Dave to be running the latest version, - // while Charlie is running an older version (v0.15.0). - daveInfo, err := daveTap.GetInfo(ctx, &taprpc.GetInfoRequest{}) - require.NoError(t.t, err) - - charlieInfo, err := charlieTap.GetInfo(ctx, &taprpc.GetInfoRequest{}) - require.NoError(t.t, err) - - require.NotEqual(t.t, daveInfo.Version, charlieInfo.Version) - - res, err := charlie.ChannelBalance(ctx, &lnrpc.ChannelBalanceRequest{}) - require.NoError(t.t, err) - - charlieSatsBefore := res.LocalBalance - - // Now we'll restart Charlie and assert that he upgraded. We also back - // up the DB at this point, in order to induce a breach later right at - // the switching point before upgrading the channel. We will verify that - // the breach transaction will be swept by the right party. - require.NoError(t.t, net.StopAndBackupDB(charlie, WithUpgrade())) - connectAllNodes(t.t, net, nodes) - - charlieInfo, err = charlieTap.GetInfo(ctx, &taprpc.GetInfoRequest{}) - require.NoError(t.t, err) - - // Dave and Charlie should both be running the same version (latest). - require.Equal(t.t, daveInfo.Version, charlieInfo.Version) - - // Let's send another 5 asset and keysend payments from Charlie to Dave. - // Charlie is now on the latest version of LiT and the channel upgraded. - for range 5 { - sendAssetKeySendPayment( - t.t, charlie, dave, 50, assetID, fn.None[int64](), - ) - } - - res, err = charlie.ChannelBalance(ctx, &lnrpc.ChannelBalanceRequest{}) - require.NoError(t.t, err) - - charlieSatsAfter := res.LocalBalance - - // Because of no-op HTLCs, the satoshi balance of Charlie should not - // have shifted while sending the asset payments. - require.Equal(t.t, charlieSatsBefore, charlieSatsAfter) - - logBalance(t.t, nodes, assetID, "after upgrade") - - // Now let's restart Charlie and restore the DB to the previous snapshot - // which corresponds to a previous (invalid) and unupgraded channel - // state. - require.NoError(t.t, net.StopAndRestoreDB(charlie)) - - // With Charlie restored, we'll now execute the force close. - t.Logf("Force close by Charlie to breach...") - charlieChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(assetFundResp.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: assetFundResp.Txid, - }, - } - _, breachTxid, err := net.CloseChannel(charlie, charlieChanPoint, true) - require.NoError(t.t, err) - - t.Logf("Channel closed! Mining blocks, close_txid=%v", breachTxid) - - // Next, we'll mine a block to confirm the breach transaction. - mineBlocks(t, net, 1, 1) - - // We should be able to find the transfer of the breach for both - // parties. - charlieBreachTransfer := locateAssetTransfers( - t.t, charlieTap, *breachTxid, - ) - daveBreachTransfer := locateAssetTransfers( - t.t, daveTap, *breachTxid, - ) - - t.Logf("Charlie breach transfer: %v", - toProtoJSON(t.t, charlieBreachTransfer)) - t.Logf("Dave breach transfer: %v", - toProtoJSON(t.t, daveBreachTransfer)) - - require.Len(t.t, charlieBreachTransfer.Outputs, 2) - assetOutput := charlieBreachTransfer.Outputs[0] - assertUniverseProofExists( - t.t, universeTap, assetID, nil, assetOutput.ScriptKey, - assetOutput.Anchor.Outpoint, - ) - - op, err := wire.NewOutPointFromString(assetOutput.Anchor.Outpoint) - require.NoError(t.t, err) - - // We'll manually export the proof of the breach transfer, in order to - // verify that it indeed did not use STXO proofs. - proofResp, err := daveTap.ExportProof(ctx, &taprpc.ExportProofRequest{ - AssetId: assetID, - ScriptKey: assetOutput.ScriptKey, - Outpoint: &taprpc.OutPoint{ - Txid: op.Hash[:], - OutputIndex: op.Index, - }, - }) - require.NoError(t.t, err) - - proofFile, err := proof.DecodeFile(proofResp.RawProofFile) - require.NoError(t.t, err) - require.Equal(t.t, proofFile.NumProofs(), 3) - latestProof, err := proofFile.LastProof() - require.NoError(t.t, err) - - // This proof should not contain the STXO exclusion proofs, since the - // breach occured right before the channel upgraded. - stxoProofs := latestProof.ExclusionProofs[0].CommitmentProof.STXOProofs - require.Nil(t.t, stxoProofs) - - // With the breach transaction mined, Dave should now have a transaction - // in the mempool sweeping *both* commitment outputs. - daveJusticeTxid, err := waitForNTxsInMempool( - net.Miner.Client, 1, time.Second*5, - ) - require.NoError(t.t, err) - - t.Logf("Dave justice txid: %v", daveJusticeTxid) - - // Next, we'll mine a block to confirm Dave's justice transaction. - mineBlocks(t, net, 1, 1) - - // Dave should now have a transfer for his justice transaction. - daveJusticeTransfer := locateAssetTransfers( - t.t, daveTap, *daveJusticeTxid[0], - ) - - t.Logf("Dave justice transfer: %v", - toProtoJSON(t.t, daveJusticeTransfer)) - - // Dave should claim all of the asset balance that was put into the - // channel. - daveBalance := uint64(fundingAmount) - - itest.AssertBalances( - t.t, daveTap, daveBalance, itest.WithAssetID(assetID), - itest.WithNumUtxos(2), - ) - - t.Logf("Dave balance after breach: %d", daveBalance) - - require.Len(t.t, daveJusticeTransfer.Outputs, 2) - assetOutput = daveJusticeTransfer.Outputs[0] - op, err = wire.NewOutPointFromString(assetOutput.Anchor.Outpoint) - require.NoError(t.t, err) - - // We'll now also export the proof for the justice transaction. Here we - // expect to find STXO proofs, as the sweeping party is an upgraded node - // that supports it. - proofResp, err = daveTap.ExportProof(ctx, &taprpc.ExportProofRequest{ - AssetId: assetID, - ScriptKey: assetOutput.ScriptKey, - Outpoint: &taprpc.OutPoint{ - Txid: op.Hash[:], - OutputIndex: op.Index, - }, - }) - require.NoError(t.t, err) - - proofFile, err = proof.DecodeFile(proofResp.RawProofFile) - require.NoError(t.t, err) - require.Equal(t.t, 4, proofFile.NumProofs()) - latestProof, err = proofFile.LastProof() - require.NoError(t.t, err) - - // This proof should contain the STXO exclusion proofs - stxoProofs = latestProof.InclusionProof.CommitmentProof.STXOProofs - require.NotNil(t.t, stxoProofs) -} - -// testCustomChannelsLiquidityEdgeCasesCore is the core logic of the liquidity -// edge cases. This test goes through certain scenarios that expose edge cases -// and behaviors that proved to be buggy in the past and have been directly -// addressed. It accepts an extra parameter which dictates whether it should use -// group keys or asset IDs. -func testCustomChannelsLiquidityEdgeCasesCore(ctx context.Context, - net *NetworkHarness, t *harnessTest, groupMode bool) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - // The topology we are going for looks like the following: - // - // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia - // | - // | - // [assets] - // | - // v - // Yara - // - // With [assets] being a custom channel and [sats] being a normal, BTC - // only channel. - // All 5 nodes need to be full litd nodes running in integrated mode - // with tapd included. We also need specific flags to be enabled, so we - // create 5 completely new nodes, ignoring the two default nodes that - // are created by the harness. - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - fabia, err := net.NewNode( - t.t, "Fabia", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - yara, err := net.NewNode( - t.t, "Yara", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - // Create the normal channel between Dave and Erin. We don't clean up - // this channel because we expect there to be in-flight HTLCs due to - // some of the edge cases we're testing. Waiting for those HTLCs to time - // out would take too long. - t.Logf("Opening normal channel between Dave and Erin...") - channelOp := openChannelAndAssert( - t, net, dave, erin, lntest.OpenChannelParams{ - Amt: 10_000_000, - SatPerVByte: 5, - }, - ) - - // This is the only public channel, we need everyone to be aware of it. - assertChannelKnown(t.t, charlie, channelOp) - assertChannelKnown(t.t, fabia, channelOp) - - universeTap := newTapClient(t.t, charlie) - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - erinTap := newTapClient(t.t, erin) - fabiaTap := newTapClient(t.t, fabia) - yaraTap := newTapClient(t.t, yara) - - assetReq := itest.CopyRequest(&mintrpc.MintAssetRequest{ - Asset: itestAsset, - }) - - // In order to use group keys in this test, the asset must belong to a - // group. - if groupMode { - assetReq.Asset.NewGroupedAsset = true - } - - // Mint an asset on Charlie and sync all nodes to Charlie as the - // universe. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{assetReq}, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - - // If groupMode is enabled, treat the asset as part of a group by - // assigning its tweaked group key. Otherwise, treat it as an ungrouped - // asset using only its asset ID. - var ( - groupID []byte - groupKey *btcec.PublicKey - ) - if groupMode { - groupID = cents.GetAssetGroup().GetTweakedGroupKey() - - groupKey, err = btcec.ParsePubKey(groupID) - require.NoError(t.t, err) - } - - t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) - t.Logf("Universes synced between all nodes, distributing assets...") - - const ( - daveFundingAmount = uint64(400_000) - erinFundingAmount = uint64(200_000) - ) - charlieFundingAmount := cents.Amount - uint64(2*400_000) - - _, _, chanPointEF := createTestAssetNetwork( - t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, - universeTap, cents, 400_000, charlieFundingAmount, - daveFundingAmount, erinFundingAmount, 0, - ) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) - - logBalance(t.t, nodes, assetID, "initial") - - // Edge case: We send a single satoshi keysend payment from Dave to - // Fabia. Which will make it so that Fabia's balance in the channel - // between Erin and her is 1 satoshi, which is below the dust limit. - // This is only allowed while Fabia doesn't have any assets on her side - // yet. - erinFabiaChan := fetchChannel(t.t, fabia, chanPointEF) - hinEF := &lnrpc.HopHint{ - NodeId: erin.PubKeyStr, - ChanId: erinFabiaChan.PeerScidAlias, - CltvExpiryDelta: 80, - FeeBaseMsat: 1000, - FeeProportionalMillionths: 1, - } - sendKeySendPayment( - t.t, dave, fabia, 1, withPayRouteHints([]*lnrpc.RouteHint{{ - HopHints: []*lnrpc.HopHint{hinEF}, - }}), - ) - logBalance(t.t, nodes, assetID, "after single sat keysend") - - // We make sure that a single sat keysend payment is not allowed when - // it carries assets. - sendAssetKeySendPayment( - t.t, erin, fabia, 123, assetID, fn.Some[int64](1), - withPayErrSubStr( - fmt.Sprintf("keysend payment satoshi amount must be "+ - "greater than or equal to %d satoshis", - rfqmath.DefaultOnChainHtlcSat), - ), - ) - - // Normal case. - // Send 50 assets from Charlie to Dave. - sendAssetKeySendPayment( - t.t, charlie, dave, 50, assetID, fn.None[int64](), - withGroupKey(groupID), - ) - - logBalance(t.t, nodes, assetID, "after 50 assets") - - // Normal case. - // Send 1k sats from Charlie to Dave. - sendKeySendPayment(t.t, charlie, dave, 1000) - - logBalance(t.t, nodes, assetID, "after 1k sats") - - // Edge case: The channel reserve check should trigger, and we should - // get a payment failure, not a timeout. - // - // Now Dave tries to send 50 assets to Charlie. There shouldn't be - // enough sats in the channel. - // - // Assume an acceptable completion window which is half the payment - // timeout. If the payment succeeds within this duration this means we - // didn't fall into a routing loop. - timeoutChan := time.After(PaymentTimeout / 2) - done := make(chan bool, 1) - - go func() { - sendAssetKeySendPayment( - t.t, dave, charlie, 50, assetID, fn.None[int64](), - withFailure(lnrpc.Payment_FAILED, failureNoRoute), - withGroupKey(groupID), - ) - - done <- true - }() - - select { - case <-done: - case <-timeoutChan: - t.Fatalf("Payment didn't fail within expected time duration") - } - - logBalance(t.t, nodes, assetID, "after failed 50 assets") - - // Send 10k sats from Charlie to Dave. - sendKeySendPayment(t.t, charlie, dave, 10000) - - logBalance(t.t, nodes, assetID, "10k sats") - - // Now Dave tries to send 50 assets again, this time he should have - // enough sats. - sendAssetKeySendPayment( - t.t, dave, charlie, 50, assetID, fn.None[int64](), - withGroupKey(groupID), - ) - - logBalance(t.t, nodes, assetID, "after 50 sats backwards") - - // Edge case: This refers to a bug where an asset allocation would be - // expected for this HTLC. This is a dust HTLC and it can not carry - // assets. - // - // Send 1 sat from Charlie to Dave. - sendKeySendPayment(t.t, charlie, dave, 1) - - logBalance(t.t, nodes, assetID, "after 1 sat") - - // Pay a normal bolt11 invoice involving RFQ flow. - _ = createAndPayNormalInvoice( - t.t, charlie, dave, erin, 20_000, assetID, withSmallShards(), - withGroupKey(groupID), - ) - - logBalance(t.t, nodes, assetID, "after 20k sat asset payment") - - // Edge case: There was a bug when paying an asset invoice that would - // evaluate to more than the channel capacity, causing a payment failure - // even though enough asset balance exists. - // - // Pay a bolt11 invoice with assets, which evaluates to more than the - // channel btc capacity. - _ = createAndPayNormalInvoice( - t.t, charlie, dave, erin, 1_000_000, assetID, withSmallShards(), - withGroupKey(groupID), - ) - - logBalance(t.t, nodes, assetID, "after big asset payment (btc "+ - "invoice, multi-hop)") - - // Edge case: Big asset invoice paid by direct peer with assets. - const bigAssetAmount = 100_000 - - invoiceResp := createAssetInvoice( - t.t, charlie, dave, bigAssetAmount, assetID, - withInvGroupKey(groupID), - ) - - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, - withGroupKey(groupID), - ) - - logBalance(t.t, nodes, assetID, "after big asset payment (asset "+ - "invoice, direct)") - - var groupBytes []byte - if groupMode { - groupBytes = schnorr.SerializePubKey(groupKey) - } - - // Make sure the invoice on the receiver side and the payment on the - // sender side show the individual HTLCs that arrived for it and that - // they show the correct asset amounts when decoded. - assertInvoiceHtlcAssets( - t.t, dave, invoiceResp, assetID, groupBytes, bigAssetAmount, - ) - assertPaymentHtlcAssets( - t.t, charlie, invoiceResp.RHash, assetID, groupBytes, - bigAssetAmount, - ) - - // Dave sends 200k assets and 5k sats to Yara. - sendAssetKeySendPayment( - t.t, dave, yara, 2*bigAssetAmount, assetID, fn.None[int64](), - withGroupKey(groupID), - ) - sendKeySendPayment(t.t, dave, yara, 5_000) - - logBalance(t.t, nodes, assetID, "after 200k assets to Yara") - - // Edge case: Now Charlie creates a big asset invoice to be paid for by - // Yara with assets. This is a multi-hop payment going over 2 asset - // channels, where the total asset value exceeds the btc capacity of the - // channels. - invoiceResp = createAssetInvoice( - t.t, dave, charlie, bigAssetAmount, assetID, - withInvGroupKey(groupID), - ) - - payInvoiceWithAssets( - t.t, yara, dave, invoiceResp.PaymentRequest, assetID, - withGroupKey(groupID), - ) - - logBalance(t.t, nodes, assetID, "after big asset payment (asset "+ - "invoice, multi-hop)") - - // Edge case: Now Charlie creates a tiny asset invoice to be paid for by - // Yara with satoshi. This is a multi-hop payment going over 2 asset - // channels, where the total asset value is less than the default anchor - // amount of 354 sats. - createAssetInvoice( - t.t, dave, charlie, 1, assetID, withInvoiceErrSubStr( - "could not create any quotes for the invoice", - ), - withInvGroupKey(groupID), - ) - - logBalance(t.t, nodes, assetID, "after small payment (asset "+ - "invoice, <354sats)") - - // Edge case: We now create a small BTC invoice on Erin and ask Charlie - // to pay it with assets. We should get a payment failure as the amount - // is too small to be paid with assets economically. But a payment is - // still possible, since the amount is large enough to represent a - // single unit (17.1 sat per unit). - btcInvoiceResp, err := erin.AddInvoice(ctx, &lnrpc.Invoice{ - Memo: "small BTC invoice", - ValueMsat: 18_000, - }) - require.NoError(t.t, err) - - payInvoiceWithAssets( - t.t, charlie, dave, btcInvoiceResp.PaymentRequest, assetID, - withFeeLimit(2_000), withGroupKey(groupID), withPayErrSubStr( - "failed to acquire any quotes", - ), - ) - - // When we override the uneconomical payment, it should succeed. - payInvoiceWithAssets( - t.t, charlie, dave, btcInvoiceResp.PaymentRequest, assetID, - withFeeLimit(2_000), withAllowOverpay(), - withGroupKey(groupID), - ) - logBalance( - t.t, nodes, assetID, "after small payment (BTC invoice 1 sat)", - ) - - // When we try to pay an invoice amount that's smaller than the - // corresponding value of a single asset unit, the payment will always - // be rejected, even if we set the allow_uneconomical flag. - btcInvoiceResp, err = erin.AddInvoice(ctx, &lnrpc.Invoice{ - Memo: "very small BTC invoice", - ValueMsat: 1_000, - }) - require.NoError(t.t, err) - - payInvoiceWithAssets( - t.t, charlie, dave, btcInvoiceResp.PaymentRequest, assetID, - withFeeLimit(1_000), withAllowOverpay(), withPayErrSubStr( - "failed to acquire any quotes", - ), withGroupKey(groupID), - ) - - // Edge case: Check if the RFQ HTLC tracking accounts for cancelled - // HTLCs. We achieve this by manually creating & using an RFQ quote with - // a set max amount. We first pay to a hodl invoice that we eventually - // cancel, then pay to a normal invoice which should succeed. - - // We start by sloshing some funds in the Erin<->Fabia. - sendAssetKeySendPayment( - t.t, erin, fabia, 100_000, assetID, fn.Some[int64](20_000), - withGroupKey(groupID), - ) - - logBalance(t.t, nodes, assetID, "balance after 1st slosh") - - // If we are running this test in group mode, then the manual rfq - // negotiation needs to also happen on the group key. - var assetSpecifier rfqrpc.AssetSpecifier - if groupMode { - assetSpecifier = rfqrpc.AssetSpecifier{ - Id: &rfqrpc.AssetSpecifier_GroupKey{ - GroupKey: groupID, - }, - } - } else { - assetSpecifier = rfqrpc.AssetSpecifier{ - Id: &rfqrpc.AssetSpecifier_AssetId{ - AssetId: assetID, - }, - } - } - - // We create the RFQ order. We set the max amt to ~180k sats which is - // going to evaluate to about 10k assets. - inOneHour := time.Now().Add(time.Hour) - resQ, err := charlieTap.RfqClient.AddAssetSellOrder( - ctx, &rfqrpc.AddAssetSellOrderRequest{ - AssetSpecifier: &assetSpecifier, - PaymentMaxAmt: 180_000_000, - Expiry: uint64(inOneHour.Unix()), - PeerPubKey: dave.PubKey[:], - TimeoutSeconds: 100, - }, - ) - require.NoError(t.t, err) - - // We now create a hodl invoice on Fabia, for 10k assets. - hodlInv := createAssetHodlInvoice( - t.t, erin, fabia, 10_000, assetID, - withInvGroupKey(groupID), - ) - - // Charlie tries to pay via Dave, by providing the RFQ quote ID that was - // manually created above. - var quoteID rfqmsg.ID - copy(quoteID[:], resQ.GetAcceptedQuote().Id) - - payInvoiceWithAssets( - t.t, charlie, dave, hodlInv.payReq, assetID, withSmallShards(), - withFailure(lnrpc.Payment_IN_FLIGHT, failureNone), - withRFQ(quoteID), withGroupKey(groupID), - ) - - // We now assert that the expected numbers of HTLCs are present on each - // node. - // Reminder, topology looks like this: - // - // Charlie <-> Dave <-> Erin <-> Fabia - // - // Therefore the routing nodes should have double the number of HTLCs - // required for the payment present. - assertNumHtlcs(t.t, charlie, 3) - assertNumHtlcs(t.t, dave, 6) - assertNumHtlcs(t.t, erin, 6) - assertNumHtlcs(t.t, fabia, 3) - - // Now let's cancel the invoice on Fabia. - payHash := hodlInv.preimage.Hash() - _, err = fabia.InvoicesClient.CancelInvoice( - ctx, &invoicesrpc.CancelInvoiceMsg{ - PaymentHash: payHash[:], - }, - ) - require.NoError(t.t, err) - - // There should be no HTLCs present on any channel. - assertNumHtlcs(t.t, charlie, 0) - assertNumHtlcs(t.t, dave, 0) - assertNumHtlcs(t.t, erin, 0) - assertNumHtlcs(t.t, fabia, 0) - - // Now Fabia creates another invoice. We also use a fixed msat value for - // the invoice. Since our itest oracle evaluates every asset to about - // 17.1 sats, this invoice should be a bit below 10k assets, so roughly - // the same volume as the previous invoice we just cancelled. - invoiceResp = createAssetInvoice( - t.t, erin, fabia, 0, assetID, withInvGroupKey(groupID), - withMsatAmount(170_000_000), - ) - - // Now Charlie pays the invoice, again by using the manually specified - // RFQ quote ID. This payment should succeed. - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, - withSmallShards(), withRFQ(quoteID), - withGroupKey(groupID), - ) - - logBalance(t.t, nodes, assetID, "after manual rfq hodl") - - // Edge case: Charlie negotiates a quote with Dave which has a low max - // amount (~170k sats). Then Charlie creates an invoice with a total - // amount slightly larger than the max allowed in the quote (200k sats). - // Erin will try to pay that invoice with sats, in shards of max size - // 80k sats. Dave will eventually stop forwarding HTLCs as the RFQ HTLC - // tracking mechanism should stop them from being forwarded, as they - // violate the maximum allowed amount of the quote. - - // Charlie starts by negotiating the quote. - inOneHour = time.Now().Add(time.Hour) - res, err := charlieTap.RfqClient.AddAssetBuyOrder( - ctx, &rfqrpc.AddAssetBuyOrderRequest{ - AssetSpecifier: &assetSpecifier, - AssetMaxAmt: 10_000, - Expiry: uint64(inOneHour.Unix()), - PeerPubKey: dave.PubKey[:], - TimeoutSeconds: 10, - }, - ) - require.NoError(t.t, err) - - type acceptedQuote = *rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote - quote, ok := res.Response.(acceptedQuote) - require.True(t.t, ok) - - // We now manually add the invoice in order to inject the above, - // manually generated, quote. - hint := &lnrpc.HopHint{ - NodeId: dave.PubKeyStr, - ChanId: quote.AcceptedQuote.Scid, - CltvExpiryDelta: 80, - FeeBaseMsat: 1000, - FeeProportionalMillionths: 1, - } - var preimage lntypes.Preimage - _, _ = rand.Read(preimage[:]) - payHash = preimage.Hash() - iResp, err := charlie.AddHoldInvoice( - ctx, &invoicesrpc.AddHoldInvoiceRequest{ - Memo: "", - Value: 200_000, - Hash: payHash[:], - RouteHints: []*lnrpc.RouteHint{{ - HopHints: []*lnrpc.HopHint{hint}, - }}, - }, - ) - require.NoError(t.t, err) - - htlcStream, err := dave.RouterClient.SubscribeHtlcEvents( - ctx, &routerrpc.SubscribeHtlcEventsRequest{}, - ) - require.NoError(t.t, err) - - // Now Erin tries to pay the invoice. Since rfq quote cannot satisfy the - // total amount of the invoice this payment will fail. - payPayReqWithSatoshi( - t.t, erin, iResp.PaymentRequest, - withFailure(lnrpc.Payment_IN_FLIGHT, failureNone), - withGroupKey(groupID), withMaxShards(4), - ) - - t.Logf("Asserting number of HTLCs on each node...") - assertMinNumHtlcs(t.t, dave, 2) - - t.Logf("Asserting HTLC events on Dave...") - assertHtlcEvents( - t.t, htlcStream, withNumEvents(1), withForwardFailure(), - ) - - _, err = charlie.InvoicesClient.CancelInvoice( - ctx, &invoicesrpc.CancelInvoiceMsg{ - PaymentHash: payHash[:], - }, - ) - require.NoError(t.t, err) - - assertNumHtlcs(t.t, dave, 0) - - logBalance(t.t, nodes, assetID, "after small manual rfq") - - _ = htlcStream.CloseSend() - _, _ = erin.RouterClient.ResetMissionControl( - context.Background(), &routerrpc.ResetMissionControlRequest{}, - ) - - // Edge case: Fabia creates an invoice which Erin cannot satisfy with - // his side of asset liquidity. This tests that Erin will not try to - // add an HTLC with more asset units than what his local balance is. To - // validate that the channel is still healthy, we follow up with a - // smaller invoice payment which is meant to succeed. - - // We now create a hodl invoice on Fabia, for 125k assets. - hodlInv = createAssetHodlInvoice(t.t, erin, fabia, 125_000, assetID) - - htlcStream, err = erin.RouterClient.SubscribeHtlcEvents( - ctx, &routerrpc.SubscribeHtlcEventsRequest{}, - ) - require.NoError(t.t, err) - - // Charlie tries to pay, this is not meant to succeed, as Erin does not - // have enough assets to forward to Fabia. - payInvoiceWithAssets( - t.t, charlie, dave, hodlInv.payReq, assetID, - withFailure(lnrpc.Payment_IN_FLIGHT, failureNone), - ) - - // Let's check that at least 2 HTLCs were added on the Erin->Fabia link, - // which means that Erin would have an extra incoming HTLC for each - // outgoing one. So we expect a minimum of 4 HTLCs present on Erin. - assertMinNumHtlcs(t.t, erin, 4) - - // We also want to make sure that at least one failure occurred that - // hinted at the problem (not enough assets to forward). - assertHtlcEvents( - t.t, htlcStream, withNumEvents(1), - withLinkFailure(routerrpc.FailureDetail_INSUFFICIENT_BALANCE), - ) - - logBalance(t.t, nodes, assetID, "with min 4 present HTLCs") - - // Now Fabia cancels the invoice, this is meant to cancel back any - // locked in HTLCs and reset Erin's local balance back to its original - // value. - payHash = hodlInv.preimage.Hash() - _, err = fabia.InvoicesClient.CancelInvoice( - ctx, &invoicesrpc.CancelInvoiceMsg{ - PaymentHash: payHash[:], - }, - ) - require.NoError(t.t, err) - - // Let's assert that Erin cancelled all his HTLCs. - assertNumHtlcs(t.t, erin, 0) - - logBalance(t.t, nodes, assetID, "after hodl cancel & 0 present HTLCs") - - // Now let's create a smaller invoice and pay it, to validate that the - // channel is still healthy. - invoiceResp = createAssetInvoice(t.t, erin, fabia, 50_000, assetID) - - _, _ = charlie.RouterClient.ResetMissionControl( - context.Background(), &routerrpc.ResetMissionControlRequest{}, - ) - payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, - ) - - logBalance(t.t, nodes, assetID, "after safe asset htlc failure") - - // Another test case: Make sure an asset invoice contains the correct - // channel policy. We expect it to be the policy for the direction from - // edge node to receiver node. To test this, we first set two different - // policies on the channel between Erin and Fabia. - resp, err := erin.UpdateChannelPolicy(ctx, &lnrpc.PolicyUpdateRequest{ - Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ - ChanPoint: chanPointEF, - }, - BaseFeeMsat: 31337, - FeeRatePpm: 443322, - TimeLockDelta: 19, - }) - require.NoError(t.t, err) - require.Empty(t.t, resp.FailedUpdates) - - resp, err = fabia.UpdateChannelPolicy(ctx, &lnrpc.PolicyUpdateRequest{ - Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ - ChanPoint: chanPointEF, - }, - BaseFeeMsat: 42069, - FeeRatePpm: 223344, - TimeLockDelta: 18, - }) - require.NoError(t.t, err) - require.Empty(t.t, resp.FailedUpdates) - - // We now create an invoice on Fabia and expect Erin's policy to be used - // in the invoice. - invoiceResp = createAssetInvoice(t.t, erin, fabia, 1_000, assetID) - req, err := erin.DecodePayReq(ctx, &lnrpc.PayReqString{ - PayReq: invoiceResp.PaymentRequest, - }) - require.NoError(t.t, err) - - require.Len(t.t, req.RouteHints, 1) - require.Len(t.t, req.RouteHints[0].HopHints, 1) - invoiceHint := req.RouteHints[0].HopHints[0] - require.Equal(t.t, erin.PubKeyStr, invoiceHint.NodeId) - require.EqualValues(t.t, 31337, invoiceHint.FeeBaseMsat) - require.EqualValues(t.t, 443322, invoiceHint.FeeProportionalMillionths) - require.EqualValues(t.t, 19, invoiceHint.CltvExpiryDelta) - - // Now we pay the invoice and expect the same policy with very expensive - // fees to be used. - payInvoiceWithSatoshi( - t.t, dave, invoiceResp, withFeeLimit(100_000_000), - ) - - logBalance(t.t, nodes, assetID, "after policy checks") - - resBuy, err := daveTap.RfqClient.AddAssetBuyOrder( - ctx, &rfqrpc.AddAssetBuyOrderRequest{ - AssetSpecifier: &assetSpecifier, - AssetMaxAmt: 1_000, - Expiry: uint64(inOneHour.Unix()), - PeerPubKey: charlie.PubKey[:], - TimeoutSeconds: 100, - }, - ) - require.NoError(t.t, err) - - scid := resBuy.GetAcceptedQuote().Scid - - invResp := createAssetInvoice( - t.t, charlie, dave, 1_000, assetID, - withInvGroupKey(groupID), withRouteHints([]*lnrpc.RouteHint{ - { - HopHints: []*lnrpc.HopHint{ - { - NodeId: charlie.PubKeyStr, - ChanId: scid, - }, - }, - }, - }), - ) - - payInvoiceWithAssets( - t.t, charlie, dave, invResp.PaymentRequest, assetID, - withGroupKey(groupID), - ) - - logBalance(t.t, nodes, assetID, "after invoice with route hints") -} - -// testCustomChannelsLiquidityEdgeCases is a test that runs through some -// taproot asset channel liquidity related edge cases. -func testCustomChannelsLiquidityEdgeCases(ctx context.Context, - net *NetworkHarness, t *harnessTest) { - - // Run liquidity edge cases and only use single asset IDs for invoices - // and payments. - testCustomChannelsLiquidityEdgeCasesCore(ctx, net, t, false) -} - -// testCustomChannelsLiquidityEdgeCasesGroup is a test that runs through some -// taproot asset channel liquidity related edge cases using group keys. -func testCustomChannelsLiquidityEdgeCasesGroup(ctx context.Context, - net *NetworkHarness, t *harnessTest) { - - // Run liquidity edge cases and only use group keys for invoices and - // payments. - testCustomChannelsLiquidityEdgeCasesCore(ctx, net, t, true) -} - -// testCustomChannelsMultiRFQ tests that sending and receiving payments works -// when using the multi-rfq features of tapd. This means that liquidity across -// multiple channels and peers can be used to send out a payment, or receive to -// an invoice. -func testCustomChannelsMultiRFQ(ctx context.Context, net *NetworkHarness, - t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - litdArgsDiffOracle := slices.Clone(litdArgsTemplateDiffOracle) - - charlie, err := net.NewNode( - t.t, "Charlie", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, charlie.Cfg.LitAddr(), - )) - - litdArgsDiffOracle = append(litdArgsDiffOracle, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, charlie.Cfg.LitAddr(), - )) - - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - fabia, err := net.NewNode( - t.t, "Fabia", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - yara, err := net.NewNode( - t.t, "Yara", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - george, err := net.NewNode( - t.t, "George", lndArgs, false, true, litdArgsDiffOracle..., - ) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave, erin, fabia, yara, george} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - // Let's create the tap clients. - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - erinTap := newTapClient(t.t, erin) - fabiaTap := newTapClient(t.t, fabia) - yaraTap := newTapClient(t.t, yara) - georgeTap := newTapClient(t.t, george) - - assetReq := itest.CopyRequest(&mintrpc.MintAssetRequest{ - Asset: itestAsset, - }) - - assetReq.Asset.NewGroupedAsset = true - - // Mint an asset on Charlie and sync all nodes to Charlie as the - // universe. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{assetReq}, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - groupID := cents.GetAssetGroup().GetTweakedGroupKey() - - syncUniverses(t.t, charlieTap, dave, erin, fabia, yara, george) - - multiRfqNodes := multiRfqNodes{ - charlie: itestNode{ - Lnd: charlie, - Tapd: charlieTap, - }, - dave: itestNode{ - Lnd: dave, - Tapd: daveTap, - }, - erin: itestNode{ - Lnd: erin, - Tapd: erinTap, - }, - fabia: itestNode{ - Lnd: fabia, - Tapd: fabiaTap, - }, - yara: itestNode{ - Lnd: yara, - Tapd: yaraTap, - }, - george: itestNode{ - Lnd: george, - Tapd: georgeTap, - }, - universeTap: charlieTap, - } - - createTestMultiRFQAssetNetwork( - t, net, multiRfqNodes, cents, 10_000, 10_000, 10_000, - ) - - logBalance(t.t, nodes, assetID, "before multi-rfq receive") - - hodlInv := createAssetHodlInvoice(t.t, nil, fabia, 20_000, assetID) - - payInvoiceWithSatoshi( - t.t, charlie, &lnrpc.AddInvoiceResponse{ - PaymentRequest: hodlInv.payReq, - }, - withFailure(lnrpc.Payment_IN_FLIGHT, failureNone), - ) - - logBalance(t.t, nodes, assetID, "after inflight multi-rfq") - - // Assert that some HTLCs are present from Fabia's point of view. - assertMinNumHtlcs(t.t, fabia, 1) - - // Assert that Charlie also has at least one outgoing HTLC as a sanity - // check. - assertMinNumHtlcs(t.t, charlie, 1) - - // Now let's cancel the invoice and assert that all inbound channels - // have cleared their HTLCs. - payHash := hodlInv.preimage.Hash() - _, err = fabia.InvoicesClient.CancelInvoice( - ctx, &invoicesrpc.CancelInvoiceMsg{ - PaymentHash: payHash[:], - }, - ) - require.NoError(t.t, err) - - assertNumHtlcs(t.t, dave, 0) - assertNumHtlcs(t.t, erin, 0) - assertNumHtlcs(t.t, yara, 0) - - logBalance(t.t, nodes, assetID, "after cancelled hodl") - - // Now let's create a normal invoice that will be settled once all the - // HTLCs have been received. This is only possible because the payer - // uses multiple bolt11 hop hints to reach the destination. - invoiceResp := createAssetInvoice( - t.t, nil, fabia, 15_000, nil, withInvGroupKey(groupID), - ) - - payInvoiceWithSatoshi( - t.t, charlie, invoiceResp, - ) - - logBalance(t.t, nodes, assetID, "after multi-rfq receive") - - // Now we'll test that sending with multiple rfq quotes works. - - // Let's start by providing some liquidity to Charlie's peers, in order - // for them to be able to push some amount if Fabia picks them as part - // of the route. - sendKeySendPayment(t.t, charlie, erin, 800_000) - sendKeySendPayment(t.t, charlie, dave, 800_000) - sendKeySendPayment(t.t, charlie, yara, 800_000) - - // Let's ask for the rough equivalent of ~15k assets. Fabia, who's going - // to pay the invoice, only has parts of assets that are less than 10k - // in channels with one of the 3 intermediate peers. The only way to - // pay this invoice is by splitting the payment across multiple peers by - // using multiple RFQ quotes. - invAmt := int64(15_000 * 17) - - iResp, err := charlie.AddHoldInvoice( - ctx, &invoicesrpc.AddHoldInvoiceRequest{ - Memo: "", - Value: invAmt, - Hash: payHash[:], - }, - ) - require.NoError(t.t, err) - - payReq := iResp.PaymentRequest - - payInvoiceWithAssets( - t.t, fabia, nil, payReq, assetID, - withFailure(lnrpc.Payment_IN_FLIGHT, failureNone), - ) - - assertMinNumHtlcs(t.t, charlie, 2) - assertMinNumHtlcs(t.t, fabia, 2) - - logBalance(t.t, nodes, assetID, "multi-rfq send in-flight") - - _, err = charlie.SettleInvoice(ctx, &invoicesrpc.SettleInvoiceMsg{ - Preimage: hodlInv.preimage[:], - }) - require.NoError(t.t, err) - - assertNumHtlcs(t.t, charlie, 0) - assertNumHtlcs(t.t, fabia, 0) - - logBalance(t.t, nodes, assetID, "after multi-rfq send") - - // Let's make another round-trip involving multi-rfq functionality. - // Let's have Fabia receive another large payment and send it back - // again, this time with a greater amount. - invoiceResp = createAssetInvoice(t.t, nil, fabia, 25_000, assetID) - - payInvoiceWithSatoshi( - t.t, charlie, invoiceResp, - ) - - logBalance(t.t, nodes, assetID, "after multi-rfq receive (2nd)") - - // Let's bump up the invoice amount a bit, to roughly ~22k assets. - invAmt = 22_000 * 17 - inv, err := charlie.AddInvoice(ctx, &lnrpc.Invoice{ - Value: invAmt, - }) - require.NoError(t.t, err) - - payReq = inv.PaymentRequest - - payInvoiceWithAssets( - t.t, fabia, nil, payReq, nil, withGroupKey(groupID), - ) - - logBalance(t.t, nodes, assetID, "after multi-rfq send (2nd)") -} - -// testCustomChannelsStrictForwarding is a test that tests the strict forwarding -// behavior of a node when it comes to paying asset invoices with assets and -// BTC invoices with satoshis. -func testCustomChannelsStrictForwarding(ctx context.Context, - net *NetworkHarness, t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - // The topology we are going for looks like the following: - // - // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia - // | - // | - // [assets] - // | - // v - // Yara - // - // With [assets] being a custom channel and [sats] being a normal, BTC - // only channel. - // All 5 nodes need to be full litd nodes running in integrated mode - // with tapd included. We also need specific flags to be enabled, so we - // create 5 completely new nodes, ignoring the two default nodes that - // are created by the harness. - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - fabia, err := net.NewNode( - t.t, "Fabia", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - yara, err := net.NewNode( - t.t, "Yara", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - // Create the normal channel between Dave and Erin. - t.Logf("Opening normal channel between Dave and Erin...") - channelOp := openChannelAndAssert( - t, net, dave, erin, lntest.OpenChannelParams{ - Amt: 10_000_000, - SatPerVByte: 5, - }, - ) - defer closeChannelAndAssert(t, net, dave, channelOp, false) - - // This is the only public channel, we need everyone to be aware of it. - assertChannelKnown(t.t, charlie, channelOp) - assertChannelKnown(t.t, fabia, channelOp) - - universeTap := newTapClient(t.t, charlie) - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - erinTap := newTapClient(t.t, erin) - fabiaTap := newTapClient(t.t, fabia) - yaraTap := newTapClient(t.t, yara) - - // Mint an asset on Charlie and sync all nodes to Charlie as the - // universe. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - - t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) - t.Logf("Universes synced between all nodes, distributing assets...") - - const ( - daveFundingAmount = uint64(400_000) - erinFundingAmount = uint64(200_000) - ) - charlieFundingAmount := cents.Amount - uint64(2*400_000) - - _, _, _ = createTestAssetNetwork( - t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, - universeTap, cents, 400_000, charlieFundingAmount, - daveFundingAmount, erinFundingAmount, 0, - ) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) - - logBalance(t.t, nodes, assetID, "initial") - - // Do a payment from Charlie to Erin to shift the balances in all - // channels enough to allow for the following payments in any direction. - // Pay a normal bolt11 invoice involving RFQ flow. - _ = createAndPayNormalInvoice( - t.t, charlie, dave, erin, 500_000, assetID, withSmallShards(), - ) - - logBalance(t.t, nodes, assetID, "after payment") - - // Edge case: Now Dave creates an asset invoice to be paid for by Erin - // with satoshi. For the last hop we try to settle the invoice in - // satoshi, where we will check whether Daves's strict forwarding - // works as expected. Charlie is only used as a dummy RFQ peer in this - // case, Erin totally ignores the RFQ hint and just pays with sats. - assetInvoice := createAssetInvoice(t.t, charlie, dave, 40, assetID) - - assetInvoiceStream, err := dave.InvoicesClient.SubscribeSingleInvoice( - ctx, &invoicesrpc.SubscribeSingleInvoiceRequest{ - RHash: assetInvoice.RHash, - }, - ) - require.NoError(t.t, err) - - // Erin pays Dave with enough satoshis, but Charlie will not settle as - // he expects assets. - hops := [][]byte{dave.PubKey[:]} - payInvoiceWithSatoshiLastHop(t.t, erin, assetInvoice, hops, withFailure( - lnrpc.Payment_FAILED, 0, - )) - - // Make sure the invoice hasn't been settled and there's no HTLC on the - // channel between Erin and Dave. - t.lndHarness.LNDHarness.AssertInvoiceState( - assetInvoiceStream, lnrpc.Invoice_OPEN, - ) - assertHTLCNotActive(t.t, erin, channelOp, assetInvoice.RHash) - assertInvoiceState( - t.t, dave, assetInvoice.PaymentAddr, lnrpc.Invoice_OPEN, - ) - - logBalance(t.t, nodes, assetID, "after failed payment (asset "+ - "invoice, strict forwarding)") - - // Now let's make sure that we can actually still pay the invoice with - // assets from Charlie. - payInvoiceWithAssets( - t.t, charlie, dave, assetInvoice.PaymentRequest, assetID, - ) - t.lndHarness.LNDHarness.AssertInvoiceState( - assetInvoiceStream, lnrpc.Invoice_SETTLED, - ) - assertInvoiceState( - t.t, dave, assetInvoice.PaymentAddr, lnrpc.Invoice_SETTLED, - ) - - // Edge case: We now try the opposite: Dave creates a BTC invoice but - // Charlie tries to pay it with assets. This should fail as well. - btcInvoice := createNormalInvoice(t.t, dave, 1_000) - btcInvoiceStream, err := dave.InvoicesClient.SubscribeSingleInvoice( - ctx, &invoicesrpc.SubscribeSingleInvoiceRequest{ - RHash: btcInvoice.RHash, - }, - ) - require.NoError(t.t, err) - - payInvoiceWithAssets( - t.t, charlie, dave, btcInvoice.PaymentRequest, assetID, - withFailure(lnrpc.Payment_FAILED, failureIncorrectDetails), - ) - t.lndHarness.LNDHarness.AssertInvoiceState( - btcInvoiceStream, lnrpc.Invoice_OPEN, - ) - assertHTLCNotActive(t.t, erin, channelOp, btcInvoice.RHash) - assertInvoiceState( - t.t, dave, btcInvoice.PaymentAddr, lnrpc.Invoice_OPEN, - ) - - // And finally we make sure that we can still pay the invoice with - // satoshis from Erin, using custom records. - payInvoiceWithSatoshi(t.t, erin, btcInvoice, withDestCustomRecords( - map[uint64][]byte{106823: {0x01}}, - )) - t.lndHarness.LNDHarness.AssertInvoiceState( - btcInvoiceStream, lnrpc.Invoice_SETTLED, - ) - assertInvoiceState( - t.t, dave, btcInvoice.PaymentAddr, lnrpc.Invoice_SETTLED, - ) -} - -// testCustomChannelsBalanceConsistency is a test that test the balance of nodes -// under channel opening circumstances. -func testCustomChannelsBalanceConsistency(ctx context.Context, - net *NetworkHarness, t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - universeTap := newTapClient(t.t, charlie) - - // Mint an asset on Charlie and sync Dave to Charlie as the universe. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - var groupKey []byte - if cents.AssetGroup != nil { - groupKey = cents.AssetGroup.TweakedGroupKey - } - - t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, charlieTap, dave) - t.Logf("Universes synced between all nodes, distributing assets...") - - charlieBalance := cents.Amount - - // Charlie should have a single balance output with the full balance. - itest.AssertBalances( - t.t, charlieTap, cents.Amount, itest.WithAssetID(assetID), - itest.WithNumUtxos(1), - ) - - // The script key should be local to charlie, and the script key should - // be known. It is after all the asset he just minted himself. - itest.AssertBalances( - t.t, charlieTap, cents.Amount, itest.WithAssetID(assetID), - itest.WithNumUtxos(1), itest.WithScriptKey(cents.ScriptKey), - itest.WithScriptKeyType(asset.ScriptKeyBip86), - ) - - fundingScriptTree := tapscript.NewChannelFundingScriptTree() - fundingScriptKey := fundingScriptTree.TaprootKey - fundingScriptKeyBytes := fundingScriptKey.SerializeCompressed() - - fundRespCD, err := charlieTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: charlieBalance, - AssetId: assetID, - PeerPubkey: daveTap.node.PubKey[:], - FeeRateSatPerVbyte: 5, - PushSat: 0, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded channel between Charlie and Dave: %v", fundRespCD) - - // Make sure the pending channel shows up in the list and has the - // custom records set as JSON. - assertPendingChannels( - t.t, charlieTap.node, cents, 1, charlieBalance, 0, - ) - - // Let's confirm the channel. - mineBlocks(t, net, 6, 1) - - // Tapd should not report any balance for Charlie, since the asset is - // used in a funding transaction. It should also not report any balance - // for Dave. All those balances are reported through channel balances. - itest.AssertBalances(t.t, charlieTap, 0, itest.WithAssetID(assetID)) - itest.AssertBalances(t.t, daveTap, 0, itest.WithAssetID(assetID)) - - // There should only be a single asset piece for Charlie, the one in the - // channel. - itest.AssertBalances( - t.t, charlieTap, charlieBalance, itest.WithAssetID(assetID), - itest.WithNumUtxos(1), - itest.WithScriptKeyType(asset.ScriptKeyScriptPathChannel), - itest.WithScriptKey(fundingScriptKeyBytes), - ) - - // 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), - ) - - // Make sure the channel shows the correct asset information. - assertAssetChan( - t.t, charlieTap.node, daveTap.node, charlieBalance, - []*taprpc.Asset{cents}, - ) - - logBalance(t.t, nodes, assetID, "initial") - - // Normal case. - // Send 500 assets from Charlie to Dave. - sendAssetKeySendPayment( - t.t, charlie, dave, 500, assetID, fn.None[int64](), - ) - - logBalance(t.t, nodes, assetID, "after 500 assets") - - // Tapd should still not report balances for Charlie and Dave, since - // they are still locked up in the funding transaction. - itest.AssertBalances(t.t, charlieTap, 0, itest.WithAssetID(assetID)) - itest.AssertBalances(t.t, daveTap, 0, itest.WithAssetID(assetID)) - - // Send 10k sats from Charlie to Dave. Dave needs the sats to be able to - // send assets. - sendKeySendPayment(t.t, charlie, dave, 10000) - - // Now Dave tries to send 250 assets. - sendAssetKeySendPayment( - t.t, dave, charlie, 250, assetID, fn.None[int64](), - ) - - logBalance(t.t, nodes, assetID, "after 250 sats backwards") - - // Tapd should still not report balances for Charlie and Dave, since - // they are still locked up in the funding transaction. - itest.AssertBalances(t.t, charlieTap, 0, itest.WithAssetID(assetID)) - itest.AssertBalances(t.t, daveTap, 0, itest.WithAssetID(assetID)) - - // We will now close the channel. - t.Logf("Close the channel between Charlie and Dave...") - charlieChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(fundRespCD.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: fundRespCD.Txid, - }, - } - - closeChannelAndAssert(t, net, charlie, charlieChanPoint, false) - - // Charlie should have a single balance output with the balance 250 less - // than the total amount minted. - itest.AssertBalances( - t.t, charlieTap, charlieBalance-250, itest.WithAssetID(assetID), - itest.WithNumUtxos(1), - itest.WithScriptKeyType(asset.ScriptKeyBip86), - ) - itest.AssertBalances( - t.t, daveTap, 250, itest.WithAssetID(assetID), - itest.WithNumUtxos(1), - itest.WithScriptKeyType(asset.ScriptKeyBip86), - ) -} - -// testCustomChannelsSingleAssetMultiInput tests whether it is possible to fund -// a channel using FundChannel that uses multiple inputs from the same asset. -func testCustomChannelsSingleAssetMultiInput(ctx context.Context, - net *NetworkHarness, t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - - // Mint an assets on Charlie and sync Dave to Charlie as the universe. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - - t.Logf("Minted %d lightning cents, syncing universes...", - cents.Amount) - syncUniverses(t.t, charlieTap, dave) - t.Logf("Universes synced between all nodes, distributing assets...") - - // Charlie should have two balance outputs with the full balance. - itest.AssertBalances( - t.t, charlieTap, cents.Amount, itest.WithAssetID(assetID), - itest.WithNumUtxos(1), - itest.WithScriptKeyType(asset.ScriptKeyBip86), - ) - - // Send assets to Dave so he can fund a channel. - halfCentsAmount := cents.Amount / 2 - daveAddr1, err := daveTap.NewAddr(ctx, &taprpc.NewAddrRequest{ - Amt: halfCentsAmount, - AssetId: assetID, - ProofCourierAddr: fmt.Sprintf( - "%s://%s", proof.UniverseRpcCourierType, - charlieTap.node.Cfg.LitAddr(), - ), - }) - require.NoError(t.t, err) - daveAddr2, err := daveTap.NewAddr(ctx, &taprpc.NewAddrRequest{ - Amt: halfCentsAmount, - 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 twice...", halfCentsAmount) - - // Send the assets to Dave. - itest.AssertAddrCreated(t.t, daveTap, cents, daveAddr1) - itest.AssertAddrCreated(t.t, daveTap, cents, daveAddr2) - sendResp, err := charlieTap.SendAsset(ctx, &taprpc.SendAssetRequest{ - TapAddrs: []string{daveAddr1.Encoded, daveAddr2.Encoded}, - }) - require.NoError(t.t, err) - itest.ConfirmAndAssertOutboundTransferWithOutputs( - t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, - []uint64{ - cents.Amount - 2*halfCentsAmount, halfCentsAmount, - halfCentsAmount, - }, 0, 1, 3, - ) - itest.AssertNonInteractiveRecvComplete(t.t, daveTap, 2) - - // Fund a channel using multiple inputs from the same asset. - fundRespCD, err := daveTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: 2 * halfCentsAmount, - AssetId: assetID, - PeerPubkey: charlieTap.node.PubKey[:], - FeeRateSatPerVbyte: 5, - PushSat: 0, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded channel between Charlie and Dave: %v", fundRespCD) - - // Let's confirm the channel. - mineBlocks(t, net, 6, 1) - - // Tapd should not report any balance for Charlie, since the asset is - // used in a funding transaction. It should also not report any balance - // for Dave. All those balances are reported through channel balances. - itest.AssertBalances(t.t, charlieTap, 0, itest.WithAssetID(assetID)) - itest.AssertBalances(t.t, daveTap, 0, itest.WithAssetID(assetID)) - - // Make sure the channel shows the correct asset information. - assertAssetChan( - t.t, charlieTap.node, daveTap.node, 2*halfCentsAmount, - []*taprpc.Asset{cents}, - ) -} - -// testCustomChannelsOraclePricing tests that all asset transfers are correctly -// priced when using an oracle that isn't tapd's mock oracle. -func testCustomChannelsOraclePricing(ctx context.Context, net *NetworkHarness, - t *harnessTest) { - - usdMetaData := &taprpc.AssetMeta{ - Data: []byte(`{ -"description":"this is a USD stablecoin with decimal display of 6" -}`), - Type: taprpc.AssetMetaType_META_TYPE_JSON, - } - - const decimalDisplay = 6 - tcAsset := &mintrpc.MintAsset{ - AssetType: taprpc.AssetType_NORMAL, - Name: "USD", - AssetMeta: usdMetaData, - // We mint 1 million USD with a decimal display of 6, which - // results in 1 trillion asset units. - Amount: 1_000_000_000_000, - DecimalDisplay: decimalDisplay, - } - - oracleAddr := fmt.Sprintf("localhost:%d", port.NextAvailablePort()) - oracle := newOracleHarness(oracleAddr) - oracle.start(t.t) - t.t.Cleanup(oracle.stop) - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplateNoOracle) - litdArgs = append(litdArgs, - "--taproot-assets.experimental.rfq.priceoracletlsinsecure", - fmt.Sprintf( - "--taproot-assets.experimental.rfq.priceoracleaddress="+ - "rfqrpc://%s", oracleAddr, - )) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - // The topology we are going for looks like the following: - // - // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia - // | - // | - // [assets] - // | - // v - // Yara - // - // With [assets] being a custom channel and [sats] being a normal, BTC - // only channel. - // All 5 nodes need to be full litd nodes running in integrated mode - // with tapd included. We also need specific flags to be enabled, so we - // create 5 completely new nodes, ignoring the two default nodes that - // are created by the harness. - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - fabia, err := net.NewNode( - t.t, "Fabia", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - yara, err := net.NewNode( - t.t, "Yara", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - // Create the normal channel between Dave and Erin. - t.Logf("Opening normal channel between Dave and Erin...") - const btcChannelFundingAmount = 10_000_000 - chanPointDE := openChannelAndAssert( - t, net, dave, erin, lntest.OpenChannelParams{ - Amt: btcChannelFundingAmount, - SatPerVByte: 5, - }, - ) - defer closeChannelAndAssert(t, net, dave, chanPointDE, false) - - // This is the only public channel, we need everyone to be aware of it. - assertChannelKnown(t.t, charlie, chanPointDE) - assertChannelKnown(t.t, fabia, chanPointDE) - - universeTap := newTapClient(t.t, charlie) - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - erinTap := newTapClient(t.t, erin) - fabiaTap := newTapClient(t.t, fabia) - yaraTap := newTapClient(t.t, yara) - - // Mint an asset on Charlie and sync Dave to Charlie as the universe. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{ - { - Asset: tcAsset, - }, - }, - ) - usdAsset := mintedAssets[0] - assetID := usdAsset.AssetGenesis.AssetId - - // Now that we've minted the asset, we can set the price in the oracle. - var id asset.ID - copy(id[:], assetID) - - // Let's assume the current USD price for 1 BTC is 66,548.40. We'll take - // that price and add a 4% spread, 2% on each side (buy/sell) to earn - // money as the oracle. 2% is 1,330.97, so we'll set the sell price to - // 65,217.43 and the purchase price to 67,879.37. - // The following numbers are to help understand the magic numbers below. - // They're the price in USD/BTC, the price of 1 USD in sats and the - // expected price in asset units per BTC. - // 65,217.43 => 1533.332 => 65_217_430_000 - // 66,548.40 => 1502.666 => 66_548_400_000 - // 67,879.37 => 1473.202 => 67_879_370_000 - salePrice := rfqmath.NewBigIntFixedPoint(65_217_43, 2) - purchasePrice := rfqmath.NewBigIntFixedPoint(67_879_37, 2) - - // We now have the prices defined in USD. But the asset has a decimal - // display of 6, so we need to multiply them by 10^6. - factor := rfqmath.NewBigInt( - big.NewInt(int64(math.Pow10(decimalDisplay))), - ) - salePrice.Coefficient = salePrice.Coefficient.Mul(factor) - purchasePrice.Coefficient = purchasePrice.Coefficient.Mul(factor) - oracle.setPrice(id, purchasePrice, salePrice) - - t.Logf("Minted %d USD assets, syncing universes...", usdAsset.Amount) - syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) - t.Logf("Universes synced between all nodes, distributing assets...") - - const ( - sendAmount = uint64(400_000_000) - daveFundingAmount = uint64(400_000_000) - erinFundingAmount = uint64(200_000_000) - ) - charlieFundingAmount := usdAsset.Amount - 2*sendAmount - - chanPointCD, chanPointDY, chanPointEF := createTestAssetNetwork( - t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, - universeTap, usdAsset, sendAmount, charlieFundingAmount, - daveFundingAmount, erinFundingAmount, 0, - ) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) - - // We now create an invoice at Fabia for 100 USD, which is 100_000_000 - // asset units with decimal display of 6. - const fabiaInvoiceAssetAmount = 100_000_000 - invoiceResp := createAssetInvoice( - t.t, erin, fabia, fabiaInvoiceAssetAmount, assetID, - ) - decodedInvoice, err := fabia.DecodePayReq(ctx, &lnrpc.PayReqString{ - PayReq: invoiceResp.PaymentRequest, - }) - require.NoError(t.t, err) - - // The invoice amount should come out as 100 * 1533.332. - require.EqualValues(t.t, 153_333_242, decodedInvoice.NumMsat) - - numUnits, rate := payInvoiceWithAssets( - t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, - ) - logBalance(t.t, nodes, assetID, "after invoice") - - // The calculated amount Charlie has to pay should come out as - // 153_333_242 / 1473.202, which is quite exactly 4% more than will - // arrive at the destination (which is the oracle's configured spread). - // This is before routing fees though. - const charlieInvoiceAmount = 104_081_638 - require.EqualValues(t.t, charlieInvoiceAmount, numUnits) - - // The default routing fees are 1ppm + 1msat per hop, and we have 2 - // hops in total. - charliePaidMSat := addRoutingFee(addRoutingFee(lnwire.MilliSatoshi( - decodedInvoice.NumMsat, - ))) - charliePaidAmount := rfqmath.MilliSatoshiToUnits( - charliePaidMSat, rate, - ).ScaleTo(0).ToUint64() - assertPaymentHtlcAssets( - t.t, charlie, invoiceResp.RHash, assetID, nil, - charliePaidAmount, - ) - - // We now make sure the asset and satoshi channel balances are exactly - // what we expect them to be. - var ( - // channelFundingAmount is the hard coded satoshi amount that - // currently goes into asset channels. - channelFundingAmount int64 = 100_000 - - // commitFeeP2TR is the default commit fee for a P2TR channel - // commitment with 4 outputs (to_local, to_remote, 2 anchors). - commitFeeP2TR int64 = 2420 - commitFeeP2WSH int64 = 2810 - anchorAmount int64 = 330 - assetHtlcCarryAmount = int64( - rfqmath.DefaultOnChainHtlcSat, - ) - unbalancedLocalAmount = channelFundingAmount - commitFeeP2TR - - anchorAmount - balancedLocalAmount = unbalancedLocalAmount - anchorAmount - ) - - // Checking Charlie's sat and asset balances in channel Charlie->Dave. - assertChannelSatBalance( - t.t, charlie, chanPointCD, - balancedLocalAmount-assetHtlcCarryAmount, assetHtlcCarryAmount, - ) - assertChannelAssetBalance( - t.t, charlie, chanPointCD, - charlieFundingAmount-charliePaidAmount, charliePaidAmount, - ) - - // Checking Dave's sat and asset balances in channel Charlie->Dave. - assertChannelSatBalance( - t.t, dave, chanPointCD, - assetHtlcCarryAmount, balancedLocalAmount-assetHtlcCarryAmount, - ) - assertChannelAssetBalance( - t.t, dave, chanPointCD, - charliePaidAmount, charlieFundingAmount-charliePaidAmount, - ) - - // Checking Dave's sat balance in channel Dave->Erin. - forwardAmountDave := addRoutingFee( - lnwire.MilliSatoshi(decodedInvoice.NumMsat), - ).ToSatoshis() - assertChannelSatBalance( - t.t, dave, chanPointDE, - btcChannelFundingAmount-commitFeeP2WSH-2*anchorAmount- - int64(forwardAmountDave), - int64(forwardAmountDave), - ) - - // Checking Erin's sat balance in channel Dave->Erin. - assertChannelSatBalance( - t.t, erin, chanPointDE, - int64(forwardAmountDave), - btcChannelFundingAmount-commitFeeP2WSH-2*anchorAmount- - int64(forwardAmountDave), - ) - - // Checking Erin's sat and asset balances in channel Erin->Fabia. - assertChannelSatBalance( - t.t, erin, chanPointEF, - balancedLocalAmount-assetHtlcCarryAmount, assetHtlcCarryAmount, - ) - assertChannelAssetBalance( - t.t, erin, chanPointEF, - erinFundingAmount-fabiaInvoiceAssetAmount, - fabiaInvoiceAssetAmount, - ) - - // Checking Fabia's sat and asset balances in channel Erin->Fabia. - assertChannelSatBalance( - t.t, fabia, chanPointEF, - assetHtlcCarryAmount, balancedLocalAmount-assetHtlcCarryAmount, - ) - assertChannelAssetBalance( - t.t, erin, chanPointEF, - fabiaInvoiceAssetAmount, - erinFundingAmount-fabiaInvoiceAssetAmount, - ) - - t.Logf("Closing Charlie -> Dave channel") - closeAssetChannelAndAssert( - t, net, charlie, dave, chanPointCD, [][]byte{assetID}, nil, - universeTap, noOpCoOpCloseBalanceCheck, - ) - - t.Logf("Closing Dave -> Yara channel, close initiated by Yara") - closeAssetChannelAndAssert( - t, net, yara, dave, chanPointDY, [][]byte{assetID}, nil, - universeTap, noOpCoOpCloseBalanceCheck, - ) - - t.Logf("Closing Erin -> Fabia channel") - closeAssetChannelAndAssert( - t, net, erin, fabia, chanPointEF, [][]byte{assetID}, nil, - universeTap, noOpCoOpCloseBalanceCheck, - ) -} - -// testCustomChannelsFee tests whether the custom channel funding process -// fails if the proposed fee rate is lower than the minimum relay fee. -func testCustomChannelsFee(ctx context.Context, net *NetworkHarness, - t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - - // Mint an assets on Charlie and sync Dave to Charlie as the universe. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - - t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, charlieTap, dave) - t.Logf("Universes synced between all nodes, distributing assets...") - - // Fund a channel with a fee rate of zero. - zeroFeeRate := uint32(0) - - _, err = charlieTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: cents.Amount, - AssetId: assetID, - PeerPubkey: daveTap.node.PubKey[:], - FeeRateSatPerVbyte: zeroFeeRate, - PushSat: 0, - }, - ) - - errSpecifyFeerate := "fee rate must be specified" - require.ErrorContains(t.t, err, errSpecifyFeerate) - - net.feeService.SetMinRelayFeerate( - chainfee.SatPerVByte(2).FeePerKVByte(), - ) - - // Fund a channel with a fee rate that is too low. - tooLowFeeRate := uint32(1) - tooLowFeeRateAmount := chainfee.SatPerVByte(tooLowFeeRate) - - _, err = charlieTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: cents.Amount, - AssetId: assetID, - PeerPubkey: daveTap.node.PubKey[:], - FeeRateSatPerVbyte: tooLowFeeRate, - PushSat: 0, - }, - ) - - errFeeRateTooLow := fmt.Sprintf("fee rate %s too low, "+ - "min_relay_fee: ", tooLowFeeRateAmount.FeePerKWeight()) - require.ErrorContains(t.t, err, errFeeRateTooLow) -} - -// testCustomChannelsHtlcForceClose tests that we can force close a channel -// with HTLCs in both directions and that the HTLC outputs are correctly -// swept. -func testCustomChannelsHtlcForceClose(ctxb context.Context, net *NetworkHarness, - t *harnessTest) { - - runCustomChannelsHtlcForceClose(ctxb, t, net, false) -} - -// testCustomChannelsHtlcForceCloseMpp tests that we can force close a channel -// with HTLCs in both directions and that the HTLC outputs are correctly -// swept, using MPP. -func testCustomChannelsHtlcForceCloseMpp(ctxb context.Context, - net *NetworkHarness, t *harnessTest) { - - runCustomChannelsHtlcForceClose(ctxb, t, net, true) -} - -// runCustomChannelsHtlcForceClose is a helper function that runs the HTLC force -// close test with the given MPP setting. -func runCustomChannelsHtlcForceClose(ctx context.Context, t *harnessTest, - net *NetworkHarness, mpp bool) { - - t.Logf("Running test with MPP: %v", mpp) - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // Zane will serve as our designated Universe node. - zane, err := net.NewNode( - t.t, "Zane", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), - )) - - // Next, we'll make Alice and Bob, who will be the main nodes under - // test. - alice, err := net.NewNode( - t.t, "Alice", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - bob, err := net.NewNode( - t.t, "Bob", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - // Now we'll connect all nodes, and also fund them with some coins. - nodes := []*HarnessNode{alice, bob} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - aliceTap := newTapClient(t.t, alice) - bobTap := newTapClient(t.t, bob) - - // Next, we'll mint an asset for Alice, who will be the node that opens - // the channel outbound. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, aliceTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - - t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, aliceTap, bob) - t.Logf("Universes synced between all nodes, distributing assets...") - - // With the assets created, and synced -- we'll now open the channel - // between Alice and Bob. - t.Logf("Opening asset channels...") - assetFundResp, err := aliceTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetID, - PeerPubkey: bob.PubKey[:], - FeeRateSatPerVbyte: 5, - }, - ) - require.NoError(t.t, err) - - aliceChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(assetFundResp.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: assetFundResp.Txid, - }, - } - - t.Logf("Funded channel between Alice and Bob: %v", assetFundResp) - - // With the channel open, mine a block to confirm it. - mineBlocks(t, net, 6, 1) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(alice, bob)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(bob, alice)) - - // First, we'll send over some funds from Alice to Bob, as we want Bob - // to be able to extend HTLCs in the other direction. - const ( - numPayments = 10 - keySendAssetAmount = 2_500 - keySendSatAmount = 5_000 - ) - for i := 0; i < numPayments; i++ { - sendAssetKeySendPayment( - t.t, alice, bob, keySendAssetAmount, assetID, - fn.None[int64](), - ) - } - - // With noop HTLCs implemented the sats balance of Bob will only - // increase up to the reserve amount. Let's make a direct non-asset - // keysend to make sure the sats balance is also enough. - sendKeySendPayment(t.t, alice, bob, keySendSatAmount) - - logBalance(t.t, nodes, assetID, "after keysends to Bob") - - // Now that both parties have some funds, we'll move onto the main test. - // - // We'll make 2 hodl invoice for each peer, so 4 total. From Alice's - // PoV, she'll have two outgoing HTLCs (or +4 with MPP), and two - // incoming HTLCs. - var ( - bobHodlInvoices []assetHodlInvoice - aliceHodlInvoices []assetHodlInvoice - - // The default oracle rate is 17_180 mSat/asset unit, so 10_000 - // will be equal to 171_800_000 mSat. When we use the mpp bool - // for the smallShards param of payInvoiceWithAssets, that - // means we'll split the payment into shards of 80_000_000 mSat - // max. So we'll get three shards per payment. - assetInvoiceAmt = 10_000 - assetsPerMPPShard = 4656 - ) - for i := 0; i < 2; i++ { - bobHodlInvoices = append( - bobHodlInvoices, createAssetHodlInvoice( - t.t, alice, bob, uint64(assetInvoiceAmt), - assetID, - ), - ) - aliceHodlInvoices = append( - aliceHodlInvoices, createAssetHodlInvoice( - t.t, bob, alice, uint64(assetInvoiceAmt), - assetID, - ), - ) - } - - // Now we'll have both Bob and Alice pay each other's invoices. We only - // care that they're in flight at this point, as they won't be settled - // yet. - for _, aliceInvoice := range aliceHodlInvoices { - opts := []payOpt{ - withFailure( - lnrpc.Payment_IN_FLIGHT, - lnrpc.PaymentFailureReason_FAILURE_REASON_NONE, - ), - } - if mpp { - opts = append(opts, withSmallShards()) - } - payInvoiceWithAssets( - t.t, bob, alice, aliceInvoice.payReq, assetID, opts..., - ) - } - for _, bobInvoice := range bobHodlInvoices { - payInvoiceWithAssets( - t.t, alice, bob, bobInvoice.payReq, assetID, - withFailure( - lnrpc.Payment_IN_FLIGHT, - lnrpc.PaymentFailureReason_FAILURE_REASON_NONE, - ), - ) - } - - // Make sure we can sweep all the HTLCs. - aliceExpectedBalance, bobExpectedBalance := assertForceCloseSweeps( - ctx, net, t, alice, bob, aliceChanPoint, - itestAsset.Amount-fundingAmount, assetInvoiceAmt, - assetsPerMPPShard, assetID, nil, aliceHodlInvoices, - bobHodlInvoices, mpp, - ) - - // Finally, we'll assert that Alice's balance has been incremented by - // the timeout value. - aliceExpectedBalance += uint64(assetInvoiceAmt - 1) - t.Logf("Expecting Alice's balance to be %d", aliceExpectedBalance) - assertSpendableBalance( - t.t, aliceTap, assetID, nil, aliceExpectedBalance, - ) - - t.Logf("Sending all settled funds to Zane") - - // As a final sanity check, both Alice and Bob should be able to send - // their entire balances to Zane, our 3rd party. - // - // We'll make two addrs for Zane, one for Alice, and one for bob. - zaneTap := newTapClient(t.t, zane) - aliceAddr, err := zaneTap.NewAddr(ctx, &taprpc.NewAddrRequest{ - Amt: aliceExpectedBalance, - AssetId: assetID, - ProofCourierAddr: fmt.Sprintf( - "%s://%s", proof.UniverseRpcCourierType, - zaneTap.node.Cfg.LitAddr(), - ), - }) - require.NoError(t.t, err) - bobAddr, err := zaneTap.NewAddr(ctx, &taprpc.NewAddrRequest{ - Amt: bobExpectedBalance, - AssetId: assetID, - ProofCourierAddr: fmt.Sprintf( - "%s://%s", proof.UniverseRpcCourierType, - zaneTap.node.Cfg.LitAddr(), - ), - }) - require.NoError(t.t, err) - - _, err = aliceTap.SendAsset(ctx, &taprpc.SendAssetRequest{ - TapAddrs: []string{aliceAddr.Encoded}, - }) - require.NoError(t.t, err) - mineBlocks(t, net, 1, 1) - - itest.AssertNonInteractiveRecvComplete(t.t, zaneTap, 1) - - _, err = bobTap.SendAsset(ctx, &taprpc.SendAssetRequest{ - TapAddrs: []string{bobAddr.Encoded}, - }) - require.NoError(t.t, err) - mineBlocks(t, net, 1, 1) - - itest.AssertNonInteractiveRecvComplete(t.t, zaneTap, 2) - - // Zane's balance should now be the sum of Alice's and Bob's balances. - zaneExpectedBalance := aliceExpectedBalance + bobExpectedBalance - assertSpendableBalance( - t.t, zaneTap, assetID, nil, zaneExpectedBalance, - ) -} - -// testCustomChannelsForwardBandwidth is a test that runs through some Taproot -// Assets Channel liquidity edge cases, specifically related to forwarding HTLCs -// into channels with no available asset bandwidth. -func testCustomChannelsForwardBandwidth(ctx context.Context, - net *NetworkHarness, t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Charlie as the proof courier. But in order for Charlie to also - // use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - // The topology we are going for looks like the following: - // - // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia - // | - // | - // [assets] - // | - // v - // Yara - // - // With [assets] being a custom channel and [sats] being a normal, BTC - // only channel. - // All 5 nodes need to be full litd nodes running in integrated mode - // with tapd included. We also need specific flags to be enabled, so we - // create 5 completely new nodes, ignoring the two default nodes that - // are created by the harness. - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - fabia, err := net.NewNode( - t.t, "Fabia", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - yara, err := net.NewNode( - t.t, "Yara", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - // Create the normal channel between Dave and Erin. - t.Logf("Opening normal channel between Dave and Erin...") - channelOp := openChannelAndAssert( - t, net, dave, erin, lntest.OpenChannelParams{ - Amt: 10_000_000, - SatPerVByte: 5, - }, - ) - defer closeChannelAndAssert(t, net, dave, channelOp, false) - - // This is the only public channel, we need everyone to be aware of it. - assertChannelKnown(t.t, charlie, channelOp) - assertChannelKnown(t.t, fabia, channelOp) - - universeTap := newTapClient(t.t, charlie) - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - erinTap := newTapClient(t.t, erin) - fabiaTap := newTapClient(t.t, fabia) - yaraTap := newTapClient(t.t, yara) - - // Mint an asset on Charlie and sync all nodes to Charlie as the - // universe. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - - t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) - t.Logf("Universes synced between all nodes, distributing assets...") - - const ( - daveFundingAmount = uint64(400_000) - erinFundingAmount = uint64(200_000) - ) - charlieFundingAmount := cents.Amount - uint64(2*400_000) - - _, _, chanPointEF := createTestAssetNetwork( - t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, - universeTap, cents, 400_000, charlieFundingAmount, - daveFundingAmount, erinFundingAmount, 0, - ) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) - - logBalance(t.t, nodes, assetID, "initial") - - // We now deplete the channel between Erin and Fabia by moving all - // assets to Fabia. - sendAssetKeySendPayment( - t.t, erin, fabia, erinFundingAmount, assetID, fn.None[int64](), - ) - logBalance(t.t, nodes, assetID, "after moving assets to Fabia") - - // Test case 1: We cannot keysend more assets from Erin to Fabia. - sendAssetKeySendPayment( - t.t, erin, fabia, 1, assetID, fn.None[int64](), - withFailure(lnrpc.Payment_FAILED, failureNoBalance), - ) - - // Test case 2: We cannot pay an invoice from Charlie to Fabia. - invoiceResp := createAssetInvoice(t.t, erin, fabia, 123, assetID) - payInvoiceWithSatoshi( - t.t, charlie, invoiceResp, - withFailure(lnrpc.Payment_FAILED, failureNoRoute), - ) - - // Test case 3: We now create an asset buy order for a normal amount of - // assets. We then "fake" an invoice referencing that buy order that - // is for an amount that is too small to be paid with a single asset - // unit. This should be handled gracefully and not lead to a crash. - // Ideally such an invoice shouldn't be created in the first place, but - // we want to make sure that the system doesn't crash in this case. - numUnits := uint64(10) - buyOrderResp, err := fabiaTap.RfqClient.AddAssetBuyOrder( - ctx, &rfqrpc.AddAssetBuyOrderRequest{ - AssetSpecifier: &rfqrpc.AssetSpecifier{ - Id: &rfqrpc.AssetSpecifier_AssetId{ - AssetId: assetID, - }, - }, - AssetMaxAmt: numUnits, - Expiry: uint64( - time.Now().Add(time.Hour).Unix(), - ), - PeerPubKey: erin.PubKey[:], - TimeoutSeconds: 10, - }, - ) - require.NoError(t.t, err) - - quoteResp := buyOrderResp.Response - quote, ok := quoteResp.(*rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote) - require.True(t.t, ok) - - // We calculate the milli-satoshi amount one below the equivalent of a - // single asset unit. - rate, err := rpcutils.UnmarshalFixedPoint(&oraclerpc.FixedPoint{ - Coefficient: quote.AcceptedQuote.AskAssetRate.Coefficient, - Scale: quote.AcceptedQuote.AskAssetRate.Scale, - }) - require.NoError(t.t, err) - - oneUnit := uint64(1) - oneUnitFP := rfqmath.NewBigIntFixedPoint(oneUnit, 0) - oneUnitMilliSat := rfqmath.UnitsToMilliSatoshi(oneUnitFP, *rate) - - t.Logf("Got quote for %v asset units per BTC", rate) - msatPerUnit := float64(oneUnitMilliSat) / float64(oneUnit) - t.Logf("Got quote for %v asset units at %3f msat/unit from peer %s "+ - "with SCID %d", numUnits, msatPerUnit, erin.PubKeyStr, - quote.AcceptedQuote.Scid) - - // We now manually add the invoice in order to inject the above, - // manually generated, quote. - hopHint := &lnrpc.HopHint{ - NodeId: erin.PubKeyStr, - ChanId: quote.AcceptedQuote.Scid, - CltvExpiryDelta: 80, - FeeBaseMsat: 1000, - FeeProportionalMillionths: 1, - } - invoiceResp2, err := fabia.AddInvoice(ctx, &lnrpc.Invoice{ - Memo: "too small invoice", - ValueMsat: int64(oneUnitMilliSat - 1), - RouteHints: []*lnrpc.RouteHint{{ - HopHints: []*lnrpc.HopHint{hopHint}, - }}, - }) - require.NoError(t.t, err) - - payInvoiceWithSatoshi(t.t, dave, invoiceResp2, withFailure( - lnrpc.Payment_FAILED, failureNoRoute, - )) - - // Let's make sure we can still use the channel between Erin and Fabia - // by doing a satoshi keysend payment. - sendKeySendPayment(t.t, erin, fabia, 2000) - logBalance(t.t, nodes, assetID, "after BTC only keysend") - - // Finally, we close the channel between Erin and Fabia to make sure - // everything is settled correctly. - closeAssetChannelAndAssert( - t, net, erin, fabia, chanPointEF, [][]byte{assetID}, nil, - universeTap, noOpCoOpCloseBalanceCheck, - ) -} - -// testCustomChannelsDecodeAssetInvoice tests that we're able to properly -// decode and display asset invoice related information. -func testCustomChannelsDecodeAssetInvoice(ctx context.Context, - net *NetworkHarness, t *harnessTest) { - - // First, we'll set up some information for our custom oracle that we'll - // use to feed in price information. - oracleAddr := fmt.Sprintf("localhost:%d", port.NextAvailablePort()) - oracle := newOracleHarness(oracleAddr) - oracle.start(t.t) - t.t.Cleanup(oracle.stop) - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplateNoOracle) - litdArgs = append(litdArgs, - "--taproot-assets.experimental.rfq.priceoracletlsinsecure", - fmt.Sprintf( - "--taproot-assets.experimental.rfq.priceoracleaddress="+ - "rfqrpc://%s", oracleAddr, - )) - - // We'll just make a single node here, as this doesn't actually rely on - // a set of active channels. - alice, err := net.NewNode( - t.t, "Alice", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - aliceTap := newTapClient(t.t, alice) - - // Fund Alice so she'll have enough funds to mint the asset. - fundAllNodes(t.t, net, []*HarnessNode{alice}) - - // Next, we'll make a new asset with a specified decimal display. We'll - // also make grouped asset as well. - usdMetaData := &taprpc.AssetMeta{ - Data: []byte(`{ -"description":"this is a USD stablecoin with decimal display of 6" -}`), - Type: taprpc.AssetMetaType_META_TYPE_JSON, - } - - const decimalDisplay = 6 - tcAsset := &mintrpc.MintAsset{ - AssetType: taprpc.AssetType_NORMAL, - Name: "USD", - AssetMeta: usdMetaData, - // We mint 1 million USD with a decimal display of 6, which - // results in 1 trillion asset units. - Amount: 1_000_000_000_000, - DecimalDisplay: decimalDisplay, - NewGroupedAsset: true, - } - - // Mint an asset on Charlie and sync Dave to Charlie as the universe. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, aliceTap, - []*mintrpc.MintAssetRequest{ - { - Asset: tcAsset, - }, - }, - ) - usdAsset := mintedAssets[0] - assetID := usdAsset.AssetGenesis.AssetId - - // Now that we've minted the asset, we can set the price in the oracle. - var id asset.ID - copy(id[:], assetID) - - // We'll assume a price of $100,000.00 USD for a single BTC. This is - // just the current subjective price our oracle will use. From this BTC - // price, we'll scale things up to be in the precision of the asset we - // minted above. - btcPrice := rfqmath.NewBigIntFixedPoint( - 100_000_00, 2, - ) - factor := rfqmath.NewBigInt( - big.NewInt(int64(math.Pow10(decimalDisplay))), - ) - btcPrice.Coefficient = btcPrice.Coefficient.Mul(factor) - oracle.setPrice(id, btcPrice, btcPrice) - - // Now we'll make a normal invoice for 1 BTC using Alice. - expirySeconds := 10 - amountSat := 100_000_000 - invoiceResp, err := alice.AddInvoice(ctx, &lnrpc.Invoice{ - Value: int64(amountSat), - Memo: "normal invoice", - Expiry: int64(expirySeconds), - }) - require.NoError(t.t, err) - - payReq := invoiceResp.PaymentRequest - - // Now that we have our payment request, we'll call into the new decode - // asset pay req call. - decodeResp, err := aliceTap.DecodeAssetPayReq(ctx, &tchrpc.AssetPayReq{ - AssetId: assetID, - PayReqString: payReq, - }) - require.NoError(t.t, err) - - // The decimal display information, genesis, and asset group information - // should all match. - require.EqualValues( - t.t, decimalDisplay, decodeResp.DecimalDisplay.DecimalDisplay, - ) - require.Equal(t.t, usdAsset.AssetGenesis, decodeResp.GenesisInfo) - require.Equal(t.t, usdAsset.AssetGroup, decodeResp.AssetGroup) - - // The 1 BTC invoice should map to 100k asset units, with decimal - // display 6 that's 100 billion asset units. - const expectedUnits = 100_000_000_000 - require.Equal(t.t, int64(expectedUnits), int64(decodeResp.AssetAmount)) - - // We do the same call again, but this time using the group key for the - // decoding query. - decodeResp2, err := aliceTap.DecodeAssetPayReq(ctx, &tchrpc.AssetPayReq{ - GroupKey: usdAsset.AssetGroup.TweakedGroupKey, - PayReqString: payReq, - }) - require.NoError(t.t, err) - - require.Equal(t.t, decodeResp.AssetAmount, decodeResp2.AssetAmount) - require.Equal(t.t, decodeResp.AssetGroup, decodeResp2.AssetGroup) - require.Equal( - t.t, decodeResp.DecimalDisplay, decodeResp2.DecimalDisplay, - ) -} - -// testCustomChannelsSelfPayment tests that circular self-payments can be made -// to re-balance between BTC and assets. -func testCustomChannelsSelfPayment(ctx context.Context, net *NetworkHarness, - t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Alice as the proof courier. But in order for Alice to also - // use itself, we need to define its port upfront. - alicePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, alicePort), - )) - - // Next, we'll make Alice and Bob, who will be the main nodes under - // test. - alice, err := net.NewNodeWithPort( - t.t, "Alice", lndArgs, false, true, alicePort, litdArgs..., - ) - require.NoError(t.t, err) - bob, err := net.NewNode( - t.t, "Bob", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - // Now we'll connect all nodes, and also fund them with some coins. - nodes := []*HarnessNode{alice, bob} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - aliceTap := newTapClient(t.t, alice) - - // Next, we'll mint an asset for Alice, who will be the node that opens - // the channel outbound. - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, aliceTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets[0] - assetID := cents.AssetGenesis.AssetId - - t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, aliceTap, bob) - t.Logf("Universes synced between all nodes, distributing assets...") - - // With the assets created, and synced -- we'll now open the channel - // between Alice and Bob. - t.Logf("Opening asset channel...") - assetFundResp, err := aliceTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetID, - PeerPubkey: bob.PubKey[:], - FeeRateSatPerVbyte: 5, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded asset channel between Alice and Bob: %v", assetFundResp) - - assetChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(assetFundResp.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: assetFundResp.Txid, - }, - } - - // With the channel open, mine a block to confirm it. - mineBlocks(t, net, 6, 1) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(alice, bob)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(bob, alice)) - - t.Logf("Opening normal channel between Alice and Bob...") - satChanPoint := openChannelAndAssert( - t, net, alice, bob, lntest.OpenChannelParams{ - Amt: 10_000_000, - SatPerVByte: 5, - }, - ) - defer closeChannelAndAssert(t, net, alice, satChanPoint, false) - - assetChan := fetchChannel(t.t, alice, assetChanPoint) - assetChanSCID := assetChan.ChanId - satChan := fetchChannel(t.t, alice, satChanPoint) - satChanSCID := satChan.ChanId - - t.Logf("Alice pubkey: %x", alice.PubKey[:]) - t.Logf("Bob pubkey: %x", bob.PubKey[:]) - t.Logf("Outgoing channel SCID: %d", satChanSCID) - logBalance(t.t, nodes, assetID, "initial") - - t.Logf("Key sending 15k assets from Alice to Bob...") - const ( - assetKeySendAmount = 15_000 - numInvoicePayments = 10 - assetInvoiceAmount = 1_234 - btcInvoiceAmount = 10_000 - btcKeySendAmount = 200_000 - btcReserveAmount = 2000 - btcHtlcCost = numInvoicePayments * 354 - ) - sendAssetKeySendPayment( - t.t, alice, bob, assetKeySendAmount, assetID, - fn.Some[int64](btcReserveAmount+btcHtlcCost), - ) - - // We also send 200k sats from Alice to Bob, to make sure the BTC - // channel has liquidity in both directions. - sendKeySendPayment(t.t, alice, bob, btcKeySendAmount) - logBalance(t.t, nodes, assetID, "after keysend") - - // We now do a series of small payments. They should all succeed and the - // balances should be updated accordingly. - aliceAssetBalance := uint64(fundingAmount - assetKeySendAmount) - bobAssetBalance := uint64(assetKeySendAmount) - for i := 0; i < numInvoicePayments; i++ { - // The BTC balance of Alice before we start the payment. We - // expect that to go down by at least the invoice amount. - btcBalanceAliceBefore := fetchChannel( - t.t, alice, satChanPoint, - ).LocalBalance - - invoiceResp := createAssetInvoice( - t.t, bob, alice, assetInvoiceAmount, assetID, - ) - payInvoiceWithSatoshi( - t.t, alice, invoiceResp, withOutgoingChanIDs( - []uint64{satChanSCID}, - ), withAllowSelfPayment(), - ) - - logBalance( - t.t, nodes, assetID, - "after paying invoice "+strconv.Itoa(i), - ) - - // The accumulated delta from the rounding of multiple sends. - // We basically allow the balance to be off by one unit for each - // payment. - delta := float64(i + 1) - - // We now expect the channel balance to have decreased in the - // BTC channel and increased in the assets channel. - assertChannelAssetBalanceWithDelta( - t.t, alice, assetChanPoint, - aliceAssetBalance+assetInvoiceAmount, - bobAssetBalance-assetInvoiceAmount, delta, - ) - aliceAssetBalance += assetInvoiceAmount - bobAssetBalance -= assetInvoiceAmount - - btcBalanceAliceAfter := fetchChannel( - t.t, alice, satChanPoint, - ).LocalBalance - - // The difference between the two balances should be at least - // the invoice amount. - decodedInvoice, err := alice.DecodePayReq( - context.Background(), &lnrpc.PayReqString{ - PayReq: invoiceResp.PaymentRequest, - }, - ) - require.NoError(t.t, err) - require.GreaterOrEqual( - t.t, btcBalanceAliceBefore-btcBalanceAliceAfter, - decodedInvoice.NumSatoshis, - ) - } - - // We now do the opposite: We create a satoshi invoice on Alice and - // attempt to pay it with assets. - aliceAssetBalance, bobAssetBalance = channelAssetBalance( - t.t, alice, assetChanPoint, - ) - for i := 0; i < numInvoicePayments; i++ { - // The BTC balance of Alice before we start the payment. We - // expect that to go down by at least the invoice amount. - btcBalanceAliceBefore := fetchChannel( - t.t, alice, satChanPoint, - ).LocalBalance - - hopHint := &lnrpc.HopHint{ - NodeId: bob.PubKeyStr, - ChanId: satChan.PeerScidAlias, - CltvExpiryDelta: 80, - FeeBaseMsat: 1000, - FeeProportionalMillionths: 1, - } - invoiceResp := createNormalInvoice( - t.t, alice, btcInvoiceAmount, withRouteHints( - []*lnrpc.RouteHint{{ - HopHints: []*lnrpc.HopHint{hopHint}, - }}, - ), - ) - sentUnits, _ := payInvoiceWithAssets( - t.t, alice, bob, invoiceResp.PaymentRequest, assetID, - withAllowSelfPayment(), withOutgoingChanIDs( - []uint64{assetChanSCID}, - ), - ) - - logBalance( - t.t, nodes, assetID, - "after paying sat invoice "+strconv.Itoa(i), - ) - - // The accumulated delta from the rounding of multiple sends. - // We basically allow the balance to be off by one unit for each - // payment. - delta := float64(i + 1) - - // We now expect the channel balance to have increased in the - // BTC channel and decreased in the assets channel. - assertChannelAssetBalanceWithDelta( - t.t, alice, assetChanPoint, - aliceAssetBalance-sentUnits, - bobAssetBalance+sentUnits, delta, - ) - aliceAssetBalance -= sentUnits - bobAssetBalance += sentUnits - - btcBalanceAliceAfter := fetchChannel( - t.t, alice, satChanPoint, - ).LocalBalance - - // The difference between the two balances should be at least - // the invoice amount. - decodedInvoice, err := alice.DecodePayReq( - context.Background(), &lnrpc.PayReqString{ - PayReq: invoiceResp.PaymentRequest, - }, - ) - require.NoError(t.t, err) - require.GreaterOrEqual( - t.t, btcBalanceAliceAfter-btcBalanceAliceBefore, - decodedInvoice.NumSatoshis, - ) - } -} - -// testCustomChannelsMultiChannelPathfinding tests that multiple channels with -// different assets are properly considered when pathfinding for payments. -func testCustomChannelsMultiChannelPathfinding(ctx context.Context, - net *NetworkHarness, t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - // We use Alice as the proof courier. But in order for Alice to also - // use itself, we need to define its port upfront. - alicePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, alicePort), - )) - - // Next, we'll make Alice and Bob, who will be the main nodes under - // test. - alice, err := net.NewNodeWithPort( - t.t, "Alice", lndArgs, false, true, alicePort, litdArgs..., - ) - require.NoError(t.t, err) - bob, err := net.NewNode( - t.t, "Bob", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - charlie, err := net.NewNode( - t.t, "Charlie", lndArgs, false, true, litdArgs..., - ) - require.NoError(t.t, err) - - // Now we'll connect all nodes, and also fund them with some coins. - nodes := []*HarnessNode{alice, bob, charlie} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - aliceTap := newTapClient(t.t, alice) - - // Next, we'll mint an asset for Alice, who will be the node that opens - // the channel outbound. - mintedAssets1 := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, aliceTap, - []*mintrpc.MintAssetRequest{ - { - Asset: itestAsset, - }, - }, - ) - cents := mintedAssets1[0] - assetIDCents := cents.AssetGenesis.AssetId - - // We'll mint a second asset, representing british pences. - mintedAssets2 := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, aliceTap, - []*mintrpc.MintAssetRequest{ - { - Asset: &mintrpc.MintAsset{ - AssetType: taprpc.AssetType_NORMAL, - Name: "itest-asset-pences", - AssetMeta: dummyMetaData, - Amount: 1_000_000, - }, - }, - }, - ) - pences := mintedAssets2[0] - assetIDPences := pences.AssetGenesis.AssetId - - t.Logf("Minted %d lightning cents and %d lightning pences, syncing "+ - "universes...", cents.Amount, pences.Amount) - syncUniverses(t.t, aliceTap, bob) - t.Logf("Universes synced between all nodes, distributing assets...") - - // With the assets created, and synced -- we'll now open the channel - // between Alice and Bob. - t.Logf("Opening asset channel with cents...") - assetFundResp1, err := aliceTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetIDCents, - PeerPubkey: bob.PubKey[:], - FeeRateSatPerVbyte: 5, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded cents channel between Alice and Bob: %v", assetFundResp1) - - // With the channel open, mine a block to confirm it. - mineBlocks(t, net, 6, 1) - - t.Logf("Opening asset channel with pences...") - assetFundResp2, err := aliceTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - AssetId: assetIDPences, - PeerPubkey: bob.PubKey[:], - FeeRateSatPerVbyte: 5, - }, - ) - require.NoError(t.t, err) - t.Logf("Funded pences channel between Alice and Bob: %v", - assetFundResp2) - - // With the channel open, mine a block to confirm it. - mineBlocks(t, net, 6, 1) - - t.Logf("Opening normal channel between Bob and Charlie...") - satChanPoint := openChannelAndAssert( - t, net, bob, charlie, lntest.OpenChannelParams{ - Amt: 10_000_000, - SatPerVByte: 5, - }, - ) - defer closeChannelAndAssert(t, net, charlie, satChanPoint, false) - - // Before we start sending out payments, let's make sure each node can - // see the other one in the graph and has all required features. - require.NoError(t.t, t.lndHarness.AssertNodeKnown(alice, bob)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(bob, alice)) - require.NoError(t.t, t.lndHarness.AssertNodeKnown(alice, charlie)) - - // We now make sure that the balance of the cents channel is higher on - // Alice, by sending some of the pences to Bob in a keysend payment. - const pencesKeySendAmount = 5_000 - sendAssetKeySendPayment( - t.t, alice, bob, pencesKeySendAmount, assetIDPences, - fn.None[int64](), - ) - - logBalance(t.t, nodes, assetIDCents, "cents, after keysend pences") - logBalance(t.t, nodes, assetIDPences, "pences, after keysend pences") - - // We now create a normal invoice on Charlie for some amount, then try - // to pay it with pences. - const btcInvoiceAmount = 500_00 - invoiceResp := createNormalInvoice(t.t, charlie, btcInvoiceAmount) - payInvoiceWithAssets( - t.t, alice, bob, invoiceResp.PaymentRequest, assetIDPences, - ) -} - -// testCustomChannelsPassiveAssets tests that passive assets (assets -// in the same input commitment but not used for channel funding) are -// not included in the funding assets sent to the responder -func testCustomChannelsPassiveAssets(ctx context.Context, net *NetworkHarness, - t *harnessTest) { - - lndArgs := slices.Clone(lndArgsTemplate) - litdArgs := slices.Clone(litdArgsTemplate) - - charliePort := port.NextAvailablePort() - litdArgs = append(litdArgs, fmt.Sprintf( - "--taproot-assets.proofcourieraddr=%s://%s", - proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), - )) - - charlie, err := net.NewNodeWithPort( - t.t, "Charlie", lndArgs, false, true, charliePort, litdArgs..., - ) - require.NoError(t.t, err) - dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) - require.NoError(t.t, err) - - nodes := []*HarnessNode{charlie, dave} - connectAllNodes(t.t, net, nodes) - fundAllNodes(t.t, net, nodes) - - charlieTap := newTapClient(t.t, charlie) - daveTap := newTapClient(t.t, dave) - - // Mint two assets in the same batch with the same group key. This puts - // them in the same anchor UTXO/commitment. - assetA := &mintrpc.MintAsset{ - AssetType: taprpc.AssetType_NORMAL, - Name: "asset-a-for-channel", - AssetMeta: dummyMetaData, - Amount: 100_000, - NewGroupedAsset: true, - } - assetB := &mintrpc.MintAsset{ - AssetType: taprpc.AssetType_NORMAL, - Name: "asset-b-passive", - AssetMeta: dummyMetaData, - Amount: 50_000, - GroupedAsset: true, - GroupAnchor: "asset-a-for-channel", - } - mintedAssets := itest.MintAssetsConfirmBatch( - t.t, t.lndHarness.Miner.Client, charlieTap, - []*mintrpc.MintAssetRequest{ - {Asset: assetA}, - {Asset: assetB}, - }, - ) - require.Len(t.t, mintedAssets, 2) - - mintedA := mintedAssets[0] - mintedB := mintedAssets[1] - assetIDA := mintedA.AssetGenesis.AssetId - - // Both assets should have the same group key. - require.NotNil(t.t, mintedA.AssetGroup) - require.NotNil(t.t, mintedB.AssetGroup) - groupKey := mintedA.AssetGroup.TweakedGroupKey - require.Equal(t.t, groupKey, mintedB.AssetGroup.TweakedGroupKey) - - // Check if both assets share the same anchor outpoint (i.e. UTXO). - outpointA := mintedA.ChainAnchor.AnchorOutpoint - outpointB := mintedB.ChainAnchor.AnchorOutpoint - t.Logf("Asset A anchor outpoint: %s", outpointA) - t.Logf("Asset B anchor outpoint: %s", outpointB) - require.Equal(t.t, outpointA, outpointB, - "assets must be in same UTXO for passive asset test") - - syncUniverses(t.t, charlieTap, dave) - mineBlocks(t, net, 1, 0) - - // Fund a channel using the group key. We request 75,000 units which - // forces Asset A (100,000) to be selected since Asset B (50,000) is - // insufficient. Asset B should become passive and not appear in Dave's - // view of the channel. - const fundingAmount = 75_000 - fundResp, err := charlieTap.FundChannel( - ctx, &tchrpc.FundChannelRequest{ - AssetAmount: fundingAmount, - GroupKey: groupKey, - PeerPubkey: daveTap.node.PubKey[:], - FeeRateSatPerVbyte: 5, - PushSat: DefaultPushSat, - }, - ) - require.NoError(t.t, err) - - mineBlocks(t, net, 6, 1) - - // Assert Dave (responder) only sees Asset A in the channel, not the - // passive Asset B. Use wait.NoError to handle the race condition where - // the channel may not be immediately visible after mining. - var chanData *rfqmsg.JsonAssetChannel - err = wait.NoError(func() error { - var chanErr error - chanData, chanErr = getChannelCustomData(dave, charlie) - if chanErr != nil { - return chanErr - } - - // There should be exactly one funding asset (Asset A). - if len(chanData.FundingAssets) != 1 { - return fmt.Errorf("expected 1 funding asset, got %d", - len(chanData.FundingAssets)) - } - - // Verify it's Asset A, not Asset B. - fundingAssetID := chanData.FundingAssets[0].AssetGenesis.AssetID - if hex.EncodeToString(assetIDA) != fundingAssetID { - return fmt.Errorf("funding asset should be Asset A, "+ - "got %s", fundingAssetID) - } - - // Verify capacity matches funding amount, not inflated by - // Asset B. - if chanData.Capacity != uint64(fundingAmount) { - return fmt.Errorf("capacity should be %d, not "+ - "inflated by passive assets, got %d", - fundingAmount, chanData.Capacity) - } - - return nil - }, defaultTimeout) - require.NoError(t.t, err) - - chanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(fundResp.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: fundResp.Txid, - }, - } - closeAssetChannelAndAssert( - t, net, charlie, dave, chanPoint, [][]byte{assetIDA}, nil, - charlieTap, noOpCoOpCloseBalanceCheck, - ) -} diff --git a/itest/litd_node.go b/itest/litd_node.go index fe75d730..36c12cc2 100644 --- a/itest/litd_node.go +++ b/itest/litd_node.go @@ -68,26 +68,6 @@ var ( "litdbbackend", terminal.DatabaseBackendBbolt, "Set the "+ "database backend to use when starting a LiT daemon.", ) - - // backwardCompatFlagOverride is a map of LiT versions to a map of - // config file flags that need to be overwritten or added for the target - // version to work. If a flag in the map is empty, it means it will be - // removed from the config file of that version. - backwardCompatFlagOverride = map[string]map[string]string{ - "v0.14.1-alpha": { - "databasebackend": "", - "lnd.logging.no-commit-hash": "", - }, - } - - // backwardCompatConfigOverride is a map of LiT versions to a map of - // config file overriding functions. - backwardCompatConfigOverride = map[string]func(*LitNodeConfig){ - "v0.14.1-alpha": func(cfg *LitNodeConfig) { - cfg.OldChannelFormat = true - cfg.SkipBalanceChecks = true - }, - } ) // Option is a function for updating a node's configuration. @@ -123,14 +103,6 @@ type LitNodeConfig struct { LitPort int LitRESTPort int - // OldChannelFormat is a flag for turning off certain checks for old - // versions of litd during the backward compatibility test. - OldChannelFormat bool - - // SkipBalanceChecks is a flag for turning off certain checks for old - // versions of litd during the backward compatibility test. - SkipBalanceChecks bool - // backupDBDir is the path where a database backup is stored, if any. backupDBDir string } @@ -648,62 +620,19 @@ func renameFile(fromFileName, toFileName string) { } } -// overrideFlagsAndBinary is a helper function that checks if the passed node -// name needs a version downgrade and if so, it will return the new binary -// name and the new arguments. -func (hn *HarnessNode) overrideFlagsAndBinary(backwardCompat map[string]string, - binary string, args []LitArgOption) (string, []LitArgOption) { - - noopArg := WithLitArg("taproot-assets.channel.noop-htlcs", "") - - if _, ok := backwardCompat[hn.Name()]; !ok { - args = append(args, noopArg) - return binary, args - } - - downgradeVersion, ok := backwardCompat[hn.Cfg.Name] - if !ok { - return binary, args - } - - newBinary := fmt.Sprintf("%s-%s", binary, downgradeVersion) - - flagOverride, ok := backwardCompatFlagOverride[downgradeVersion] - if !ok { - return newBinary, args - } - - for k, v := range flagOverride { - if v == "" { - args = append(args, WithoutLitArg(k)) - } else { - args = append(args, WithLitArg(k, v)) - } - } - - cfgOverride, ok := backwardCompatConfigOverride[downgradeVersion] - if ok && cfgOverride != nil { - cfgOverride(hn.Cfg) - } - - return newBinary, args -} - // Start launches a new process running lnd. Additionally, the PID of the // launched process is saved in order to possibly kill the process forcibly // later. // // This may not clean up properly if an error is returned, so the caller should // call shutdown() regardless of the return value. -func (hn *HarnessNode) Start(litdBinary string, - backwardCompat map[string]string, litdError chan<- error, +func (hn *HarnessNode) Start(litdBinary string, litdError chan<- error, waitForStart bool, litArgOpts ...LitArgOption) error { hn.quit = make(chan struct{}) - litdBinary, litArgOpts = hn.overrideFlagsAndBinary( - backwardCompat, litdBinary, litArgOpts, - ) + noopArg := WithLitArg("taproot-assets.channel.noop-htlcs", "") + litArgOpts = append(litArgOpts, noopArg) args := hn.Cfg.GenArgs(litArgOpts...) hn.cmd = exec.Command(litdBinary, args...) diff --git a/itest/litd_test.go b/itest/litd_test.go index ef80c1d7..7dac3380 100644 --- a/itest/litd_test.go +++ b/itest/litd_test.go @@ -120,8 +120,8 @@ func TestLightningTerminal(t *testing.T) { lndSubTest := lndHarness.Subtest(t1) litdHarness, err := NewNetworkHarness( - lndSubTest, chainBackend, binary, feeService, - testCase.backwardCompat, + lndSubTest, chainBackend, binary, + feeService, ) require.NoError(t1, err) diff --git a/itest/litd_test_list_on_test.go b/itest/litd_test_list_on_test.go index dbb1b8ef..b339eea6 100644 --- a/itest/litd_test_list_on_test.go +++ b/itest/litd_test_list_on_test.go @@ -27,143 +27,4 @@ var allTestCases = []*testCase{ name: "terminal large http header", test: testLargeHttpHeader, }, - { - name: "custom channels", - test: testCustomChannels, - noAliceBob: true, - }, - { - name: "custom channels backward compatibility", - test: testCustomChannels, - noAliceBob: true, - backwardCompat: map[string]string{ - "Dave": "v0.14.1-alpha", - "Fabia": "v0.14.1-alpha", - }, - }, - { - name: "custom channels large", - test: testCustomChannelsLarge, - noAliceBob: true, - }, - { - name: "custom channels grouped asset", - test: testCustomChannelsGroupedAsset, - noAliceBob: true, - }, - { - name: "custom channels group tranches force close", - test: testCustomChannelsGroupTranchesForceClose, - noAliceBob: true, - }, - { - name: "custom channels group tranches htlc force close", - test: testCustomChannelsGroupTranchesHtlcForceClose, - noAliceBob: true, - }, - { - name: "custom channels force close", - test: testCustomChannelsForceClose, - noAliceBob: true, - }, - { - name: "custom channels breach", - test: testCustomChannelsBreach, - noAliceBob: true, - }, - { - name: "custom channels liquidity", - test: testCustomChannelsLiquidityEdgeCases, - noAliceBob: true, - }, - { - name: "custom channels liquidity group", - test: testCustomChannelsLiquidityEdgeCasesGroup, - noAliceBob: true, - }, - { - name: "custom channels htlc force close", - test: testCustomChannelsHtlcForceClose, - noAliceBob: true, - }, - { - name: "custom channels htlc force close MPP", - test: testCustomChannelsHtlcForceCloseMpp, - noAliceBob: true, - }, - { - name: "custom channels balance consistency", - test: testCustomChannelsBalanceConsistency, - noAliceBob: true, - }, - { - name: "custom channels single asset multi input", - test: testCustomChannelsSingleAssetMultiInput, - noAliceBob: true, - }, - { - name: "custom channels passive assets", - test: testCustomChannelsPassiveAssets, - noAliceBob: true, - }, - { - name: "custom channels oracle pricing", - test: testCustomChannelsOraclePricing, - noAliceBob: true, - }, - { - name: "custom channels fee", - test: testCustomChannelsFee, - noAliceBob: true, - }, - { - name: "custom channels forward bandwidth", - test: testCustomChannelsForwardBandwidth, - noAliceBob: true, - }, - { - name: "custom channels strict forwarding", - test: testCustomChannelsStrictForwarding, - noAliceBob: true, - }, - { - name: "custom channels decode payreq", - test: testCustomChannelsDecodeAssetInvoice, - noAliceBob: true, - }, - { - name: "custom channels self-payment", - test: testCustomChannelsSelfPayment, - noAliceBob: true, - }, - { - name: "custom channels multi rfq", - test: testCustomChannelsMultiRFQ, - noAliceBob: true, - backwardCompat: map[string]string{ - "Yara": "v0.15.0-alpha", - }, - }, - { - name: "custom channels multi channel pathfinding", - test: testCustomChannelsMultiChannelPathfinding, - noAliceBob: true, - }, - { - name: "custom channels self-payment backward " + - "compatibility", - test: testCustomChannelsSelfPayment, - noAliceBob: true, - backwardCompat: map[string]string{ - "Alice": "v0.14.1-alpha", - }, - }, - { - name: "custom channels v1 upgrade", - test: testCustomChannelsV1Upgrade, - noAliceBob: true, - backwardCompat: map[string]string{ - "Charlie": "v0.15.0-alpha", - }, - }, } diff --git a/itest/network_harness.go b/itest/network_harness.go index 347f597c..7be3d5a6 100644 --- a/itest/network_harness.go +++ b/itest/network_harness.go @@ -69,11 +69,6 @@ type NetworkHarness struct { Alice *HarnessNode Bob *HarnessNode - // backwardCompat is a map of node names to the version of litd that - // should be used for them. If the map is empty, then the latest/current - // version will be used for all nodes. - backwardCompat map[string]string - // Channel for transmitting stderr output from failed lightning node // to main process. lndErrorChan chan error @@ -83,20 +78,19 @@ type NetworkHarness struct { // NewNetworkHarness creates a new network test harness. func NewNetworkHarness(lndHarness *lntest.HarnessTest, b node.BackendConfig, - litdBinary string, feeService lntest.WebFeeService, - backwardCompat map[string]string) (*NetworkHarness, error) { + litdBinary string, + feeService lntest.WebFeeService) (*NetworkHarness, error) { n := NetworkHarness{ - activeNodes: make(map[int]*HarnessNode), - nodesByPub: make(map[string]*HarnessNode), - lndErrorChan: make(chan error), - netParams: lndHarness.Miner().ActiveNet, - Miner: lndHarness.Miner(), - LNDHarness: lndHarness, - BackendCfg: b, - litdBinary: litdBinary, - feeService: feeService, - backwardCompat: backwardCompat, + activeNodes: make(map[int]*HarnessNode), + nodesByPub: make(map[string]*HarnessNode), + lndErrorChan: make(chan error), + netParams: lndHarness.Miner().ActiveNet, + Miner: lndHarness.Miner(), + LNDHarness: lndHarness, + BackendCfg: b, + litdBinary: litdBinary, + feeService: feeService, } return &n, nil } @@ -377,7 +371,7 @@ func (n *NetworkHarness) newNode(t *testing.T, name string, extraArgs, n.activeNodes[node.NodeID] = node n.mtx.Unlock() - err = node.Start(n.litdBinary, n.backwardCompat, n.lndErrorChan, wait) + err = node.Start(n.litdBinary, n.lndErrorChan, wait) if err != nil { return nil, err } @@ -778,72 +772,22 @@ func (n *NetworkHarness) RestartNodeNoUnlock(node *HarnessNode, } return node.Start( - n.litdBinary, n.backwardCompat, n.lndErrorChan, wait, - litArgOpts..., + n.litdBinary, n.lndErrorChan, wait, litArgOpts..., ) } -// suspendCfg contains any configurations related to suspending a node. This may -// change the way the node starts up again. -type suspendCfg struct { - upgrade bool - downgrade string -} - -// SuspendOption is a functional option that may change the suspend -// configuration. -type SuspendOption func(cfg *suspendCfg) - -// WithUpgrade is a functional option which indicates that the node should be -// upgraded to the latest version of LiT. -func WithUpgrade() SuspendOption { - return func(cfg *suspendCfg) { - cfg.upgrade = true - } -} - -// WithDowngrade is a functional option which indicates that the node should be -// downgraded to the defined LiT version. -func WithDowngrade(version string) SuspendOption { - return func(cfg *suspendCfg) { - cfg.downgrade = version - } -} - // SuspendNode stops the given node and returns a callback that can be used to -// start it again. If the upgrade flag is set then any backwards compatibility -// version that the node was running previously will be eliminated, forcing it -// to use the current (latest) build. -func (n *NetworkHarness) SuspendNode(node *HarnessNode, - opts ...SuspendOption) (func() error, error) { +// start it again. +func (n *NetworkHarness) SuspendNode( + node *HarnessNode) (func() error, error) { if err := node.Stop(); err != nil { return nil, err } - cfg := &suspendCfg{} - - for _, opt := range opts { - opt(cfg) - } - - // If the upgrade flag was set, delete any entry from the backwards - // compatibility map. This will force the node to use the latest build - // of LiT. - if cfg.upgrade { - delete(n.backwardCompat, node.Name()) - } - - // If a downgrade version was defined, update the backwards - // compatibility entry with that version for this LiT node. This will - // force the node to start up using the defined version. - if cfg.downgrade != "" { - n.backwardCompat[node.Name()] = cfg.downgrade - } - restart := func() error { return node.Start( - n.litdBinary, n.backwardCompat, n.lndErrorChan, true, + n.litdBinary, n.lndErrorChan, true, ) } @@ -879,10 +823,8 @@ func (n *NetworkHarness) StopNode(node *HarnessNode) error { } // StopAndBackupDB backs up the database of the target node. -func (n *NetworkHarness) StopAndBackupDB(node *HarnessNode, - opts ...SuspendOption) error { - - restart, err := n.SuspendNode(node, opts...) +func (n *NetworkHarness) StopAndBackupDB(node *HarnessNode) error { + restart, err := n.SuspendNode(node) if err != nil { return err } @@ -906,10 +848,8 @@ func (n *NetworkHarness) StopAndBackupDB(node *HarnessNode, // StopAndRestoreDB stops the target node, restores the database from a backup // and starts the node again. -func (n *NetworkHarness) StopAndRestoreDB(node *HarnessNode, - opts ...SuspendOption) error { - - restart, err := n.SuspendNode(node, opts...) +func (n *NetworkHarness) StopAndRestoreDB(node *HarnessNode) error { + restart, err := n.SuspendNode(node) if err != nil { return err } diff --git a/itest/oracle_test.go b/itest/oracle_test.go deleted file mode 100644 index 1d8ff543..00000000 --- a/itest/oracle_test.go +++ /dev/null @@ -1,280 +0,0 @@ -package itest - -import ( - "context" - "crypto/tls" - "encoding/hex" - "fmt" - "net" - "testing" - "time" - - "github.com/lightninglabs/taproot-assets/asset" - "github.com/lightninglabs/taproot-assets/rfqmath" - "github.com/lightninglabs/taproot-assets/rfqmsg" - "github.com/lightninglabs/taproot-assets/rpcutils" - oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" - "github.com/lightningnetwork/lnd/cert" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" -) - -// oracleHarness is a basic integration test RPC price oracle server harness. -type oracleHarness struct { - oraclerpc.UnimplementedPriceOracleServer - - listenAddr string - - grpcListener net.Listener - grpcServer *grpc.Server - - purchasePrices map[asset.ID]rfqmath.BigIntFixedPoint - salePrices map[asset.ID]rfqmath.BigIntFixedPoint -} - -func newOracleHarness(listenAddr string) *oracleHarness { - return &oracleHarness{ - listenAddr: listenAddr, - purchasePrices: make(map[asset.ID]rfqmath.BigIntFixedPoint), - salePrices: make(map[asset.ID]rfqmath.BigIntFixedPoint), - } -} - -func (o *oracleHarness) setPrice(assetID asset.ID, purchasePrice, - salePrice rfqmath.BigIntFixedPoint) { - - o.purchasePrices[assetID] = purchasePrice - o.salePrices[assetID] = salePrice -} - -func (o *oracleHarness) start(t *testing.T) { - // Start the mock RPC price oracle service. - // - // Generate self-signed certificate. This allows us to use TLS for the - // gRPC server. - tlsCert, err := generateSelfSignedCert() - require.NoError(t, err) - - // Create the gRPC server with TLS - transportCredentials := credentials.NewTLS(&tls.Config{ - Certificates: []tls.Certificate{tlsCert}, - }) - o.grpcServer = grpc.NewServer(grpc.Creds(transportCredentials)) - - serviceAddr := fmt.Sprintf("rfqrpc://%s", o.listenAddr) - log.Infof("Starting RPC price oracle service at address: %s\n", - serviceAddr) - - oraclerpc.RegisterPriceOracleServer(o.grpcServer, o) - - go func() { - var err error - o.grpcListener, err = net.Listen("tcp", o.listenAddr) - if err != nil { - log.Errorf("Error oracle listening: %v", err) - return - } - if err := o.grpcServer.Serve(o.grpcListener); err != nil { - log.Errorf("Error oracle serving: %v", err) - } - }() -} - -func (o *oracleHarness) stop() { - if o.grpcServer != nil { - o.grpcServer.Stop() - } - if o.grpcListener != nil { - _ = o.grpcListener.Close() - } -} - -// getAssetRates returns the asset rates for a given transaction type and -// subject asset max amount. -func (o *oracleHarness) getAssetRates(id asset.ID, - transactionType oraclerpc.TransactionType) (oraclerpc.AssetRates, - error) { - - // Determine the rate based on the transaction type. - var subjectAssetRate rfqmath.BigIntFixedPoint - if transactionType == oraclerpc.TransactionType_PURCHASE { - rate, ok := o.purchasePrices[id] - if !ok { - return oraclerpc.AssetRates{}, fmt.Errorf("purchase "+ - "price not found for asset ID=%v", id) - } - subjectAssetRate = rate - } else { - rate, ok := o.salePrices[id] - if !ok { - return oraclerpc.AssetRates{}, fmt.Errorf("sale "+ - "price not found for asset ID=%v", id) - } - subjectAssetRate = rate - } - - // Marshal subject asset rate to RPC format. - rpcSubjectAssetToBtcRate, err := rpcutils.MarshalBigIntFixedPoint( - subjectAssetRate, - ) - if err != nil { - return oraclerpc.AssetRates{}, err - } - - // Marshal payment asset rate to RPC format. - rpcPaymentAssetToBtcRate, err := rpcutils.MarshalBigIntFixedPoint( - rfqmsg.MilliSatPerBtc, - ) - if err != nil { - return oraclerpc.AssetRates{}, err - } - - expiry := time.Now().Add(5 * time.Minute).Unix() - return oraclerpc.AssetRates{ - SubjectAssetRate: rpcSubjectAssetToBtcRate, - PaymentAssetRate: rpcPaymentAssetToBtcRate, - ExpiryTimestamp: uint64(expiry), - }, nil -} - -// QueryAssetRates queries the asset rates for a given transaction type, subject -// asset, and payment asset. An asset rate is the number of asset units per -// BTC. -// -// Example use case: -// -// Alice is trying to pay an invoice by spending an asset. Alice therefore -// requests that Bob (her asset channel counterparty) purchase the asset from -// her. Bob's payment, in BTC, will pay the invoice. -// -// Alice requests a bid quote from Bob. Her request includes an asset rates hint -// (ask). Alice obtains the asset rates hint by calling this endpoint. She sets: -// - `SubjectAsset` to the asset she is trying to sell. -// - `SubjectAssetMaxAmount` to the max channel asset outbound. -// - `PaymentAsset` to BTC. -// - `TransactionType` to SALE. -// - `AssetRateHint` to nil. -// -// Bob calls this endpoint to get the bid quote asset rates that he will send as -// a response to Alice's request. He sets: -// - `SubjectAsset` to the asset that Alice is trying to sell. -// - `SubjectAssetMaxAmount` to the value given in Alice's quote request. -// - `PaymentAsset` to BTC. -// - `TransactionType` to PURCHASE. -// - `AssetRateHint` to the value given in Alice's quote request. -func (o *oracleHarness) QueryAssetRates(_ context.Context, - req *oraclerpc.QueryAssetRatesRequest) ( - *oraclerpc.QueryAssetRatesResponse, error) { - - // Ensure that the payment asset is BTC. We only support BTC as the - // payment asset in this example. - if !rpcutils.IsAssetBtc(req.PaymentAsset) { - log.Infof("Payment asset is not BTC: %v", req.PaymentAsset) - - return &oraclerpc.QueryAssetRatesResponse{ - Result: &oraclerpc.QueryAssetRatesResponse_Error{ - Error: &oraclerpc.QueryAssetRatesErrResponse{ - Message: "unsupported payment asset, " + - "only BTC is supported", - }, - }, - }, nil - } - - // Ensure that the subject asset is set correctly. - subjectAssetID, err := parseSubjectAsset(req.SubjectAsset) - if err != nil { - log.Errorf("Error parsing subject asset: %v", err) - return nil, fmt.Errorf("error parsing subject asset: %w", err) - } - - _, hasPurchase := o.purchasePrices[subjectAssetID] - _, hasSale := o.salePrices[subjectAssetID] - - log.Infof("Have for asset=%x, purchase=%v, sale=%v", subjectAssetID[:], - hasPurchase, hasSale) - - // Ensure that the subject asset is supported. - if !hasPurchase || !hasSale { - log.Infof("Unsupported subject asset ID str: %v\n", - req.SubjectAsset) - - return &oraclerpc.QueryAssetRatesResponse{ - Result: &oraclerpc.QueryAssetRatesResponse_Error{ - Error: &oraclerpc.QueryAssetRatesErrResponse{ - Message: "unsupported subject asset", - }, - }, - }, nil - } - - assetRates, err := o.getAssetRates(subjectAssetID, req.TransactionType) - if err != nil { - return nil, err - } - - log.Infof("QueryAssetRates returning rates (subject_asset_rate=%v, "+ - "payment_asset_rate=%v)", assetRates.SubjectAssetRate, - assetRates.PaymentAssetRate) - - return &oraclerpc.QueryAssetRatesResponse{ - Result: &oraclerpc.QueryAssetRatesResponse_Ok{ - Ok: &oraclerpc.QueryAssetRatesOkResponse{ - AssetRates: &assetRates, - }, - }, - }, nil -} - -// parseSubjectAsset parses the subject asset from the given asset specifier. -func parseSubjectAsset(subjectAsset *oraclerpc.AssetSpecifier) (asset.ID, - error) { - - // Ensure that the subject asset is set. - if subjectAsset == nil { - return asset.ID{}, fmt.Errorf("subject asset is not set (nil)") - } - - // Check the subject asset bytes if set. - var subjectAssetID asset.ID - switch { - case len(subjectAsset.GetAssetId()) > 0: - copy(subjectAssetID[:], subjectAsset.GetAssetId()) - - case len(subjectAsset.GetAssetIdStr()) > 0: - assetIDBytes, err := hex.DecodeString( - subjectAsset.GetAssetIdStr(), - ) - if err != nil { - return asset.ID{}, fmt.Errorf("error decoding asset "+ - "ID hex string: %w", err) - } - - copy(subjectAssetID[:], assetIDBytes) - - default: - return asset.ID{}, fmt.Errorf("subject asset ID bytes and ID " + - "str not set") - } - - return subjectAssetID, nil -} - -// generateSelfSignedCert generates a self-signed TLS certificate and private -// key. -func generateSelfSignedCert() (tls.Certificate, error) { - certBytes, keyBytes, err := cert.GenCertPair( - "itest price oracle", nil, nil, false, 24*time.Hour, - ) - if err != nil { - return tls.Certificate{}, err - } - - tlsCert, err := tls.X509KeyPair(certBytes, keyBytes) - if err != nil { - return tls.Certificate{}, err - } - - return tlsCert, nil -} diff --git a/itest/test_harness.go b/itest/test_harness.go index 77a43cce..d1286eff 100644 --- a/itest/test_harness.go +++ b/itest/test_harness.go @@ -129,12 +129,6 @@ type testCase struct { test func(ctx context.Context, net *NetworkHarness, t *harnessTest) noAliceBob bool - - // backwardCompat is a map of node names to the version they are going - // to be downgraded to during the specific test. Make sure to update the - // LITD_COMPAT_VERSIONS variable in the Makefile to include all versions - // used for any backward compatibility tests. - backwardCompat map[string]string } // waitForNTxsInMempool polls until finding the desired number of transactions diff --git a/scripts/check-go-version-dockerfile.sh b/scripts/check-go-version-dockerfile.sh index 7aaa74e2..e21b5fec 100755 --- a/scripts/check-go-version-dockerfile.sh +++ b/scripts/check-go-version-dockerfile.sh @@ -34,7 +34,6 @@ exception_list=( # Exclude the tools Dockerfile as otherwise the linter may need to be # considered every time the Go version is updated. "./tools/Dockerfile" - "./itest/backward-compat" ) # is_exception checks if a file is in the exception list. diff --git a/scripts/check-go-version-yaml.sh b/scripts/check-go-version-yaml.sh index f1e69a2d..09ab1267 100755 --- a/scripts/check-go-version-yaml.sh +++ b/scripts/check-go-version-yaml.sh @@ -64,9 +64,7 @@ fi target_go_version="$1" # File paths to be excluded from the check. -exception_list=( - "./itest/backward-compat" -) +exception_list=() # is_exception checks if a file is in the exception list. is_exception() { diff --git a/scripts/install-backward-compat-versions.sh b/scripts/install-backward-compat-versions.sh deleted file mode 100755 index a5c72244..00000000 --- a/scripts/install-backward-compat-versions.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/bash - -# Build and install older versions of `litd` for backward compatibility -# integration tests. -# -# Usage: -# ./scripts/install-backward-compat-versions.sh " ..." -# -# Example: -# ./scripts/install-backward-compat-versions.sh "v0.12.0-beta v0.14.1-alpha" -# -# Parameters: -# $1 Space-separated list of `litd` versions to install (required). -# -# Environment Variables: -# REPO_URL URL of the litd repository (defaults to: -# https://github.com/lightninglabs/lightning-terminal.git) -# -# Notes: -# - Each version will be installed under a version-specific path. - -set -ex - -VERSIONS="$1" -REPO_URL="${REPO_URL:-https://github.com/lightninglabs/lightning-terminal.git}" - -# Make sure at least one version is set. -if [[ -z "${VERSIONS}" ]]; then - echo "Usage: $0 ' ...'" - echo "Please provide at least one version to install." - exit 1 -fi - -# Directory of the script file, independent of where it's called from. -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -ITEST_DIR="${DIR}/../itest" -TARGET_DIR="${ITEST_DIR}/backward-compat" - -mkdir -p "${TARGET_DIR}" && cd "${TARGET_DIR}" - -# First, make sure we've cloned each version of litd. -for VERSION in ${VERSIONS}; do - VERSION_DIR="${TARGET_DIR}/litd-${VERSION}" - - # Check if the version is already installed. - if [[ -f "${VERSION_DIR}/Makefile" ]]; then - echo "Version ${VERSION} is already installed." - continue - fi - - # Clone the specified version of litd. - echo "Installing litd ${VERSION} to ${VERSION_DIR}..." - git clone ${REPO_URL} --depth 1 -b "$VERSION" "${VERSION_DIR}" -done - -# Build the itest binary for each version. -for VERSION in ${VERSIONS}; do - VERSION_DIR="${TARGET_DIR}/litd-${VERSION}" - SRC="${VERSION_DIR}/itest/litd-itest" - DST="${ITEST_DIR}/litd-itest-${VERSION}" - - if [[ -f "${DST}" ]]; then - echo "Binary ${DST} is already available, skipping build." - continue - fi - - echo "Building litd ${VERSION}..." - cd "${VERSION_DIR}" - - # The Makefile was changed a bit, but the "itest-only" target that skips the - # frontend build has existed for a while. We use it to build the itest binary - # (but don't actually run the tests). - make itest-only icase=nothing - - # Copy the resulting binary to the main itest directory. - echo "Copying ${SRC} to ${DST}" - cp "${SRC}" "${DST}" -done -