From 977c3e9db4207b991b3cf94a95617a7987469085 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 9 Oct 2023 15:05:58 +0200 Subject: [PATCH 1/3] loadtest: allow test case to be specified on command line too Because we are running a compiled Go test case, we can't use any custom flags as the internal flag parser only accepts the pre-defined "-test.XXX" flags. But we couldn't use those either because we attempted to also parse the flags with the lnd flag parsing library. This commit fixes the issue by removing any custom flag parsing and allowing test cases to be specified using the -test.run flag. For example: -test.run="^TestPerformance/mint_batch_stress$" --- itest/loadtest/config.go | 24 ++------------- itest/loadtest/load_test.go | 61 ++++++++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/itest/loadtest/config.go b/itest/loadtest/config.go index 20570c273..9fc966897 100644 --- a/itest/loadtest/config.go +++ b/itest/loadtest/config.go @@ -73,7 +73,6 @@ type Config struct { // binary. func DefaultConfig() Config { return Config{ - TestCases: []string{"mint_batch_stress"}, Alice: &User{ Tapd: &TapConfig{ Name: "alice", @@ -95,21 +94,11 @@ func DefaultConfig() Config { // // The configuration proceeds as follows: // 1. Start with a default config with sane settings -// 2. Pre-parse the command line to check for an alternative config file -// 3. Load configuration file overwriting defaults with any specified options -// 4. Parse CLI options and overwrite/add any specified options +// 2. Load configuration file overwriting defaults with any specified options func LoadConfig() (*Config, error) { - // Pre-parse the command line options to pick up an alternative config - // file. - preCfg := DefaultConfig() - if _, err := flags.Parse(&preCfg); err != nil { - return nil, err - } - - // Next, load any additional configuration options from the file. - cfg := preCfg + // First, load any additional configuration options from the file. + cfg := DefaultConfig() fileParser := flags.NewParser(&cfg, flags.Default) - err := flags.NewIniParser(fileParser).ParseFile(defaultConfigPath) if err != nil { // If it's a parsing related error, then we'll return @@ -120,13 +109,6 @@ func LoadConfig() (*Config, error) { } } - // Finally, parse the remaining command line options again to ensure - // they take precedence. - flagParser := flags.NewParser(&cfg, flags.Default) - if _, err := flagParser.Parse(); err != nil { - return nil, err - } - // Make sure everything we just loaded makes sense. cleanCfg, err := ValidateConfig(cfg) if err != nil { diff --git a/itest/loadtest/load_test.go b/itest/loadtest/load_test.go index 0465fc825..9b105895f 100644 --- a/itest/loadtest/load_test.go +++ b/itest/loadtest/load_test.go @@ -9,6 +9,18 @@ import ( "github.com/stretchr/testify/require" ) +type testCase struct { + name string + fn func(t *testing.T, ctx context.Context, cfg *Config) +} + +var loadTestCases = []testCase{ + { + name: "mint_batch_stress", + fn: execMintBatchStressTest, + }, +} + // TestPerformance executes the configured performance tests. func TestPerformance(t *testing.T) { cfg, err := LoadConfig() @@ -18,23 +30,44 @@ func TestPerformance(t *testing.T) { ctxt, cancel := context.WithTimeout(ctxb, cfg.TestSuiteTimeout) defer cancel() - for _, testCase := range cfg.TestCases { - execTestCase(t, ctxt, testCase, cfg) - } -} + for _, tc := range loadTestCases { + tc := tc -// execTestCase is the method in charge of executing a single test case. -func execTestCase(t *testing.T, ctx context.Context, testName string, - cfg *Config) { + if !shouldRunCase(tc.name, cfg.TestCases) { + t.Logf("Not running test case '%s' as not configured", + tc.name) - ctxt, cancel := context.WithTimeout(ctx, cfg.TestTimeout) - defer cancel() + continue + } - switch testName { - case "mint_batch_stress": - execMintBatchStressTest(t, ctxt, cfg) + success := t.Run(tc.name, func(tt *testing.T) { + ctxt, cancel := context.WithTimeout( + ctxt, cfg.TestTimeout, + ) + defer cancel() - default: - require.Fail(t, "unknown test case: %v", testName) + tc.fn(t, ctxt, cfg) + }) + if !success { + t.Fatalf("test case %v failed", tc.name) + } } } + +// shouldRunCase returns true if the given test case should be run. This will +// return true if the config file does not specify any test cases. In that case +// we can select the test cases to run using the command line +// (-test.run="TestPerformance/test_case_name") +func shouldRunCase(name string, configuredCases []string) bool { + if len(configuredCases) == 0 { + return true + } + + for _, c := range configuredCases { + if c == name { + return true + } + } + + return false +} From 46389a76c0e13cdf4956abad032092193b247cf3 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 9 Oct 2023 15:06:00 +0200 Subject: [PATCH 2/3] loadtest: refactor stress test for better re-usability --- itest/loadtest/load_test.go | 4 +-- itest/loadtest/mint_batch_test.go | 54 ++++++------------------------- itest/loadtest/utils.go | 46 ++++++++++++++++++++++---- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/itest/loadtest/load_test.go b/itest/loadtest/load_test.go index 9b105895f..2c048d0d6 100644 --- a/itest/loadtest/load_test.go +++ b/itest/loadtest/load_test.go @@ -16,8 +16,8 @@ type testCase struct { var loadTestCases = []testCase{ { - name: "mint_batch_stress", - fn: execMintBatchStressTest, + name: "mint", + fn: mintTest, }, } diff --git a/itest/loadtest/mint_batch_test.go b/itest/loadtest/mint_batch_test.go index 56311905a..b261718ab 100644 --- a/itest/loadtest/mint_batch_test.go +++ b/itest/loadtest/mint_batch_test.go @@ -11,7 +11,6 @@ import ( "testing" "time" - "github.com/btcsuite/btcd/rpcclient" "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/itest" "github.com/lightninglabs/taproot-assets/taprpc" @@ -24,60 +23,27 @@ import ( //go:embed testdata/8k-metadata.hex var imageMetadataHex []byte -// execMintBatchStressTest checks that we are able to mint a batch of assets -// and that other memebers in the federation see the universe updated -// accordingly. -func execMintBatchStressTest(t *testing.T, ctx context.Context, cfg *Config) { - // Create tapd clients. - alice, aliceCleanUp := getTapClient(t, ctx, cfg.Alice.Tapd) - defer aliceCleanUp() - - _, err := alice.GetInfo(ctx, &taprpc.GetInfoRequest{}) - require.NoError(t, err) - - bob, bobCleanUp := getTapClient(t, ctx, cfg.Bob.Tapd) - defer bobCleanUp() - - _, err = bob.GetInfo(ctx, &taprpc.GetInfoRequest{}) - require.NoError(t, err) - - // Create bitcoin client. - bitcoinClient := getBitcoinConn(t, cfg.Bitcoin) - - itest.MineBlocks(t, bitcoinClient, 1, 0) - - // If we fail from this point onward, we might have created a - // transaction that isn't mined yet. To make sure we can run the test - // again, we'll make sure to clean up the mempool by mining a block. - t.Cleanup(func() { - itest.MineBlocks(t, bitcoinClient, 1, 0) - }) +// mintTest checks that we are able to mint a batch of assets and that other +// members in the federation see the universe updated accordingly. +func mintTest(t *testing.T, ctx context.Context, cfg *Config) { + // Start by initializing all our client connections. + alice, bob, bitcoinClient := initClients(t, ctx, cfg) imageMetadataBytes, err := hex.DecodeString( strings.Trim(string(imageMetadataHex), "\n"), ) require.NoError(t, err) - aliceHost := fmt.Sprintf("%s:%d", cfg.Alice.Tapd.Host, - cfg.Alice.Tapd.Port) - - minterTimeout := 10 * time.Minute - mintBatchStressTest( - t, ctx, bitcoinClient, alice, bob, aliceHost, cfg.BatchSize, - imageMetadataBytes, minterTimeout, - ) -} - -func mintBatchStressTest(t *testing.T, ctx context.Context, - bitcoinClient *rpcclient.Client, alice, bob itest.TapdClient, - aliceHost string, batchSize int, imageMetadataBytes []byte, - minterTimeout time.Duration) { - var ( + minterTimeout = cfg.TestTimeout + batchSize = cfg.BatchSize batchReqs = make([]*mintrpc.MintAssetRequest, batchSize) baseName = fmt.Sprintf("jpeg-%d", rand.Int31()) metaPrefixSize = binary.MaxVarintLen16 metadataPrefix = make([]byte, metaPrefixSize) + aliceHost = fmt.Sprintf( + "%s:%d", alice.cfg.Host, alice.cfg.Port, + ) ) // Before we mint a new group, let's first find out how many there diff --git a/itest/loadtest/utils.go b/itest/loadtest/utils.go index 5fe40d096..ae0f61cfb 100644 --- a/itest/loadtest/utils.go +++ b/itest/loadtest/utils.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/btcsuite/btcd/rpcclient" + "github.com/lightninglabs/taproot-assets/itest" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" @@ -28,14 +29,45 @@ var ( ) type rpcClient struct { + cfg *TapConfig taprpc.TaprootAssetsClient universerpc.UniverseClient mintrpc.MintClient assetwalletrpc.AssetWalletClient } +func initClients(t *testing.T, ctx context.Context, + cfg *Config) (*rpcClient, *rpcClient, *rpcclient.Client) { + + // Create tapd clients. + alice := getTapClient(t, ctx, cfg.Alice.Tapd) + + _, err := alice.GetInfo(ctx, &taprpc.GetInfoRequest{}) + require.NoError(t, err) + + bob := getTapClient(t, ctx, cfg.Bob.Tapd) + + _, err = bob.GetInfo(ctx, &taprpc.GetInfoRequest{}) + require.NoError(t, err) + + // Create bitcoin client. + bitcoinClient := getBitcoinConn(t, cfg.Bitcoin) + + // Test bitcoin client connection by mining a block. + itest.MineBlocks(t, bitcoinClient, 1, 0) + + // If we fail from this point onward, we might have created a + // transaction that isn't mined yet. To make sure we can run the test + // again, we'll make sure to clean up the mempool by mining a block. + t.Cleanup(func() { + itest.MineBlocks(t, bitcoinClient, 1, 0) + }) + + return alice, bob, bitcoinClient +} + func getTapClient(t *testing.T, ctx context.Context, - cfg *TapConfig) (*rpcClient, func()) { + cfg *TapConfig) *rpcClient { creds := credentials.NewTLS(&tls.Config{}) if cfg.TLSPath != "" { @@ -72,7 +104,7 @@ func getTapClient(t *testing.T, ctx context.Context, } svrAddr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) - conn, err := grpc.Dial(svrAddr, opts...) + conn, err := grpc.DialContext(ctx, svrAddr, opts...) require.NoError(t, err) assetsClient := taprpc.NewTaprootAssetsClient(conn) @@ -81,17 +113,19 @@ func getTapClient(t *testing.T, ctx context.Context, assetWalletClient := assetwalletrpc.NewAssetWalletClient(conn) client := &rpcClient{ + cfg: cfg, TaprootAssetsClient: assetsClient, UniverseClient: universeClient, MintClient: mintMintClient, AssetWalletClient: assetWalletClient, } - cleanUp := func() { - conn.Close() - } + t.Cleanup(func() { + err := conn.Close() + require.NoError(t, err) + }) - return client, cleanUp + return client } func getBitcoinConn(t *testing.T, cfg *BitcoinConfig) *rpcclient.Client { From ba9fa8ca77267f0f2c5ca8235e82baa938b76d0d Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 9 Oct 2023 15:06:01 +0200 Subject: [PATCH 3/3] 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 9fc966897..ac133d418 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 2c048d0d6..1d32a83b8 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 000000000..b63412d1f --- /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 ae0f61cfb..ad75256d8 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) {