Skip to content

Commit 34290c8

Browse files
committed
itest: add supply commit mint/burn itests
1 parent 8841fec commit 34290c8

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: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
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 {
180+
return resp.BurnSubtreeRoot != nil &&
181+
resp.BurnSubtreeRoot.RootNode.RootSum == int64(burnAmt) //nolint:lll
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{ //nolint:lll
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+
foundBurn = true
221+
222+
require.True(t.t, bytes.Equal(
223+
rpcFirstAsset.AssetGenesis.AssetId,
224+
burnLeaf.LeafKey.AssetId,
225+
), "burn leaf asset ID mismatch")
226+
break
227+
}
228+
}
229+
require.True(t.t, foundBurn, "expected burn leaf not found")
230+
231+
// Finally, we'll verify that the final supply commitment has the
232+
// pkScript that we expect.
233+
require.Len(t.t, finalMinedBlocks, 1, "expected one mined block")
234+
block := finalMinedBlocks[0]
235+
blockHash, _ := t.lndHarness.Miner().GetBestBlock()
236+
237+
fetchBlockHash, err := chainhash.NewHash(fetchResp.BlockHash)
238+
require.NoError(t.t, err)
239+
require.True(t.t, fetchBlockHash.IsEqual(blockHash))
240+
241+
// Re-compute the supply commitment root hash from the latest fetch,
242+
// then use that to derive the expected commitment output.
243+
supplyCommitRootHash := fn.ToArray[[32]byte](
244+
fetchResp.SupplyCommitmentRoot.RootHash,
245+
)
246+
internalKey, err := btcec.ParsePubKey(fetchResp.AnchorTxOutInternalKey)
247+
require.NoError(t.t, err)
248+
expectedTxOut, _, err := supplycommit.RootCommitTxOut(
249+
internalKey, nil, supplyCommitRootHash,
250+
)
251+
require.NoError(t.t, err)
252+
253+
foundCommitTxOut := false
254+
for _, tx := range block.Transactions {
255+
for _, txOut := range tx.TxOut {
256+
pkScriptMatch := bytes.Equal(
257+
txOut.PkScript, expectedTxOut.PkScript,
258+
)
259+
if txOut.Value == expectedTxOut.Value && pkScriptMatch {
260+
foundCommitTxOut = true
261+
break
262+
}
263+
}
264+
if foundCommitTxOut {
265+
break
266+
}
267+
}
268+
require.True(
269+
t.t, foundCommitTxOut,
270+
"supply commitment tx output not found in block",
271+
)
272+
273+
t.Log("Supply commit mint and burn test completed successfully")
274+
}

itest/supply_commit_test.go

Lines changed: 53 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,54 @@ 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],
505+
) (*taprpc.Asset, btcec.PublicKey) {
506+
507+
// Ensure supply commitments are enabled.
508+
mintReq.Asset.EnableSupplyCommitments = true
509+
510+
// Mint the asset.
511+
rpcAssets := MintAssetsConfirmBatch(
512+
t.t, t.lndHarness.Miner().Client, t.tapd,
513+
[]*mintrpc.MintAssetRequest{mintReq},
514+
)
515+
require.Len(t.t, rpcAssets, 1, "expected one minted asset")
516+
rpcAsset := rpcAssets[0]
517+
518+
// Verify the pre-commitment output.
519+
delegationKey := assertAnchorTxPreCommitOut(
520+
t, t.tapd, rpcAsset, expectedDelegationKey,
521+
)
522+
523+
return rpcAsset, delegationKey
524+
}

0 commit comments

Comments
 (0)