From 128e6829882bcfe60ced0f92287d875129afe86d Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 9 Oct 2023 15:06:01 +0200 Subject: [PATCH] loadtest: add send test --- itest/loadtest/config.go | 22 +++++- itest/loadtest/load_test.go | 4 ++ itest/loadtest/send_test.go | 129 ++++++++++++++++++++++++++++++++++++ itest/loadtest/utils.go | 62 +++++++++++++++++ 4 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 itest/loadtest/send_test.go diff --git a/itest/loadtest/config.go b/itest/loadtest/config.go index 9fc9668977..ac133d418d 100644 --- a/itest/loadtest/config.go +++ b/itest/loadtest/config.go @@ -4,6 +4,7 @@ import ( "time" "github.com/jessevdk/go-flags" + "github.com/lightninglabs/taproot-assets/taprpc" ) const ( @@ -58,9 +59,21 @@ type Config struct { // Bitcoin is the configuration for the bitcoin backend. Bitcoin *BitcoinConfig `group:"bitcoin" namespace:"bitcoin" long:"bitcoin" description:"bitcoin client configuration"` - // BatchSize is the number of assets to mint in a single batch. This is only - // relevant for some test cases. - BatchSize int `long:"batch-size" description:"the number of assets to mint in a single batch"` + // BatchSize is the number of assets to mint in a single batch. This is + // only relevant for the mint test. + BatchSize int `long:"mint-test-batch-size" description:"the number of assets to mint in a single batch; only relevant for the mint test"` + + // NumSends is the number of asset sends to perform. This is only + // relevant for the send test. + NumSends int `long:"send-test-num-sends" description:"the number of send operations to perform; only relevant for the send test"` + + // NumAssets is the number of assets to send in each send operation. + // This is only relevant for the send test. + NumAssets uint64 `long:"send-test-num-assets" description:"the number of assets to send in each send operation; only relevant for the send test"` + + // SendType is the type of asset to attempt to send. This is only + // relevant for the send test. + SendType taprpc.AssetType `long:"send-test-send-type" description:"the type of asset to attempt to send; only relevant for the send test"` // TestSuiteTimeout is the timeout for the entire test suite. TestSuiteTimeout time.Duration `long:"test-suite-timeout" description:"the timeout for the entire test suite"` @@ -84,6 +97,9 @@ func DefaultConfig() Config { }, }, BatchSize: 100, + NumSends: 50, + NumAssets: 1, // We only mint collectibles. + SendType: taprpc.AssetType_COLLECTIBLE, TestSuiteTimeout: defaultSuiteTimeout, TestTimeout: defaultTestTimeout, } diff --git a/itest/loadtest/load_test.go b/itest/loadtest/load_test.go index 2c048d0d60..1d32a83b8e 100644 --- a/itest/loadtest/load_test.go +++ b/itest/loadtest/load_test.go @@ -19,6 +19,10 @@ var loadTestCases = []testCase{ name: "mint", fn: mintTest, }, + { + name: "send", + fn: sendTest, + }, } // TestPerformance executes the configured performance tests. diff --git a/itest/loadtest/send_test.go b/itest/loadtest/send_test.go new file mode 100644 index 0000000000..b63412d1fb --- /dev/null +++ b/itest/loadtest/send_test.go @@ -0,0 +1,129 @@ +package loadtest + +import ( + "context" + prand "math/rand" + "testing" + + "github.com/btcsuite/btcd/rpcclient" + "github.com/lightninglabs/taproot-assets/itest" + "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/stretchr/testify/require" +) + +var ( + statusDetected = taprpc.AddrEventStatus_ADDR_EVENT_STATUS_TRANSACTION_DETECTED + statusCompleted = taprpc.AddrEventStatus_ADDR_EVENT_STATUS_COMPLETED +) + +// sendTest checks that we are able to send assets between the two nodes. +func sendTest(t *testing.T, ctx context.Context, cfg *Config) { + // Start by initializing all our client connections. + alice, bob, bitcoinClient := initClients(t, ctx, cfg) + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, cfg.TestTimeout) + defer cancel() + + t.Logf("Running send test, sending %d asset(s) of type %v %d times", + cfg.NumAssets, cfg.SendType, cfg.NumSends) + for i := 1; i <= cfg.NumSends; i++ { + send, receive, ok := pickSendNode( + t, ctx, cfg.NumAssets, cfg.SendType, alice, bob, + ) + if !ok { + t.Fatalf("Aborting send test at attempt %d of %d as "+ + "no node has enough balance to send %d "+ + "assets of type %v", i, cfg.NumSends, + cfg.NumAssets, cfg.SendType) + return + } + + sendAssets( + t, ctxt, cfg.NumAssets, cfg.SendType, send, receive, + bitcoinClient, + ) + + t.Logf("Finished %d of %d send operations", i, cfg.NumSends) + } +} + +// sendAsset sends the given number of assets of the given type from the given +// node to the other node. +func sendAssets(t *testing.T, ctx context.Context, numAssets uint64, + assetType taprpc.AssetType, send, receive *rpcClient, + bitcoinClient *rpcclient.Client) { + + // Query the asset we'll be sending, so we can assert some things about + // it later. + sendAsset := send.assetIDWithBalance(t, ctx, numAssets, assetType) + t.Logf("Sending %d asset(s) with ID %x from %v to %v", numAssets, + sendAsset.AssetGenesis.AssetId, send.cfg.Name, receive.cfg.Name) + + // Let's create an address on the receiving node and make sure it's + // created correctly. + addr, err := receive.NewAddr(ctx, &taprpc.NewAddrRequest{ + AssetId: sendAsset.AssetGenesis.AssetId, + Amt: numAssets, + }) + require.NoError(t, err) + itest.AssertAddrCreated(t, receive, sendAsset, addr) + + // Before we send the asset, we record the existing transfers on the + // sending node, so we can easily select the new transfer once it + // appears. + transfersBefore := send.listTransfersSince(t, ctx, nil) + + // Initiate the send now. + _, err = send.SendAsset(ctx, &taprpc.SendAssetRequest{ + TapAddrs: []string{addr.Encoded}, + }) + require.NoError(t, err) + + // Wait for the transfer to appear on the sending node. + require.Eventually(t, func() bool { + newTransfers := send.listTransfersSince(t, ctx, transfersBefore) + return len(newTransfers) == 1 + }, defaultTimeout, wait.PollInterval) + + // And for it to be detected on the receiving node. + itest.AssertAddrEvent(t, receive, addr, 1, statusDetected) + + // Mine a block to confirm the transfer. + itest.MineBlocks(t, bitcoinClient, 1, 1) + + // Now the transfer should go to completed eventually. + itest.AssertAddrEvent(t, receive, addr, 1, statusCompleted) +} + +// pickSendNode picks a node at random, checks whether it has enough assets of +// the given type, and returns it. The second return value is the other node, +// which will be the receiving node. The boolean argument returns true if there +// is a node with sufficient balance. If that is false, the test should be +// skipped. +func pickSendNode(t *testing.T, ctx context.Context, minBalance uint64, + assetType taprpc.AssetType, a, b *rpcClient) (*rpcClient, *rpcClient, + bool) { + + send, receive := a, b + if prand.Intn(1) == 0 { + send, receive = b, a + } + + // Check if the randomly picked send node has enough balance. + if send.assetIDWithBalance(t, ctx, minBalance, assetType) != nil { + return send, receive, true + } + + // If we get here, the send node doesn't have enough balance. We'll try + // the other one. + send, receive = receive, send + if send.assetIDWithBalance(t, ctx, minBalance, assetType) != nil { + return send, receive, true + } + + // None of the nodes have enough balance. We can't run the send test + // currently. + return nil, nil, false +} diff --git a/itest/loadtest/utils.go b/itest/loadtest/utils.go index ae0f61cfb0..ad75256d8a 100644 --- a/itest/loadtest/utils.go +++ b/itest/loadtest/utils.go @@ -1,12 +1,15 @@ package loadtest import ( + "bytes" "context" "crypto/tls" "crypto/x509" + "encoding/hex" "fmt" "os" "testing" + "time" "github.com/btcsuite/btcd/rpcclient" "github.com/lightninglabs/taproot-assets/itest" @@ -26,6 +29,10 @@ var ( // maxMsgRecvSize is the largest message our client will receive. We // set this to 200MiB atm. maxMsgRecvSize = grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize) + + // defaultTimeout is a timeout that will be used for various wait + // scenarios where no custom timeout value is defined. + defaultTimeout = time.Second * 10 ) type rpcClient struct { @@ -36,6 +43,61 @@ type rpcClient struct { assetwalletrpc.AssetWalletClient } +// assetIDWithBalance returns the asset ID of an asset that has at least the +// given balance. If no such asset is found, nil is returned. +func (r *rpcClient) assetIDWithBalance(t *testing.T, ctx context.Context, + minBalance uint64, assetType taprpc.AssetType) *taprpc.Asset { + + balances, err := r.ListBalances(ctx, &taprpc.ListBalancesRequest{ + GroupBy: &taprpc.ListBalancesRequest_AssetId{ + AssetId: true, + }, + }) + require.NoError(t, err) + + for assetIDHex, balance := range balances.AssetBalances { + if balance.Balance >= minBalance && + balance.AssetType == assetType { + + assetIDBytes, err := hex.DecodeString(assetIDHex) + require.NoError(t, err) + + assets, err := r.ListAssets( + ctx, &taprpc.ListAssetRequest{}, + ) + require.NoError(t, err) + + for _, asset := range assets.Assets { + if bytes.Equal( + asset.AssetGenesis.AssetId, + assetIDBytes, + ) { + + return asset + } + } + } + } + + return nil +} + +// listTransfersSince returns all transfers that have been made since the last +// transfer in the given list. If the list is empty, all transfers are returned. +func (r *rpcClient) listTransfersSince(t *testing.T, ctx context.Context, + existingTransfers []*taprpc.AssetTransfer) []*taprpc.AssetTransfer { + + resp, err := r.ListTransfers(ctx, &taprpc.ListTransfersRequest{}) + require.NoError(t, err) + + if len(existingTransfers) == 0 { + return resp.Transfers + } + + newIndex := len(existingTransfers) + return resp.Transfers[newIndex:] +} + func initClients(t *testing.T, ctx context.Context, cfg *Config) (*rpcClient, *rpcClient, *rpcclient.Client) {