Skip to content

Commit 2352d53

Browse files
authored
Merge pull request #1352 from GeorgeTsagk/sync-loadtest
Add `sync` loadtest
2 parents d3b34d2 + 27432c8 commit 2352d53

File tree

5 files changed

+285
-3
lines changed

5 files changed

+285
-3
lines changed

itest/loadtest/config.go

+20-3
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ type User struct {
2727

2828
// TapConfig are the main parameters needed for identifying and creating a grpc
2929
// client to a tapd subsystem.
30+
//
31+
// nolint:lll
3032
type TapConfig struct {
31-
Name string `long:"name" description:"the name of the tapd instance"`
32-
Host string `long:"host" description:"the host to connect to"`
33-
Port int `long:"port" description:"the port to connect to"`
33+
Name string `long:"name" description:"the name of the tapd instance"`
34+
Host string `long:"host" description:"the host to connect to"`
35+
Port int `long:"port" description:"the port to connect to"`
36+
RestPort int `long:"restport" description:"the rest port to connect to"`
3437

3538
TLSPath string `long:"tlspath" description:"Path to tapd's TLS certificate, leave empty if TLS is disabled"`
3639
MacPath string `long:"macpath" description:"Path to tapd's macaroon file"`
@@ -115,6 +118,20 @@ type Config struct {
115118
// "collectible".
116119
SendAssetType string `long:"send-asset-type" description:"the type of asset to attempt to send; only relevant for the send test"`
117120

121+
// SyncType is the type of sync to execute in the sync test. Acceptable
122+
// values include:
123+
// "simplesyncer": re-uses simple syncer to perform a full sync
124+
// "rest": syncs the roots over rest requests
125+
SyncType string `long:"sync-type" description:"the type of sync to execute"`
126+
127+
// SyncPageSize is the page size to use in the sync test for calls made
128+
// to the universe server.
129+
SyncPageSize int `long:"sync-page-size" description:"the page size to use in the sync test when fetching data from the universe server"`
130+
131+
// SyncNumClients is the number of clients to use in the sync test. This
132+
// many clients will try to sync in parallel.
133+
SyncNumClients int `long:"sync-num-clients" description:"the number of sync clients to use for the sync test"`
134+
118135
// TestSuiteTimeout is the timeout for the entire test suite.
119136
TestSuiteTimeout time.Duration `long:"test-suite-timeout" description:"the timeout for the entire test suite"`
120137

itest/loadtest/load_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ var loadTestCases = []testCase{
5555
name: "multisig",
5656
fn: multisigTest,
5757
},
58+
{
59+
name: "sync",
60+
fn: syncTest,
61+
},
5862
}
5963

6064
// TestPerformance executes the configured performance tests.

itest/loadtest/loadtest-sample.conf

+14
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ send-test-num-assets=1
2626
# required.
2727
send-asset-type="normal"
2828

29+
# SyncType is the type of sync to execute in the sync test. Acceptable
30+
# values include:
31+
# "simplesyncer": re-uses simple syncer to perform a full sync
32+
# "rest": syncs the roots over rest requests
33+
sync-type="simplesyncer"
34+
35+
# Number of clients to perform a sync in sync test.
36+
sync-num-clients=16
37+
38+
# Page size to be used when requesting data from universe in sync test.
39+
sync-page-size=128
40+
2941
# Timeout for the entire test suite
3042
test-suite-timeout=120m
3143

@@ -43,6 +55,7 @@ bitcoin.password=lightning
4355
alice.tapd.name=alice
4456
alice.tapd.host="localhost"
4557
alice.tapd.port=XXX
58+
alice.tapd.restport=XXX
4659
alice.tapd.tlspath=/path/to/tls.cert
4760
alice.tapd.macpath=/path/to/admin.macaroon
4861
alice.lnd.name=alice_lnd
@@ -55,6 +68,7 @@ alice.lnd.macpath=/path/to/admin.macaroon
5568
bob.tapd.name=bob
5669
bob.tapd.host="localhost"
5770
bob.tapd.port=XXX
71+
bob.tapd.restport=XXX
5872
bob.tapd.tlspath=/path/to/tls.cert
5973
bob.tapd.macpath=/path/to/admin.macaroon
6074
bob.lnd.name=bob_lnd

itest/loadtest/sync_test.go

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package loadtest
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"strings"
10+
"sync"
11+
"testing"
12+
13+
tap "github.com/lightninglabs/taproot-assets"
14+
"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
15+
"github.com/lightninglabs/taproot-assets/universe"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
// syncTest checks that the universe server can handle multiple requests to its
20+
// public universe endpoints. The number of clients is configurable.
21+
func syncTest(t *testing.T, ctx context.Context, cfg *Config) {
22+
alice := initAlice(t, ctx, cfg)
23+
24+
// Let's start by logging the aggregate universe stats.
25+
res, err := alice.UniverseStats(ctx, &universerpc.StatsRequest{})
26+
require.NoError(t, err)
27+
28+
t.Logf("Universe Aggregate Stats: %+v", res)
29+
30+
// We'll use this wait group to block until all clients are done.
31+
var wg sync.WaitGroup
32+
33+
// We dispatch a client sync for the configured number of clients.
34+
for i := range cfg.SyncNumClients {
35+
switch cfg.SyncType {
36+
case "simplesyncer":
37+
wg.Add(1)
38+
go simpleSyncer(t, ctx, cfg, i, &wg)
39+
40+
case "rest":
41+
wg.Add(1)
42+
go syncAssetRootsREST(t, ctx, cfg, i, &wg)
43+
}
44+
}
45+
46+
wg.Wait()
47+
}
48+
49+
// simpleSyncer creates a simple syncer instance and uses Alice as the remote
50+
// diff engine. It always triggers a full sync as the local diff engine returns
51+
// an empty leaf node.
52+
func simpleSyncer(t *testing.T, ctx context.Context, cfg *Config, id int,
53+
wg *sync.WaitGroup) {
54+
55+
defer wg.Done()
56+
57+
// Alice is always serving as the universe server.
58+
uniURL := fmt.Sprintf("%s:%v", cfg.Alice.Tapd.Host, cfg.Alice.Tapd.Port)
59+
60+
syncer := universe.NewSimpleSyncer(
61+
universe.SimpleSyncCfg{
62+
LocalDiffEngine: noopBaseUni{},
63+
NewRemoteDiffEngine: tap.NewRpcUniverseDiff,
64+
LocalRegistrar: noopBaseUni{},
65+
SyncBatchSize: 512,
66+
},
67+
)
68+
69+
t.Logf("SimpleSyncer-%02d: Starting full universe sync", id)
70+
71+
// We don't care about the diff, so we only check if an error occurred,
72+
// otherwise the sync completed.
73+
_, err := syncer.SyncUniverse(
74+
ctx, universe.NewServerAddrFromStr(uniURL),
75+
universe.SyncFull, universe.SyncConfigs{
76+
GlobalSyncConfigs: []*universe.FedGlobalSyncConfig{
77+
{
78+
// nolint:lll
79+
ProofType: universe.ProofTypeIssuance,
80+
AllowSyncInsert: true,
81+
},
82+
},
83+
},
84+
)
85+
require.NoError(t, err)
86+
87+
t.Logf("SimpleSyncer-%02d: Completed full universe sync", id)
88+
}
89+
90+
// syncAssetRootsREST performs a series of requests to the AssetRoots endpoint
91+
// of the universe server. It automatically progresses the requested page until
92+
// the whole data is read.
93+
func syncAssetRootsREST(t *testing.T, ctx context.Context, cfg *Config,
94+
id int, wg *sync.WaitGroup) {
95+
96+
defer wg.Done()
97+
var (
98+
limit = cfg.SyncPageSize
99+
offset = 0
100+
)
101+
102+
for {
103+
// This is the URL of the universe server, in our case that's
104+
// always Alice.
105+
baseURL := fmt.Sprintf(
106+
"https://%s:%v/v1/taproot-assets/universe/roots",
107+
cfg.Alice.Tapd.Host, cfg.Alice.Tapd.RestPort,
108+
)
109+
110+
// We inject the pagination GET params.
111+
fullURL := fmt.Sprintf(
112+
"%s?offset=%v&limit=%v", baseURL, offset, limit,
113+
)
114+
115+
t.Logf("Syncer%02d: Fetching AssetRoots offset=%v, limit=%v",
116+
id, offset, limit)
117+
118+
res := getAssetRoots(t, ctx, fullURL)
119+
120+
// In order to count the length of the response without doing
121+
// JSON parsing, we simply count the occurences of a top-level
122+
// field name that repeats for all entries in the array.
123+
len := strings.Count(res, "mssmt_root")
124+
125+
// Break if we reached the end. This is signalled by retrieving
126+
// less entities than what was defined as the max limit,
127+
// meaning that there's nothing left to consume.
128+
if len < limit {
129+
break
130+
}
131+
132+
offset += limit
133+
}
134+
}
135+
136+
// getAssetRoots performs a GET request to the AssetRoots REST endpoint of the
137+
// universe server. We don't care about handling the response, we just hit the
138+
// endpoint and return the text of the body.
139+
func getAssetRoots(t *testing.T, ctx context.Context, fullURL string) string {
140+
tr := &http.Transport{
141+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
142+
}
143+
144+
client := &http.Client{Transport: tr}
145+
146+
req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
147+
require.NoError(t, err)
148+
149+
resp, err := client.Do(req)
150+
require.NoError(t, err)
151+
defer resp.Body.Close()
152+
153+
body, err := io.ReadAll(resp.Body)
154+
require.NoError(t, err)
155+
156+
return string(body)
157+
}

itest/loadtest/utils.go

+90
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ import (
1414
"github.com/btcsuite/btcd/rpcclient"
1515
tap "github.com/lightninglabs/taproot-assets"
1616
"github.com/lightninglabs/taproot-assets/cmd/commands"
17+
"github.com/lightninglabs/taproot-assets/fn"
1718
"github.com/lightninglabs/taproot-assets/itest"
19+
"github.com/lightninglabs/taproot-assets/mssmt"
1820
"github.com/lightninglabs/taproot-assets/taprpc"
1921
"github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
2022
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
2123
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
2224
tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
2325
"github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc"
2426
"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
27+
"github.com/lightninglabs/taproot-assets/universe"
2528
"github.com/lightningnetwork/lnd/lntest/rpc"
2629
"github.com/lightningnetwork/lnd/macaroons"
2730
"github.com/stretchr/testify/require"
@@ -100,6 +103,16 @@ func (r *rpcClient) listTransfersSince(t *testing.T, ctx context.Context,
100103
return resp.Transfers[newIndex:]
101104
}
102105

106+
// initAlice is similar to initClients, but only returns the Alice client.
107+
func initAlice(t *testing.T, ctx context.Context, cfg *Config) *rpcClient {
108+
alice := getTapClient(t, ctx, cfg.Alice.Tapd, cfg.Alice.Lnd)
109+
110+
_, err := alice.GetInfo(ctx, &taprpc.GetInfoRequest{})
111+
require.NoError(t, err)
112+
113+
return alice
114+
}
115+
103116
func initClients(t *testing.T, ctx context.Context,
104117
cfg *Config) (*rpcClient, *rpcClient, *rpcclient.Client) {
105118

@@ -301,3 +314,80 @@ func stringToAssetType(t string) taprpc.AssetType {
301314
return taprpc.AssetType_NORMAL
302315
}
303316
}
317+
318+
// noopBaseUni is a dummy implementation of the universe.DiffEngine and
319+
// universe.LocalRegistrar interfaces. This is meant to be used by the simple
320+
// syncer used in the sync loadtest. As we don't care about persistence and we
321+
// always want to do a full sync, we always return an empty root node to trigger
322+
// a sync.
323+
type noopBaseUni struct{}
324+
325+
// RootNode returns the root node of the base universe corresponding to the
326+
// passed ID.
327+
func (n noopBaseUni) RootNode(ctx context.Context,
328+
id universe.Identifier) (universe.Root, error) {
329+
330+
return universe.Root{
331+
Node: mssmt.EmptyLeafNode,
332+
}, nil
333+
}
334+
335+
// RootNodes returns the set of root nodes for all known base universes assets.
336+
func (n noopBaseUni) RootNodes(ctx context.Context,
337+
q universe.RootNodesQuery) ([]universe.Root, error) {
338+
339+
return nil, nil
340+
}
341+
342+
// MultiverseRoot returns the root node of the multiverse for the specified
343+
// proof type. If the given list of universe IDs is non-empty, then the root
344+
// will be calculated just for those universes.
345+
func (n *noopBaseUni) MultiverseRoot(ctx context.Context,
346+
proofType universe.ProofType,
347+
filterByIDs []universe.Identifier) (fn.Option[universe.MultiverseRoot],
348+
error) {
349+
350+
return fn.None[universe.MultiverseRoot](), nil
351+
}
352+
353+
// UpsertProofLeaf attempts to upsert a proof for an asset issuance or transfer
354+
// event. This method will return an error if the passed proof is invalid. If
355+
// the leaf is already known, then no action is taken and the existing
356+
// commitment proof returned.
357+
func (n noopBaseUni) UpsertProofLeaf(ctx context.Context,
358+
id universe.Identifier, key universe.LeafKey,
359+
leaf *universe.Leaf) (*universe.Proof, error) {
360+
361+
return nil, nil
362+
}
363+
364+
// UpsertProofLeafBatch inserts a batch of proof leaves within the target
365+
// universe tree. We assume the proofs within the batch have already been
366+
// checked that they don't yet exist in the local database.
367+
func (n noopBaseUni) UpsertProofLeafBatch(ctx context.Context,
368+
items []*universe.Item) error {
369+
370+
return nil
371+
}
372+
373+
// Close closes the noopBaseUni, stopping all goroutines and freeing all
374+
// resources.
375+
func (n noopBaseUni) Close() error {
376+
return nil
377+
}
378+
379+
// FetchProofLeaf attempts to fetch a proof leaf for the target leaf key
380+
// and given a universe identifier (assetID/groupKey).
381+
func (n noopBaseUni) FetchProofLeaf(ctx context.Context, id universe.Identifier,
382+
key universe.LeafKey) ([]*universe.Proof, error) {
383+
384+
return nil, nil
385+
}
386+
387+
// UniverseLeafKeys returns the set of leaf keys known for the specified
388+
// universe identifier.
389+
func (n noopBaseUni) UniverseLeafKeys(ctx context.Context,
390+
q universe.UniverseLeafKeysQuery) ([]universe.LeafKey, error) {
391+
392+
return nil, nil
393+
}

0 commit comments

Comments
 (0)