Skip to content

Commit a1e821b

Browse files
committed
itest: add supply commit mint/burn itests
1 parent 5d4767d commit a1e821b

File tree

4 files changed

+386
-0
lines changed

4 files changed

+386
-0
lines changed

itest/assertions.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2756,3 +2756,58 @@ func LargestUtxo(t *testing.T, client taprpc.TaprootAssetsClient,
27562756

27572757
return outputs[0]
27582758
}
2759+
2760+
// UpdateAndMineSupplyCommit updates the on-chain supply commitment for an asset
2761+
// group and mines the commitment transaction.
2762+
func UpdateAndMineSupplyCommit(t *testing.T, ctx context.Context,
2763+
tapd unirpc.UniverseClient, miner *rpcclient.Client,
2764+
groupKeyBytes []byte, expectedTxsInBlock int) []*wire.MsgBlock {
2765+
2766+
groupKeyUpdate := &unirpc.UpdateSupplyCommitRequest_GroupKeyBytes{
2767+
GroupKeyBytes: groupKeyBytes,
2768+
}
2769+
2770+
respUpdate, err := tapd.UpdateSupplyCommit(
2771+
ctx, &unirpc.UpdateSupplyCommitRequest{
2772+
GroupKey: groupKeyUpdate,
2773+
},
2774+
)
2775+
require.NoError(t, err)
2776+
require.NotNil(t, respUpdate)
2777+
2778+
// Mine the supply commitment transaction.
2779+
minedBlocks := MineBlocks(t, miner, 1, expectedTxsInBlock)
2780+
require.Len(t, minedBlocks, 1)
2781+
2782+
return minedBlocks
2783+
}
2784+
2785+
// WaitForSupplyCommit waits for a supply commitment to be available and returns
2786+
// it when the specified condition is met.
2787+
func WaitForSupplyCommit(t *testing.T, ctx context.Context,
2788+
tapd unirpc.UniverseClient, groupKeyBytes []byte,
2789+
condition func(*unirpc.FetchSupplyCommitResponse) bool,
2790+
) *unirpc.FetchSupplyCommitResponse {
2791+
2792+
groupKeyReq := &unirpc.FetchSupplyCommitRequest_GroupKeyBytes{
2793+
GroupKeyBytes: groupKeyBytes,
2794+
}
2795+
2796+
var fetchResp *unirpc.FetchSupplyCommitResponse
2797+
var err error
2798+
2799+
require.Eventually(t, func() bool {
2800+
fetchResp, err = tapd.FetchSupplyCommit(
2801+
ctx, &unirpc.FetchSupplyCommitRequest{
2802+
GroupKey: groupKeyReq,
2803+
},
2804+
)
2805+
if err != nil {
2806+
return false
2807+
}
2808+
2809+
return fetchResp != nil && condition(fetchResp)
2810+
}, defaultWaitTimeout, time.Second)
2811+
2812+
return fetchResp
2813+
}

itest/supply_commit_mint_burn_test.go

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
package itest
2+
3+
import (
4+
"bytes"
5+
"context"
6+
7+
"github.com/btcsuite/btcd/btcec/v2"
8+
"github.com/btcsuite/btcd/chaincfg/chainhash"
9+
taprootassets "github.com/lightninglabs/taproot-assets"
10+
"github.com/lightninglabs/taproot-assets/fn"
11+
"github.com/lightninglabs/taproot-assets/taprpc"
12+
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
13+
unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc"
14+
"github.com/lightninglabs/taproot-assets/universe/supplycommit"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
// testSupplyCommitMintBurn tests that supply commitment trees are correctly
19+
// updated when minting assets with group keys and burning outputs. It verifies:
20+
//
21+
// 1. Minting assets with EnableSupplyCommitments creates proper pre-commitment
22+
// outputs and updates the supply tree with mint leaves.
23+
// 2. Re-issuing assets to the same group updates the supply tree correctly.
24+
// 3. Burning assets creates burn leaves in the supply tree with negative
25+
// amounts.
26+
// 4. All operations produce valid inclusion proofs that can be verified.
27+
func testSupplyCommitMintBurn(t *harnessTest) {
28+
ctxb := context.Background()
29+
30+
t.Log("Minting initial asset group with universe/supply " +
31+
"commitments enabled")
32+
33+
// Create a mint request for a grouped asset with supply commitments.
34+
mintReq := CopyRequest(issuableAssets[0])
35+
mintReq.Asset.Amount = 5000
36+
37+
t.Log("Minting asset with supply commitments and verifying " +
38+
"pre-commitment")
39+
40+
rpcFirstAsset, delegationKey := MintAssetWithSupplyCommit(
41+
t, mintReq, fn.None[btcec.PublicKey](),
42+
)
43+
44+
// Parse out the group key from the minted asset, we'll use this later.
45+
groupKeyBytes := rpcFirstAsset.AssetGroup.TweakedGroupKey
46+
require.NotNil(t.t, groupKeyBytes)
47+
48+
// Update the on-chain supply commitment for the asset group.
49+
//
50+
// TODO(roasbeef): still rely on the time based ticker here?
51+
t.Log("Updating and mining supply commitment for asset group")
52+
UpdateAndMineSupplyCommit(
53+
t.t, ctxb, t.tapd, t.lndHarness.Miner().Client,
54+
groupKeyBytes, 1,
55+
)
56+
57+
// Fetch the latest supply commitment for the asset group.
58+
t.Log("Fetching supply commitment to verify mint leaves")
59+
fetchResp := WaitForSupplyCommit(
60+
t.t, ctxb, t.tapd, groupKeyBytes,
61+
func(resp *unirpc.FetchSupplyCommitResponse) bool {
62+
return resp.BlockHeight > 0 && len(resp.BlockHash) > 0
63+
},
64+
)
65+
66+
// Verify the issuance subtree root exists and has the correct amount.
67+
require.NotNil(t.t, fetchResp.IssuanceSubtreeRoot)
68+
require.Equal(
69+
t.t, int64(mintReq.Asset.Amount),
70+
fetchResp.IssuanceSubtreeRoot.RootNode.RootSum,
71+
)
72+
73+
// Verify the issuance leaf inclusion in the supply tree.
74+
AssertSubtreeInclusionProof(
75+
t, fetchResp.SupplyCommitmentRoot.RootHash,
76+
fetchResp.IssuanceSubtreeRoot,
77+
)
78+
79+
// Now we'll mint a second asset into the same group, this tests that
80+
// we're able to properly update the supply commitment with new mints.
81+
t.Log("Minting second tranche into the same asset group")
82+
83+
secondMintReq := &mintrpc.MintAssetRequest{
84+
Asset: &mintrpc.MintAsset{
85+
AssetType: taprpc.AssetType_NORMAL,
86+
Name: "itestbuxx-supply-commit-tranche-2",
87+
AssetMeta: &taprpc.AssetMeta{
88+
Data: []byte("second tranche metadata"),
89+
},
90+
Amount: 3000,
91+
AssetVersion: taprpc.AssetVersion_ASSET_VERSION_V1, //nolint:lll
92+
NewGroupedAsset: false,
93+
GroupedAsset: true,
94+
GroupKey: groupKeyBytes,
95+
EnableSupplyCommitments: true,
96+
},
97+
}
98+
rpcSecondAsset, _ := MintAssetWithSupplyCommit(
99+
t, secondMintReq, fn.Some(delegationKey),
100+
)
101+
102+
// Ensure both assets are in the same group.
103+
require.EqualValues(
104+
t.t, groupKeyBytes,
105+
rpcSecondAsset.AssetGroup.TweakedGroupKey,
106+
)
107+
108+
t.Log("Updating supply commitment after second mint")
109+
110+
// Update and mine the supply commitment after second mint.
111+
UpdateAndMineSupplyCommit(
112+
t.t, ctxb, t.tapd, t.lndHarness.Miner().Client,
113+
groupKeyBytes, 1,
114+
)
115+
116+
t.Log("Verifying supply tree includes both mint operations")
117+
118+
// Fetch and verify the updated supply includes both mints.
119+
expectedTotal := int64(
120+
mintReq.Asset.Amount + secondMintReq.Asset.Amount,
121+
)
122+
fetchResp = WaitForSupplyCommit(
123+
t.t, ctxb, t.tapd, groupKeyBytes,
124+
func(resp *unirpc.FetchSupplyCommitResponse) bool {
125+
return resp.IssuanceSubtreeRoot != nil &&
126+
resp.IssuanceSubtreeRoot.RootNode.RootSum == expectedTotal //nolint:lll
127+
},
128+
)
129+
130+
// Finally, we'll test burning assets from the group, and ensure that
131+
// the supply tree is updated with this information.
132+
t.Log("Burning assets from the group")
133+
134+
const (
135+
burnAmt = 1000
136+
burnNote = "supply commit burn test"
137+
)
138+
139+
burnResp, err := t.tapd.BurnAsset(ctxb, &taprpc.BurnAssetRequest{
140+
Asset: &taprpc.BurnAssetRequest_AssetId{
141+
AssetId: rpcFirstAsset.AssetGenesis.AssetId,
142+
},
143+
AmountToBurn: burnAmt,
144+
Note: burnNote,
145+
ConfirmationText: taprootassets.AssetBurnConfirmationText,
146+
})
147+
require.NoError(t.t, err)
148+
require.NotNil(t.t, burnResp)
149+
150+
t.Log("Confirming burn transaction")
151+
152+
// Confirm the burn transaction, asserting that all the expected records
153+
// on disk are in place.
154+
AssertAssetOutboundTransferWithOutputs(
155+
t.t, t.lndHarness.Miner().Client, t.tapd, burnResp.BurnTransfer,
156+
[][]byte{rpcFirstAsset.AssetGenesis.AssetId},
157+
[]uint64{mintReq.Asset.Amount - burnAmt, burnAmt},
158+
0, 1, 2, true,
159+
)
160+
161+
// Make sure that the burn is recognized in the burn records.
162+
burns := AssertNumBurns(t.t, t.tapd, 1, nil)
163+
burn := burns[0]
164+
require.Equal(t.t, uint64(burnAmt), burn.Amount)
165+
require.Equal(t.t, burnNote, burn.Note)
166+
167+
t.Log("Updating supply commitment after burn")
168+
169+
// Update and mine the supply commitment after burn.
170+
finalMinedBlocks := UpdateAndMineSupplyCommit(
171+
t.t, ctxb, t.tapd, t.lndHarness.Miner().Client,
172+
groupKeyBytes, 1,
173+
)
174+
175+
t.Log("Verifying supply tree includes burn leaves")
176+
177+
// Fetch and verify the supply tree now includes burn leaves.
178+
fetchResp = WaitForSupplyCommit(t.t, ctxb, t.tapd, groupKeyBytes,
179+
func(resp *unirpc.FetchSupplyCommitResponse) bool { //nolint:lll
180+
return resp.BurnSubtreeRoot != nil &&
181+
resp.BurnSubtreeRoot.RootNode.RootSum == int64(burnAmt)
182+
},
183+
)
184+
185+
// Verify the burn subtree inclusion in the supply tree.
186+
AssertSubtreeInclusionProof(
187+
t, fetchResp.SupplyCommitmentRoot.RootHash,
188+
fetchResp.BurnSubtreeRoot,
189+
)
190+
191+
t.Log("Fetching supply leaves for detailed verification")
192+
193+
// Fetch supply leaves to verify individual entries have all been
194+
// properly committed.
195+
respLeaves, err := t.tapd.FetchSupplyLeaves(
196+
ctxb, &unirpc.FetchSupplyLeavesRequest{
197+
GroupKey: &unirpc.FetchSupplyLeavesRequest_GroupKeyBytes{
198+
GroupKeyBytes: groupKeyBytes,
199+
},
200+
},
201+
)
202+
require.NoError(t.t, err)
203+
require.NotNil(t.t, respLeaves)
204+
205+
// Verify we have the expected issuance leaves (2 mints), and a single
206+
// burn leaf.
207+
require.Equal(
208+
t.t, len(respLeaves.IssuanceLeaves), 2,
209+
"expected at least 2 issuance leaves",
210+
)
211+
require.Equal(
212+
t.t, len(respLeaves.BurnLeaves), 1,
213+
"expected at least 1 burn leaf",
214+
)
215+
216+
// Make sure that the burn leaf has the proper amount.
217+
foundBurn := false
218+
for _, burnLeaf := range respLeaves.BurnLeaves {
219+
if burnLeaf.LeafNode.RootSum == int64(burnAmt) {
220+
221+
foundBurn = true
222+
223+
require.True(t.t, bytes.Equal(
224+
rpcFirstAsset.AssetGenesis.AssetId,
225+
burnLeaf.LeafKey.AssetId,
226+
), "burn leaf asset ID mismatch")
227+
break
228+
}
229+
}
230+
require.True(t.t, foundBurn, "expected burn leaf not found")
231+
232+
// Finally, we'll verify that the final supply commitment has the
233+
// pkScript that we expect.
234+
require.Len(t.t, finalMinedBlocks, 1, "expected one mined block")
235+
block := finalMinedBlocks[0]
236+
blockHash, _ := t.lndHarness.Miner().GetBestBlock()
237+
238+
fetchBlockHash, err := chainhash.NewHash(fetchResp.BlockHash)
239+
require.NoError(t.t, err)
240+
require.True(t.t, fetchBlockHash.IsEqual(blockHash))
241+
242+
// Re-compute the supply commitment root hash from the latest fetch,
243+
// then use that to derive the expected commitment output.
244+
supplyCommitRootHash := fn.ToArray[[32]byte](
245+
fetchResp.SupplyCommitmentRoot.RootHash,
246+
)
247+
internalKey, err := btcec.ParsePubKey(fetchResp.AnchorTxOutInternalKey)
248+
require.NoError(t.t, err)
249+
expectedTxOut, _, err := supplycommit.RootCommitTxOut(
250+
internalKey, nil, supplyCommitRootHash,
251+
)
252+
require.NoError(t.t, err)
253+
254+
foundCommitTxOut := false
255+
for _, tx := range block.Transactions {
256+
for _, txOut := range tx.TxOut {
257+
pkScriptMatch := bytes.Equal(
258+
txOut.PkScript, expectedTxOut.PkScript,
259+
)
260+
if txOut.Value == expectedTxOut.Value && pkScriptMatch {
261+
foundCommitTxOut = true
262+
break
263+
}
264+
}
265+
if foundCommitTxOut {
266+
break
267+
}
268+
}
269+
require.True(
270+
t.t, foundCommitTxOut,
271+
"supply commitment tx output not found in block",
272+
)
273+
274+
t.Log("Supply commit mint and burn test completed successfully")
275+
}

itest/supply_commit_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,8 @@ func testSupplyCommitIgnoreAsset(t *harnessTest) {
450450
func AssertInclusionProof(t *harnessTest, expectedRootHash [32]byte,
451451
inclusionProofBytes []byte, leafKey [32]byte, leafNode mssmt.Node) {
452452

453+
t.t.Helper()
454+
453455
// Decode the inclusion proof bytes into a compressed proof.
454456
var compressedProof mssmt.CompressedProof
455457
err := compressedProof.Decode(bytes.NewReader(inclusionProofBytes))
@@ -469,3 +471,53 @@ func AssertInclusionProof(t *harnessTest, expectedRootHash [32]byte,
469471
expectedRootHash[:], derivedRootHash)
470472
}
471473
}
474+
475+
// AssertSubtreeInclusionProof verifies that a subtree is properly included in
476+
// the supply commitment tree by checking the inclusion proof.
477+
func AssertSubtreeInclusionProof(t *harnessTest,
478+
supplyRootHash []byte, subtreeRoot *unirpc.SupplyCommitSubtreeRoot) {
479+
480+
require.NotNil(t.t, subtreeRoot)
481+
482+
// Convert to fixed-size arrays for verification.
483+
rootHash := fn.ToArray[[32]byte](supplyRootHash)
484+
leafKey := fn.ToArray[[32]byte](subtreeRoot.SupplyTreeLeafKey)
485+
486+
// Create the leaf node for the subtree.
487+
leafNode := mssmt.NewLeafNode(
488+
subtreeRoot.RootNode.RootHash,
489+
uint64(subtreeRoot.RootNode.RootSum),
490+
)
491+
492+
// Verify the inclusion proof.
493+
AssertInclusionProof(
494+
t, rootHash,
495+
subtreeRoot.SupplyTreeInclusionProof,
496+
leafKey, leafNode,
497+
)
498+
}
499+
500+
// MintAssetWithSupplyCommit mints an asset with supply commitments enabled
501+
// and verifies the pre-commitment output.
502+
func MintAssetWithSupplyCommit(t *harnessTest,
503+
mintReq *mintrpc.MintAssetRequest,
504+
expectedDelegationKey fn.Option[btcec.PublicKey]) (*taprpc.Asset, btcec.PublicKey) {
505+
506+
// Ensure supply commitments are enabled.
507+
mintReq.Asset.EnableSupplyCommitments = true
508+
509+
// Mint the asset.
510+
rpcAssets := MintAssetsConfirmBatch(
511+
t.t, t.lndHarness.Miner().Client, t.tapd,
512+
[]*mintrpc.MintAssetRequest{mintReq},
513+
)
514+
require.Len(t.t, rpcAssets, 1, "expected one minted asset")
515+
rpcAsset := rpcAssets[0]
516+
517+
// Verify the pre-commitment output.
518+
delegationKey := assertAnchorTxPreCommitOut(
519+
t, t.tapd, rpcAsset, expectedDelegationKey,
520+
)
521+
522+
return rpcAsset, delegationKey
523+
}

0 commit comments

Comments
 (0)