Skip to content

Commit 8cbcb0c

Browse files
Roasbeefguggero
authored andcommitted
itest: add supply commit mint/burn itests
1 parent aab8cc6 commit 8cbcb0c

File tree

4 files changed

+391
-0
lines changed

4 files changed

+391
-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: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
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+
//nolint:lll
85+
Asset: &mintrpc.MintAsset{
86+
AssetType: taprpc.AssetType_NORMAL,
87+
Name: "itestbuxx-supply-commit-tranche-2",
88+
AssetMeta: &taprpc.AssetMeta{
89+
Data: []byte("second tranche metadata"),
90+
},
91+
Amount: 3000,
92+
AssetVersion: taprpc.AssetVersion_ASSET_VERSION_V1,
93+
NewGroupedAsset: false,
94+
GroupedAsset: true,
95+
GroupKey: groupKeyBytes,
96+
EnableSupplyCommitments: true,
97+
},
98+
}
99+
rpcSecondAsset, _ := MintAssetWithSupplyCommit(
100+
t, secondMintReq, fn.Some(delegationKey),
101+
)
102+
103+
// Ensure both assets are in the same group.
104+
require.EqualValues(
105+
t.t, groupKeyBytes,
106+
rpcSecondAsset.AssetGroup.TweakedGroupKey,
107+
)
108+
109+
t.Log("Updating supply commitment after second mint")
110+
111+
// Update and mine the supply commitment after second mint.
112+
UpdateAndMineSupplyCommit(
113+
t.t, ctxb, t.tapd, t.lndHarness.Miner().Client,
114+
groupKeyBytes, 1,
115+
)
116+
117+
t.Log("Verifying supply tree includes both mint operations")
118+
119+
// Fetch and verify the updated supply includes both mints.
120+
expectedTotal := int64(
121+
mintReq.Asset.Amount + secondMintReq.Asset.Amount,
122+
)
123+
fetchResp = WaitForSupplyCommit(
124+
t.t, ctxb, t.tapd, groupKeyBytes,
125+
func(resp *unirpc.FetchSupplyCommitResponse) bool {
126+
root := resp.IssuanceSubtreeRoot
127+
return root != nil &&
128+
root.RootNode.RootSum == expectedTotal
129+
},
130+
)
131+
132+
// Finally, we'll test burning assets from the group, and ensure that
133+
// the supply tree is updated with this information.
134+
t.Log("Burning assets from the group")
135+
136+
const (
137+
burnAmt = 1000
138+
burnNote = "supply commit burn test"
139+
)
140+
141+
burnResp, err := t.tapd.BurnAsset(ctxb, &taprpc.BurnAssetRequest{
142+
Asset: &taprpc.BurnAssetRequest_AssetId{
143+
AssetId: rpcFirstAsset.AssetGenesis.AssetId,
144+
},
145+
AmountToBurn: burnAmt,
146+
Note: burnNote,
147+
ConfirmationText: taprootassets.AssetBurnConfirmationText,
148+
})
149+
require.NoError(t.t, err)
150+
require.NotNil(t.t, burnResp)
151+
152+
t.Log("Confirming burn transaction")
153+
154+
// Confirm the burn transaction, asserting that all the expected records
155+
// on disk are in place.
156+
AssertAssetOutboundTransferWithOutputs(
157+
t.t, t.lndHarness.Miner().Client, t.tapd, burnResp.BurnTransfer,
158+
[][]byte{rpcFirstAsset.AssetGenesis.AssetId},
159+
[]uint64{mintReq.Asset.Amount - burnAmt, burnAmt},
160+
0, 1, 2, true,
161+
)
162+
163+
// Make sure that the burn is recognized in the burn records.
164+
burns := AssertNumBurns(t.t, t.tapd, 1, nil)
165+
burn := burns[0]
166+
require.Equal(t.t, uint64(burnAmt), burn.Amount)
167+
require.Equal(t.t, burnNote, burn.Note)
168+
169+
t.Log("Updating supply commitment after burn")
170+
171+
// Update and mine the supply commitment after burn.
172+
finalMinedBlocks := UpdateAndMineSupplyCommit(
173+
t.t, ctxb, t.tapd, t.lndHarness.Miner().Client,
174+
groupKeyBytes, 1,
175+
)
176+
177+
t.Log("Verifying supply tree includes burn leaves")
178+
179+
// Fetch and verify the supply tree now includes burn leaves.
180+
fetchResp = WaitForSupplyCommit(
181+
t.t, ctxb, t.tapd, groupKeyBytes,
182+
func(resp *unirpc.FetchSupplyCommitResponse) bool {
183+
root := resp.BurnSubtreeRoot
184+
return root != nil &&
185+
root.RootNode.RootSum == int64(burnAmt)
186+
},
187+
)
188+
189+
// Verify the burn subtree inclusion in the supply tree.
190+
AssertSubtreeInclusionProof(
191+
t, fetchResp.SupplyCommitmentRoot.RootHash,
192+
fetchResp.BurnSubtreeRoot,
193+
)
194+
195+
t.Log("Fetching supply leaves for detailed verification")
196+
197+
// Fetch supply leaves to verify individual entries have all been
198+
// properly committed.
199+
respLeaves, err := t.tapd.FetchSupplyLeaves(
200+
ctxb, &unirpc.FetchSupplyLeavesRequest{
201+
//nolint:lll
202+
GroupKey: &unirpc.FetchSupplyLeavesRequest_GroupKeyBytes{
203+
GroupKeyBytes: groupKeyBytes,
204+
},
205+
},
206+
)
207+
require.NoError(t.t, err)
208+
require.NotNil(t.t, respLeaves)
209+
210+
// Verify we have the expected issuance leaves (2 mints), and a single
211+
// burn leaf.
212+
require.Equal(
213+
t.t, len(respLeaves.IssuanceLeaves), 2,
214+
"expected at least 2 issuance leaves",
215+
)
216+
require.Equal(
217+
t.t, len(respLeaves.BurnLeaves), 1,
218+
"expected at least 1 burn leaf",
219+
)
220+
221+
// Make sure that the burn leaf has the proper amount.
222+
foundBurn := false
223+
for _, burnLeaf := range respLeaves.BurnLeaves {
224+
if burnLeaf.LeafNode.RootSum == int64(burnAmt) {
225+
foundBurn = true
226+
227+
require.True(t.t, bytes.Equal(
228+
rpcFirstAsset.AssetGenesis.AssetId,
229+
burnLeaf.LeafKey.AssetId,
230+
), "burn leaf asset ID mismatch")
231+
break
232+
}
233+
}
234+
require.True(t.t, foundBurn, "expected burn leaf not found")
235+
236+
// Finally, we'll verify that the final supply commitment has the
237+
// pkScript that we expect.
238+
require.Len(t.t, finalMinedBlocks, 1, "expected one mined block")
239+
block := finalMinedBlocks[0]
240+
blockHash, _ := t.lndHarness.Miner().GetBestBlock()
241+
242+
fetchBlockHash, err := chainhash.NewHash(fetchResp.BlockHash)
243+
require.NoError(t.t, err)
244+
require.True(t.t, fetchBlockHash.IsEqual(blockHash))
245+
246+
// Re-compute the supply commitment root hash from the latest fetch,
247+
// then use that to derive the expected commitment output.
248+
supplyCommitRootHash := fn.ToArray[[32]byte](
249+
fetchResp.SupplyCommitmentRoot.RootHash,
250+
)
251+
internalKey, err := btcec.ParsePubKey(fetchResp.AnchorTxOutInternalKey)
252+
require.NoError(t.t, err)
253+
expectedTxOut, _, err := supplycommit.RootCommitTxOut(
254+
internalKey, nil, supplyCommitRootHash,
255+
)
256+
require.NoError(t.t, err)
257+
258+
foundCommitTxOut := false
259+
for _, tx := range block.Transactions {
260+
for _, txOut := range tx.TxOut {
261+
pkScriptMatch := bytes.Equal(
262+
txOut.PkScript, expectedTxOut.PkScript,
263+
)
264+
if txOut.Value == expectedTxOut.Value && pkScriptMatch {
265+
foundCommitTxOut = true
266+
break
267+
}
268+
}
269+
if foundCommitTxOut {
270+
break
271+
}
272+
}
273+
require.True(
274+
t.t, foundCommitTxOut,
275+
"supply commitment tx output not found in block",
276+
)
277+
278+
t.Log("Supply commit mint and burn test completed successfully")
279+
}

0 commit comments

Comments
 (0)