Skip to content

Commit

Permalink
itest+universe: add batch mint stress test
Browse files Browse the repository at this point in the history
In this commit, we add a test that mints a large batch of collectibles
with large metadata, and then checks that the batch mint succeeded.
This includes correctly updating the universe server of the minting
node, and syncing that universe tree to a second node.
  • Loading branch information
jharveyb committed Jun 13, 2023
1 parent b299962 commit 8ccece5
Show file tree
Hide file tree
Showing 17 changed files with 313 additions and 48 deletions.
4 changes: 2 additions & 2 deletions itest/addrs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func testAddresses(t *harnessTest) {
// for multiple internal asset transfers when only sending one of them
// to an external address.
rpcAssets := mintAssetsConfirmBatch(
t, t.tapd, []*mintrpc.MintAssetRequest{
t, t.tapd, nil, []*mintrpc.MintAssetRequest{
simpleAssets[0], issuableAssets[0],
},
)
Expand Down Expand Up @@ -130,7 +130,7 @@ func testAddresses(t *harnessTest) {
func testMultiAddress(t *harnessTest) {
// First, mint an asset, so we have one to create addresses for.
rpcAssets := mintAssetsConfirmBatch(
t, t.tapd, []*mintrpc.MintAssetRequest{
t, t.tapd, nil, []*mintrpc.MintAssetRequest{
simpleAssets[0], issuableAssets[0],
},
)
Expand Down
48 changes: 48 additions & 0 deletions itest/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
Expand Down Expand Up @@ -707,6 +708,53 @@ func assertListAssets(t *harnessTest, ctx context.Context, tapd *tapdHarness,
}
}

func assertUniverseRoot(t *testing.T, tapd *tapdHarness, sum int,
assetID []byte, groupKey []byte) error {

bothSet := assetID != nil && groupKey != nil
neitherSet := assetID == nil && groupKey == nil
if bothSet || neitherSet {
return fmt.Errorf("only set one of assetID or groupKey")
}

// Re-parse and serialize the keys to account for the different
// formats returned in RPC responses.
matchingGroupKey := func(root *unirpc.UniverseRoot) bool {
rootGroupKeyBytes := root.Id.GetGroupKey()
require.NotNil(t, rootGroupKeyBytes)

expectedGroupKey, err := btcec.ParsePubKey(groupKey)
require.NoError(t, err)
require.Equal(
t, rootGroupKeyBytes,
schnorr.SerializePubKey(expectedGroupKey),
)

return true
}

// Comparing the asset ID is always safe, even if nil.
matchingRoot := func(root *unirpc.UniverseRoot) bool {
require.Equal(t, root.MssmtRoot.RootSum, int64(sum))
require.Equal(t, root.Id.GetAssetId(), assetID)
if groupKey != nil {
return matchingGroupKey(root)
}

return true
}

ctx := context.Background()

uniRoots, err := tapd.AssetRoots(ctx, &unirpc.AssetRootRequest{})
require.NoError(t, err)

correctRoot := fn.Any(maps.Values(uniRoots.UniverseRoots), matchingRoot)
require.True(t, correctRoot)

return nil
}

func assertUniverseRootEqual(t *testing.T, a, b *unirpc.UniverseRoot) {
// The ids should batch exactly.
require.Equal(t, a.Id.Id, b.Id.Id)
Expand Down
22 changes: 15 additions & 7 deletions itest/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,10 @@ func copyRequests(reqs []*mintrpc.MintAssetRequest) []*mintrpc.MintAssetRequest
// testMintAssets tests that we're able to mint assets, retrieve their proofs
// and that we're able to import the proofs into a new node.
func testMintAssets(t *harnessTest) {
rpcSimpleAssets := mintAssetsConfirmBatch(t, t.tapd, simpleAssets)
rpcIssuableAssets := mintAssetsConfirmBatch(t, t.tapd, issuableAssets)
rpcSimpleAssets := mintAssetsConfirmBatch(t, t.tapd, nil, simpleAssets)
rpcIssuableAssets := mintAssetsConfirmBatch(
t, t.tapd, nil, issuableAssets,
)

// Now that all our assets have been issued, we'll use the balance
// calls to ensure that we're able to retrieve the proper balance for
Expand Down Expand Up @@ -134,6 +136,7 @@ func testMintAssets(t *harnessTest) {
// mintAssetsConfirmBatch mints all given assets in the same batch, confirms the
// batch and verifies all asset proofs of the minted assets.
func mintAssetsConfirmBatch(t *harnessTest, tapd *tapdHarness,
mempoolTimeout *time.Duration,
assetRequests []*mintrpc.MintAssetRequest) []*taprpc.Asset {

ctxb := context.Background()
Expand All @@ -154,12 +157,17 @@ func mintAssetsConfirmBatch(t *harnessTest, tapd *tapdHarness,
require.NoError(t.t, err)
require.NotEmpty(t.t, batchResp.BatchKey)

mempoolWaitTimeout := defaultWaitTimeout
if mempoolTimeout != nil {
mempoolWaitTimeout = *mempoolTimeout
}

waitForBatchState(
t, ctxt, tapd, defaultWaitTimeout,
t, ctxt, tapd, mempoolWaitTimeout,
mintrpc.BatchState_BATCH_STATE_BROADCAST,
)
hashes, err := waitForNTxsInMempool(
t.lndHarness.Miner.Client, 1, defaultWaitTimeout,
t.lndHarness.Miner.Client, 1, mempoolWaitTimeout,
)
require.NoError(t.t, err)

Expand Down Expand Up @@ -189,7 +197,7 @@ func mintAssetsConfirmBatch(t *harnessTest, tapd *tapdHarness,
block := mineBlocks(t, t.lndHarness, 1, 1)[0]
blockHash := block.BlockHash()
waitForBatchState(
t, ctxt, tapd, defaultWaitTimeout,
t, ctxt, tapd, mempoolWaitTimeout,
mintrpc.BatchState_BATCH_STATE_FINALIZED,
)

Expand Down Expand Up @@ -442,7 +450,7 @@ func testMintAssetNameCollisionError(t *harnessTest) {
},
}
rpcSimpleAssets := mintAssetsConfirmBatch(
t, t.tapd, []*mintrpc.MintAssetRequest{&assetMint},
t, t.tapd, nil, []*mintrpc.MintAssetRequest{&assetMint},
)

// Ensure minted asset with requested name was successfully minted.
Expand Down Expand Up @@ -535,7 +543,7 @@ func testMintAssetNameCollisionError(t *harnessTest) {
// Minting the asset with the name collision should work, even though
// it is also part of a cancelled batch.
rpcCollideAsset := mintAssetsConfirmBatch(
t, t.tapd, []*mintrpc.MintAssetRequest{&assetCollide},
t, t.tapd, nil, []*mintrpc.MintAssetRequest{&assetCollide},
)

collideAssetName := rpcCollideAsset[0].AssetGenesis.Name
Expand Down
2 changes: 1 addition & 1 deletion itest/collectible_split_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
func testCollectibleSend(t *harnessTest) {
// First, we'll make a collectible with emission enabled.
rpcAssets := mintAssetsConfirmBatch(
t, t.tapd, []*mintrpc.MintAssetRequest{
t, t.tapd, nil, []*mintrpc.MintAssetRequest{
issuableAssets[1],
// Our "passive" asset.
{
Expand Down
2 changes: 1 addition & 1 deletion itest/full_value_split_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func testFullValueSend(t *harnessTest) {
// First, we'll make an normal assets with enough units to allow us to
// send it around a few times.
rpcAssets := mintAssetsConfirmBatch(
t, t.tapd, []*mintrpc.MintAssetRequest{
t, t.tapd, nil, []*mintrpc.MintAssetRequest{
simpleAssets[0], issuableAssets[0],
},
)
Expand Down
182 changes: 182 additions & 0 deletions itest/mint_batch_stress_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package itest

import (
"context"
"encoding/binary"
"encoding/hex"
"os"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/taprpc"
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc"
"github.com/stretchr/testify/require"
)

const (
// testDataFileName is the name of the directory with the test data.
testDataFileName = "testdata"
)

var (
// Raw data of Cryptopunk 0 saved as a PNG.
ImageMetadataFileName = filepath.Join(
testDataFileName, "8k-metadata.hex",
)
)

func testMintBatch100StressTest(t *harnessTest) {
mintBatchStressTest(t, 100, nil)
}

func testMintBatch1kStressTest(t *harnessTest) {
minterTimeout := defaultWaitTimeout * 2
mintBatchStressTest(t, 1000, &minterTimeout)
}

func testMintBatch10kStressTest(t *harnessTest) {
minterTimeout := defaultWaitTimeout * 2
mintBatchStressTest(t, 10000, &minterTimeout)
}

func mintBatchStressTest(t *harnessTest, batchSize int,
minterTimeout *time.Duration) {

// Read base metadata.
imageMetadataHex, err := os.ReadFile(ImageMetadataFileName)
require.NoError(t.t, err)

imageMetadataBytes, err := hex.DecodeString(
strings.Trim(string(imageMetadataHex), "\n"),
)
require.NoError(t.t, err)

var (
batchReqs = make([]*mintrpc.MintAssetRequest, batchSize)
baseName = "jpeg"
metaPrefixSize = binary.MaxVarintLen16
metadataPrefix = make([]byte, metaPrefixSize)
)

// Each asset in the batch will share a name and metdata preimage, that
// will be updated based on the asset's index in the batch.
collectibleRequestTemplate := mintrpc.MintAssetRequest{
Asset: &mintrpc.MintAsset{
AssetType: taprpc.AssetType_COLLECTIBLE,
Name: baseName,
AssetMeta: &taprpc.AssetMeta{
Data: imageMetadataBytes,
Type: 0,
},
Amount: 1,
},
EnableEmission: false,
}

// Update the asset name and metadata to match an index.
incrementMintAsset := func(asset *mintrpc.MintAsset, ind int) {
asset.Name = asset.Name + strconv.Itoa(ind)
binary.PutUvarint(metadataPrefix, uint64(ind))
copy(asset.AssetMeta.Data[0:metaPrefixSize], metadataPrefix)
}

// Use the first asset of the batch as the asset group anchor.
collectibleAnchorReq := copyRequest(&collectibleRequestTemplate)
incrementMintAsset(collectibleAnchorReq.Asset, 0)
collectibleAnchorReq.EnableEmission = true
batchReqs[0] = collectibleAnchorReq

// Generate the rest of the batch, with each asset referencing the group
// anchor we created above.
for i := 1; i < batchSize; i++ {
groupedAsset := copyRequest(&collectibleRequestTemplate)
incrementMintAsset(groupedAsset.Asset, i)
groupedAsset.Asset.GroupAnchor = collectibleAnchorReq.Asset.Name
batchReqs[i] = groupedAsset
}

// Submit the batch for minting. Use an extended timeout for the TX
// appearing in the mempool so we can observe the minter hitting its own
// shorter default timeout.
t.LogfTimestamped("beginning minting of batch of %d assets", batchSize)
mintBatch := mintAssetsConfirmBatch(t, t.tapd, minterTimeout, batchReqs)
t.LogfTimestamped("finished batch mint of %d assets", batchSize)

// We can re-derive the group key to verify that the correct asset was
// used as the group anchor.
collectibleAnchor := verifyGroupAnchor(
t.t, mintBatch, collectibleAnchorReq.Asset.Name,
)
collectGroupKey := collectibleAnchor.AssetGroup.TweakedGroupKey
collectGroupKeyStr := hex.EncodeToString(collectGroupKey[:])

// We should have one group, with the specified number of assets and an
// equivalent balance, since the group is made of collectibles.
groupCount := 1
groupBalance := batchSize
assertNumGroups(t.t, t.tapd, groupCount)
assertGroupSizes(
t.t, t.tapd, []string{collectGroupKeyStr}, []int{batchSize},
)
assertBalanceByGroup(
t.t, t.tapd, collectGroupKeyStr, uint64(groupBalance),
)

// The universe tree should reflect the same properties about the batch;
// there should be one root with a group key and balance matching what
// we asserted previously.
ctx := context.Background()
uniRoots, err := t.tapd.AssetRoots(ctx, &unirpc.AssetRootRequest{})
require.NoError(t.t, err)
require.Len(t.t, uniRoots.UniverseRoots, groupCount)

assertUniverseRoot(t.t, t.tapd, groupBalance, nil, collectGroupKey)

// The universe tree should also have a leaf for each asset minted.
// TODO(jhb): Resolve issue of 33-byte group key handling.
collectUniID := unirpc.ID{
Id: &unirpc.ID_GroupKey{
GroupKey: collectGroupKey[1:],
},
}
uniLeaves, err := t.tapd.AssetLeaves(ctx, &collectUniID)
require.NoError(t.t, err)
require.Len(t.t, uniLeaves.Leaves, batchSize)

// The universe tree should also have a key for each asset, with all
// outpoints matching the chain anchor of the group anchor.
mintOutpoint := collectibleAnchor.ChainAnchor.AnchorOutpoint
uniKeys, err := t.tapd.AssetLeafKeys(ctx, &collectUniID)
require.NoError(t.t, err)
require.Len(t.t, uniKeys.AssetKeys, batchSize)

correctOp := fn.All(uniKeys.AssetKeys, func(key *unirpc.AssetKey) bool {
return key.GetOpStr() == mintOutpoint
})
require.True(t.t, correctOp)

// If we create a second tapd instance and sync the universe state,
// the synced tree should match the source tree.
bob := setupTapdHarness(
t.t, t, t.lndHarness.Bob, nil,
)
defer func() {
require.NoError(t.t, bob.stop(true))
}()

_, err = bob.AddFederationServer(
ctx, &unirpc.AddFederationServerRequest{
Servers: []*unirpc.UniverseFederationServer{
{
Host: t.tapd.rpcHost(),
},
},
},
)
require.NoError(t.t, err)
assertUniverseStateEqual(t.t, t.tapd, bob)
}
6 changes: 4 additions & 2 deletions itest/multi_asset_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func testMintMultiAssetGroups(t *harnessTest) {
// The minted batch should contain 7 assets total, and the daemon should
// now be aware of 3 asset groups. Each group should have a different
// number of assets, and a different total balance.
mintedBatch := mintAssetsConfirmBatch(t, t.tapd, complexBatch)
mintedBatch := mintAssetsConfirmBatch(t, t.tapd, nil, complexBatch)

// Once the batch is minted, we can verify that all asset groups were
// created correctly. We begin by verifying the number of asset groups.
Expand Down Expand Up @@ -280,7 +280,9 @@ func testMintMultiAssetGroupErrors(t *harnessTest) {
multiAssetGroup := []*mintrpc.MintAssetRequest{validAnchor, groupedAsset}

// The assets should be minted into the same group.
rpcGroupedAssets := mintAssetsConfirmBatch(t, t.tapd, multiAssetGroup)
rpcGroupedAssets := mintAssetsConfirmBatch(
t, t.tapd, nil, multiAssetGroup,
)
assertNumGroups(t.t, t.tapd, 1)
groupKey := rpcGroupedAssets[0].AssetGroup.TweakedGroupKey
groupKeyHex := hex.EncodeToString(groupKey)
Expand Down
Loading

0 comments on commit 8ccece5

Please sign in to comment.