diff --git a/itest/loadtest/config.go b/itest/loadtest/config.go index 339ca88e9..85a2bd21e 100644 --- a/itest/loadtest/config.go +++ b/itest/loadtest/config.go @@ -6,7 +6,6 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/jessevdk/go-flags" - "github.com/lightninglabs/taproot-assets/taprpc" ) const ( @@ -111,9 +110,10 @@ type Config struct { // 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"` + // SendAssetType is the type of asset to attempt to send. This is only + // relevant for the send test. Acceptable values are "normal" and + // "collectible". + SendAssetType string `long:"send-asset-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"` @@ -143,8 +143,8 @@ func DefaultConfig() Config { Network: "regtest", BatchSize: 100, NumSends: 50, - NumAssets: 1, // We only mint collectibles. - SendType: taprpc.AssetType_COLLECTIBLE, + NumAssets: 1, + SendAssetType: "normal", TestSuiteTimeout: defaultSuiteTimeout, TestTimeout: defaultTestTimeout, PrometheusGateway: &PrometheusGatewayConfig{ diff --git a/itest/loadtest/load_test.go b/itest/loadtest/load_test.go index 221940e96..674f6d282 100644 --- a/itest/loadtest/load_test.go +++ b/itest/loadtest/load_test.go @@ -47,6 +47,10 @@ var loadTestCases = []testCase{ name: "send", fn: sendTest, }, + { + name: "sendV2", + fn: sendTestV2, + }, { name: "multisig", fn: multisigTest, diff --git a/itest/loadtest/loadtest-sample.conf b/itest/loadtest/loadtest-sample.conf index bfe140cad..6af3592eb 100644 --- a/itest/loadtest/loadtest-sample.conf +++ b/itest/loadtest/loadtest-sample.conf @@ -22,6 +22,10 @@ send-test-num-sends=5 # Number of assets to send in each send operation for send test send-test-num-assets=1 +# Type of asset to use in the test case. For V2 test cases, a "normal" type is +# required. +send-asset-type="normal" + # Timeout for the entire test suite test-suite-timeout=120m diff --git a/itest/loadtest/send_test.go b/itest/loadtest/send_test.go index 563010fb5..fce68cd07 100644 --- a/itest/loadtest/send_test.go +++ b/itest/loadtest/send_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" prand "math/rand" + "math/rand/v2" "testing" "time" @@ -29,22 +30,24 @@ func sendTest(t *testing.T, ctx context.Context, cfg *Config) { ctxt, cancel := context.WithTimeout(ctxb, cfg.TestTimeout) defer cancel() + sendType := stringToAssetType(cfg.SendAssetType) + t.Logf("Running send test, sending %d asset(s) of type %v %d times", - cfg.NumAssets, cfg.SendType, cfg.NumSends) + cfg.NumAssets, sendType, cfg.NumSends) for i := 1; i <= cfg.NumSends; i++ { send, receive, ok := pickSendNode( - t, ctx, cfg.NumAssets, cfg.SendType, alice, bob, + t, ctx, cfg.NumAssets, 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) + cfg.NumAssets, sendType) return } sendAssets( - t, ctxt, cfg.NumAssets, cfg.SendType, send, receive, + t, ctxt, cfg.NumAssets, sendType, send, receive, bitcoinClient, cfg.TestTimeout, ) @@ -52,7 +55,155 @@ func sendTest(t *testing.T, ctx context.Context, cfg *Config) { } } -// sendAssets sends the given number of assets of the given type from the given +// sendTestV2 checks that we are able to send assets between the two nodes. It +// is a more performant and lightweight version of sendTest, as it uses less +// assertions and RPC calls. +func sendTestV2(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() + + sendType := stringToAssetType(cfg.SendAssetType) + + // Alice is set to be the minter in mintV2, so we use Alice's universe. + uniHost := fmt.Sprintf("%s:%d", alice.cfg.Host, alice.cfg.Port) + + // Let's make sure Bob is aware of all the assets that Alice may have + // minted. + itest.SyncUniverses( + ctx, t, bob, alice, uniHost, cfg.TestTimeout, + itest.WithSyncMode(itest.SyncModeFull), + ) + + // We now retrieve Alice and Bob's balances just once, and will re-use + // them in future function calls. Any update to the balances will be + // directly applied to these response objects, to skip future calls to + // ListBalances. + resAlice, err := alice.ListBalances(ctx, &taprpc.ListBalancesRequest{ + GroupBy: &taprpc.ListBalancesRequest_AssetId{ + AssetId: true, + }, + }) + require.NoError(t, err) + + resBob, err := bob.ListBalances(ctx, &taprpc.ListBalancesRequest{ + GroupBy: &taprpc.ListBalancesRequest_AssetId{ + AssetId: true, + }, + }) + require.NoError(t, err) + + for i := 1; i <= cfg.NumSends; i++ { + var ( + sender, receiver *rpcClient + senderAssets map[string]*taprpc.AssetBalance + ) + + // Assets may be sent in both directions, so we make a random + // draw to conclude who the sender is. + draw := rand.IntN(2) + + switch draw { + case 0: + sender = alice + senderAssets = resAlice.AssetBalances + receiver = bob + + case 1: + sender = bob + senderAssets = resBob.AssetBalances + receiver = alice + } + + sendAssetV2( + t, ctxt, cfg.NumAssets, sendType, senderAssets, + sender, receiver, bitcoinClient, cfg.TestTimeout, + ) + } +} + +// sendAssetV2 sends a certain amount of assets of a specific type from a sender +// to a receiver. It will scan the balance of the sender and find a suitable +// asset to carry out the send, then will dispatch the send and assert its +// completion. +func sendAssetV2(t *testing.T, ctx context.Context, numAssets uint64, + assetType taprpc.AssetType, assets map[string]*taprpc.AssetBalance, + sender, receiver *rpcClient, bitcoinClient *rpcclient.Client, + timeout time.Duration) { + + // Look over the sender's balances to see if any asset balance qualifies + // for this send. + var ( + assetID []byte + balance *taprpc.AssetBalance + ) + for _, v := range assets { + if v.Balance >= numAssets && + v.AssetGenesis.AssetType == assetType { + + assetID = v.AssetGenesis.AssetId + balance = v + + break + } + } + + // No balance satisfies the amount of this send, we can skip this round. + if assetID == nil { + t.Logf("%s could not send %v assets, no available balance", + sender.cfg.Name, numAssets) + + return + } + + t.Logf("%s sending %v assets to %s", sender.cfg.Name, numAssets, + receiver.cfg.Name) + + // Receiver creates the address to receive the assets. + addr, err := receiver.NewAddr(ctx, &taprpc.NewAddrRequest{ + AssetId: assetID, + Amt: numAssets, + ProofCourierAddr: fmt.Sprintf( + "%s://%s:%d", proof.UniverseRpcCourierType, + sender.cfg.Host, sender.cfg.Port, + ), + }) + require.NoError(t, err) + + t.Logf("%s created address %v", receiver.cfg.Name, addr.String()) + + // Sender initiates the send. + _, err = sender.SendAsset(ctx, &taprpc.SendAssetRequest{ + TapAddrs: []string{addr.Encoded}, + }) + require.NoError(t, err) + t.Logf("%s sent assets to address %v", sender.cfg.Name, addr.String()) + + // We assert the receiver detects the spend. + itest.AssertAddrEventCustomTimeout( + t, receiver, addr, 1, statusDetected, timeout, + ) + t.Logf("%s detected send", receiver.cfg.Name) + + // Mine a block to confirm the transfer. + itest.MineBlocks(t, bitcoinClient, 1, 0) + t.Log("Mined 1 block") + + // Assert that the transfer is now completed + itest.AssertAddrEventCustomTimeout( + t, receiver, addr, 1, statusCompleted, timeout, + ) + t.Logf("%s completed send of %v assets", sender.cfg.Name, numAssets) + + // If everything completed correctly, subtract the asset amount from the + // sender's asset balance. + balance.Balance -= numAssets +} + +// 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, diff --git a/itest/loadtest/utils.go b/itest/loadtest/utils.go index 1f61c3805..f8602c927 100644 --- a/itest/loadtest/utils.go +++ b/itest/loadtest/utils.go @@ -289,3 +289,15 @@ func getBitcoinConn(t *testing.T, cfg *BitcoinConfig) *rpcclient.Client { return client } + +// stringToAssetType converts a string of an asset type to its respective taprpc +// type enum value. +func stringToAssetType(t string) taprpc.AssetType { + switch t { + case "collectible": + return taprpc.AssetType_COLLECTIBLE + + default: + return taprpc.AssetType_NORMAL + } +} diff --git a/itest/utils.go b/itest/utils.go index 88ee39931..0b30cb1d7 100644 --- a/itest/utils.go +++ b/itest/utils.go @@ -42,6 +42,11 @@ var ( regtestParams = &chaincfg.RegressionNetParams ) +const ( + SyncModeIssuance = universerpc.UniverseSyncMode_SYNC_ISSUANCE_ONLY + SyncModeFull = universerpc.UniverseSyncMode_SYNC_FULL +) + // ClientEventStream is a generic interface for a client stream that allows us // to receive events from a server. type ClientEventStream[T any] interface { @@ -834,15 +839,45 @@ func MintAssetExternalSigner(t *harnessTest, tapNode *tapdHarness, return batchAssets } +// syncOptions is a struct that is used to customize the way we perform a +// universe sync. +type syncOptions struct { + syncMode universerpc.UniverseSyncMode +} + +// defaultSyncOptions returns the default syncOptions. +func defaultSyncOptions() *syncOptions { + return &syncOptions{ + syncMode: SyncModeIssuance, + } +} + +// SyncUniverseOpt is used to modify the parameters of a universe sync. +type SyncUniverseOpt func(*syncOptions) + +// WithSyncMode can be used to define which sync mode to be used when performing +// a universe sync. +func WithSyncMode(mode universerpc.UniverseSyncMode) SyncUniverseOpt { + return func(so *syncOptions) { + so.syncMode = mode + } +} + // SyncUniverses syncs the universes of two tapd instances and waits until they // are in sync. func SyncUniverses(ctx context.Context, t *testing.T, clientTapd, universeTapd commands.RpcClientsBundle, universeHost string, - timeout time.Duration) { + timeout time.Duration, opts ...SyncUniverseOpt) { ctxt, cancel := context.WithTimeout(ctx, timeout) defer cancel() + options := defaultSyncOptions() + + for _, opt := range opts { + opt(options) + } + _, err := clientTapd.AddFederationServer( ctxt, &universerpc.AddFederationServerRequest{ Servers: []*universerpc.UniverseFederationServer{ @@ -863,10 +898,9 @@ func SyncUniverses(ctx context.Context, t *testing.T, clientTapd, // If we've already added the server in a previous run, we'll // just need to kick off a sync (as that would otherwise be done // by adding the server request already). - mode := universerpc.UniverseSyncMode_SYNC_ISSUANCE_ONLY _, err := clientTapd.SyncUniverse(ctxt, &universerpc.SyncRequest{ UniverseHost: universeHost, - SyncMode: mode, + SyncMode: options.syncMode, }) require.NoError(t, err) }