From b16abbd307067ee7874d537114506748b619e346 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Thu, 9 Jan 2025 13:14:17 +0200 Subject: [PATCH 1/3] loadtest: update config file for new fields With the new mintV2 test we add a few extra parameters to the config file. In this PR we expose the new parameters in the sample config and add them to the config struct definition. --- itest/loadtest/config.go | 10 ++++++++++ itest/loadtest/loadtest-sample.conf | 11 ++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/itest/loadtest/config.go b/itest/loadtest/config.go index fdc0d02ea..339ca88e9 100644 --- a/itest/loadtest/config.go +++ b/itest/loadtest/config.go @@ -93,6 +93,16 @@ type Config struct { // 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"` + // TotalNumGroups is the total number of groups that the minted assets + // belong to. + TotalNumGroups int `long:"mint-test-total-groups" description:"the total number of groups the minted assets belong to"` + + // MintSupplyMin is the minimum supply to mint per asset. + MintSupplyMin int `long:"mint-test-supply-min" description:"the max supply to mint per asset"` + + // MintSupplyMax is the max suipply to mint per asset. + MintSupplyMax int `long:"mint-test-supply-max" description:"the min supply to mint per asset"` + // 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"` diff --git a/itest/loadtest/loadtest-sample.conf b/itest/loadtest/loadtest-sample.conf index 3db7431f6..bfe140cad 100644 --- a/itest/loadtest/loadtest-sample.conf +++ b/itest/loadtest/loadtest-sample.conf @@ -2,11 +2,20 @@ network=regtest # The name of the test case to run. Example "send" or "mint" -test-case="mint" +test-case="mintV2" # Batch size for mint test mint-test-batch-size=5 +# Total number of groups that assets belong to. +mint-test-total-groups=20 + +# Max supply per asset mint. +mint-test-supply-max=500000 + +# Min supply per asset mint. +mint-test-supply-min=100000 + # Number of send operations to perform for send test send-test-num-sends=5 From 7f26e1b93cd3e599bca5e601d24492d13bcf7d07 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Thu, 9 Jan 2025 14:18:05 +0200 Subject: [PATCH 2/3] itest: make WaitForNTxsInMempool public We make the WaitForNTxsInMempool function public as it's going to be needed in the mintV2 loadtest. --- itest/assets_test.go | 2 +- itest/test_harness.go | 4 ++-- itest/utils.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/itest/assets_test.go b/itest/assets_test.go index 2ebeb6c6d..fa8585d12 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -183,7 +183,7 @@ func testMintBatchResume(t *harnessTest) { require.NoError(t.t, t.tapd.stop(false)) require.NoError(t.t, t.tapd.start(false)) - hashes, err := waitForNTxsInMempool( + hashes, err := WaitForNTxsInMempool( t.lndHarness.Miner().Client, 1, defaultWaitTimeout, ) require.NoError(t.t, err) diff --git a/itest/test_harness.go b/itest/test_harness.go index 45a20146a..5fed49a87 100644 --- a/itest/test_harness.go +++ b/itest/test_harness.go @@ -472,10 +472,10 @@ func isMempoolEmpty(miner *rpcclient.Client, timeout time.Duration) (bool, } } -// waitForNTxsInMempool polls until finding the desired number of transactions +// WaitForNTxsInMempool polls until finding the desired number of transactions // in the provided miner's mempool. An error is returned if this number is not // met after the given timeout. -func waitForNTxsInMempool(miner *rpcclient.Client, n int, +func WaitForNTxsInMempool(miner *rpcclient.Client, n int, timeout time.Duration) ([]*chainhash.Hash, error) { breakTimeout := time.After(timeout) diff --git a/itest/utils.go b/itest/utils.go index 266ad267f..203a2c16d 100644 --- a/itest/utils.go +++ b/itest/utils.go @@ -142,7 +142,7 @@ func MineBlocks(t *testing.T, client *rpcclient.Client, var txids []*chainhash.Hash var err error if numTxs > 0 { - txids, err = waitForNTxsInMempool( + txids, err = WaitForNTxsInMempool( client, numTxs, minerMempoolTimeout, ) if err != nil { @@ -386,7 +386,7 @@ func FinalizeBatchUnconfirmed(t *testing.T, minerClient *rpcclient.Client, batchResp.Batch.BatchKey, mintrpc.BatchState_BATCH_STATE_BROADCAST, ) - hashes, err := waitForNTxsInMempool( + hashes, err := WaitForNTxsInMempool( minerClient, 1, options.mintingTimeout, ) require.NoError(t, err) From 964f42cb2ebb92f6b089f4f862303550062a89df Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Thu, 9 Jan 2025 14:20:17 +0200 Subject: [PATCH 3/3] loadtest: add mintTestV2 We add a new mintV2 test which mints normal assets of a configured supply into a fixed number of groups. This is an enhanced and more lightweight version of the previous mint test, as it uses less assertions and rpc calls. --- itest/loadtest/load_test.go | 4 + itest/loadtest/mint_batch_test.go | 198 ++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/itest/loadtest/load_test.go b/itest/loadtest/load_test.go index c142886a2..221940e96 100644 --- a/itest/loadtest/load_test.go +++ b/itest/loadtest/load_test.go @@ -39,6 +39,10 @@ var loadTestCases = []testCase{ name: "mint", fn: mintTest, }, + { + name: "mintV2", + fn: mintTestV2, + }, { name: "send", fn: sendTest, diff --git a/itest/loadtest/mint_batch_test.go b/itest/loadtest/mint_batch_test.go index fabfffb34..6019390db 100644 --- a/itest/loadtest/mint_batch_test.go +++ b/itest/loadtest/mint_batch_test.go @@ -9,12 +9,15 @@ import ( "math/rand" "strings" "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" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc" + "github.com/lightningnetwork/lnd/lntest/wait" "github.com/stretchr/testify/require" ) @@ -166,3 +169,198 @@ func mintTest(t *testing.T, ctx context.Context, cfg *Config) { itest.SyncUniverses(ctx, t, bob, alice, aliceHost, cfg.TestTimeout) } + +// mintTestV2 checks that we can mint a batch of assets. It is a more +// performant version of the existing mintTest, as it uses less assertions and +// RPC calls. +func mintTestV2(t *testing.T, ctx context.Context, cfg *Config) { + // Start by initializing all our client connections. + alice, bob, bitcoinClient := initClients(t, ctx, cfg) + + // We query the assets of each node once on this step. Every function + // that needs to take a node's assets into account will be passed these + // values instead of calling the RPC again. This is done to minimize + // collateral RPC impact of the loadtest. + resAlice, err := alice.ListAssets(ctx, &taprpc.ListAssetRequest{}) + require.NoError(t, err) + + resBob, err := bob.ListAssets(ctx, &taprpc.ListAssetRequest{}) + require.NoError(t, err) + + assetsAlice := resAlice.Assets + assetsBob := resBob.Assets + + totalAssets := make([]*taprpc.Asset, len(assetsAlice)+len(assetsBob)) + copy(totalAssets, assetsAlice) + copy(totalAssets[len(assetsAlice):], assetsBob) + + // Alice serves as the minter. + minter := alice + + // First we make sure group initialization is completed. We check if + // there's any more groups left + existingGroups := getTotalAssetGroups(totalAssets) + groupKeys := make(map[string][]byte, 0) + + for _, v := range existingGroups { + tweakedKey, err := hex.DecodeString(v) + require.NoError(t, err) + + groupKeys[v] = tweakedKey + } + + var remainingGroups int + if cfg.TotalNumGroups > len(existingGroups) { + remainingGroups = cfg.TotalNumGroups - len(existingGroups) + } + + t.Logf("Existing groups=%v, minting %v new groups", + len(existingGroups), remainingGroups) + for range remainingGroups { + mintNewGroup(t, ctx, bitcoinClient, minter, cfg) + } + + // If there's not any existing groups we skip the rest of the steps, we + // will mint into those groups in another run. + if len(existingGroups) == 0 { + return + } + + groupIndex := rand.Intn(len(existingGroups)) + groupKey := groupKeys[existingGroups[groupIndex]] + + mintIntoGroup(t, ctx, bitcoinClient, minter, groupKey, cfg) +} + +// mintNewGroup mints an asset that creates a new group. +func mintNewGroup(t *testing.T, ctx context.Context, miner *rpcclient.Client, + minter *rpcClient, cfg *Config) []*taprpc.Asset { + + mintAmt := rand.Uint64() % uint64(cfg.MintSupplyMax) + if mintAmt < uint64(cfg.MintSupplyMin) { + mintAmt = uint64(cfg.MintSupplyMin) + } + + // nolint:lll + assetRequests := []*mintrpc.MintAssetRequest{ + { + Asset: &mintrpc.MintAsset{ + AssetType: taprpc.AssetType_NORMAL, + Name: fmt.Sprintf( + "tapcoin-%d", time.Now().UnixNano(), + ), + AssetMeta: &taprpc.AssetMeta{ + Data: []byte("{}"), + Type: taprpc.AssetMetaType_META_TYPE_JSON, + }, + Amount: mintAmt, + NewGroupedAsset: true, + DecimalDisplay: 4, + }, + }, + } + + return finishMint(t, ctx, miner, minter, assetRequests) +} + +// mintIntoGroup mints as many assets as the batch size and puts them in the +// existing group that is provided by the corresponding argument. +func mintIntoGroup(t *testing.T, ctx context.Context, miner *rpcclient.Client, + minter *rpcClient, tweakedKey []byte, cfg *Config) []*taprpc.Asset { + + mintAmt := rand.Uint64() % uint64(cfg.MintSupplyMax) + if mintAmt < uint64(cfg.MintSupplyMin) { + mintAmt = uint64(cfg.MintSupplyMin) + } + + var assetRequests []*mintrpc.MintAssetRequest + + t.Logf("Minting %v assets into group %s", cfg.BatchSize, tweakedKey) + + for range cfg.BatchSize { + ts := time.Now().UnixNano() + + // nolint:lll + req := &mintrpc.MintAssetRequest{ + Asset: &mintrpc.MintAsset{ + AssetType: taprpc.AssetType_NORMAL, + Name: fmt.Sprintf("tapcoin-%d", ts), + AssetMeta: &taprpc.AssetMeta{ + Data: []byte("{}"), + Type: taprpc.AssetMetaType_META_TYPE_JSON, + }, + Amount: mintAmt, + GroupedAsset: true, + GroupKey: tweakedKey, + DecimalDisplay: 4, + }, + } + + assetRequests = append(assetRequests, req) + } + + return finishMint(t, ctx, miner, minter, assetRequests) +} + +// finishMint accepts a list of asset requests and performs the necessary RPC +// calls to create and finalize a minting batch. +func finishMint(t *testing.T, ctx context.Context, miner *rpcclient.Client, + minter *rpcClient, + assetRequests []*mintrpc.MintAssetRequest) []*taprpc.Asset { + + ctxc, streamCancel := context.WithCancel(ctx) + stream, err := minter.SubscribeMintEvents( + ctxc, &mintrpc.SubscribeMintEventsRequest{}, + ) + require.NoError(t, err) + sub := &itest.EventSubscription[*mintrpc.MintEvent]{ + ClientEventStream: stream, + Cancel: streamCancel, + } + + itest.BuildMintingBatch(t, minter, assetRequests) + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, wait.DefaultTimeout) + defer cancel() + + finalizeReq := &mintrpc.FinalizeBatchRequest{} + // Instruct the daemon to finalize the batch. + batchResp, err := minter.FinalizeBatch(ctxt, finalizeReq) + require.NoError(t, err) + require.NotEmpty(t, batchResp.Batch) + require.Len(t, batchResp.Batch.Assets, len(assetRequests)) + require.Equal( + t, mintrpc.BatchState_BATCH_STATE_BROADCAST, + batchResp.Batch.State, + ) + + itest.WaitForBatchState( + t, ctxt, minter, wait.DefaultTimeout, + batchResp.Batch.BatchKey, + mintrpc.BatchState_BATCH_STATE_BROADCAST, + ) + hashes, err := itest.WaitForNTxsInMempool( + miner, 1, wait.DefaultTimeout, + ) + require.NoError(t, err) + require.GreaterOrEqual(t, len(hashes), 1) + + return itest.ConfirmBatch( + t, miner, minter, assetRequests, sub, *hashes[0], + batchResp.Batch.BatchKey, + ) +} + +// getTotalAssetGroups returns the total number of asset groups found in the +// passed array of assets. +func getTotalAssetGroups(assets []*taprpc.Asset) []string { + groups := fn.NewSet[string]() + + for _, v := range assets { + groupKeyStr := fmt.Sprintf("%x", v.AssetGroup.TweakedGroupKey) + groups.Add(groupKeyStr) + } + + return groups.ToSlice() +}