Skip to content

Commit 81c687c

Browse files
committed
itest: add supply commit mint/burn itests
1 parent 15f7297 commit 81c687c

File tree

4 files changed

+383
-0
lines changed

4 files changed

+383
-0
lines changed

itest/assertions.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2756,3 +2756,57 @@ 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) *unirpc.FetchSupplyCommitResponse {
2790+
2791+
groupKeyReq := &unirpc.FetchSupplyCommitRequest_GroupKeyBytes{
2792+
GroupKeyBytes: groupKeyBytes,
2793+
}
2794+
2795+
var fetchResp *unirpc.FetchSupplyCommitResponse
2796+
var err error
2797+
2798+
require.Eventually(t, func() bool {
2799+
fetchResp, err = tapd.FetchSupplyCommit(
2800+
ctx, &unirpc.FetchSupplyCommitRequest{
2801+
GroupKey: groupKeyReq,
2802+
},
2803+
)
2804+
if err != nil {
2805+
return false
2806+
}
2807+
2808+
return fetchResp != nil && condition(fetchResp)
2809+
}, defaultWaitTimeout, time.Second)
2810+
2811+
return fetchResp
2812+
}

itest/supply_commit_mint_burn_test.go

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

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)