Skip to content

Commit 1fdd9da

Browse files
committed
itest: verify ignored asset outpoints in supply commitments
Add integration test to ensure universe supply commitments properly handle ignored asset outpoints. The test confirms that once an outpoint is marked as ignored, it is correctly placed in the ignore subtree of the supply commitment, and that this state is reflected in the mined commitment transaction.
1 parent 1d0a85d commit 1fdd9da

File tree

2 files changed

+288
-0
lines changed

2 files changed

+288
-0
lines changed

itest/supply_commit_test.go

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@ package itest
33
import (
44
"bytes"
55
"context"
6+
"time"
67

78
"github.com/btcsuite/btcd/btcec/v2"
9+
"github.com/btcsuite/btcd/chaincfg/chainhash"
810
"github.com/btcsuite/btcd/wire"
911
"github.com/lightninglabs/taproot-assets/fn"
12+
"github.com/lightninglabs/taproot-assets/mssmt"
1013
"github.com/lightninglabs/taproot-assets/tapgarden"
1114
"github.com/lightninglabs/taproot-assets/taprpc"
1215
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
16+
unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc"
17+
"github.com/lightninglabs/taproot-assets/universe/supplycommit"
1318
"github.com/stretchr/testify/require"
1419
)
1520

@@ -132,3 +137,282 @@ func testPreCommitOutput(t *harnessTest) {
132137
// Ensure that the second tranche asset is part of the same group.
133138
require.EqualValues(t.t, tweakedGroupKey, secondAssetGroupKey)
134139
}
140+
141+
// testSupplyCommitIgnoreAsset verifies that universe supply commitments
142+
// correctly account for ignored asset outpoints. It:
143+
//
144+
// 1. Mints an asset group with universe supply commitments enabled.
145+
// 2. Transfers a portion of the asset to a secondary node.
146+
// 3. Instructs the primary node to ignore the outpoint now owned by the
147+
// secondary node.
148+
// 4. Updates the asset group’s supply commitment, which should now include
149+
// the ignored outpoint in the “ignore” subtree.
150+
// 5. Mines the commitment transaction.
151+
// 6. Retrieves the updated supply commitment transaction and asserts that the
152+
// ignored subtree contains the expected outpoint.
153+
func testSupplyCommitIgnoreAsset(t *harnessTest) {
154+
ctxb := context.Background()
155+
156+
t.Log("Minting asset group with a single normal asset and " +
157+
"universe/supply commitments enabled")
158+
mintReq := issuableAssets[0]
159+
mintReq.Asset.UniverseCommitments = true
160+
rpcAssets := MintAssetsConfirmBatch(
161+
t.t, t.lndHarness.Miner().Client, t.tapd,
162+
[]*mintrpc.MintAssetRequest{mintReq},
163+
)
164+
require.Len(t.t, rpcAssets, 1, "expected one minted asset")
165+
rpcAsset := rpcAssets[0]
166+
167+
// Send some of the asset to a secondary node. We will then use the
168+
// primary node to ignore the asset outpoint owned by the secondary
169+
// node.
170+
t.Log("Setting up secondary node as recipient of asset")
171+
secondLnd := t.lndHarness.NewNodeWithCoins("SecondLnd", nil)
172+
secondTapd := setupTapdHarness(t.t, t, secondLnd, t.universeServer)
173+
defer func() {
174+
require.NoError(t.t, secondTapd.stop(!*noDelete))
175+
}()
176+
177+
t.Log("Sending asset to secondary node")
178+
sendAssetAmount := uint64(10)
179+
sendChangeAmount := rpcAsset.Amount - sendAssetAmount
180+
181+
sendResp := sendAssetAndAssert(
182+
ctxb, t, t.tapd, secondTapd, sendAssetAmount, sendChangeAmount,
183+
rpcAsset.AssetGenesis, rpcAsset, 0, 1, 1,
184+
)
185+
require.Len(t.t, sendResp.RpcResp.Transfer.Outputs, 2)
186+
t.Log("Asset transfer completed successfully")
187+
188+
// Parse the group key from the minted asset.
189+
groupKeyBytes := rpcAsset.AssetGroup.TweakedGroupKey
190+
require.NotNil(t.t, groupKeyBytes)
191+
192+
// Ignore the asset outpoint owned by the secondary node.
193+
t.Log("Registering supply commitment asset ignore for asset outpoint " +
194+
"owned by secondary node")
195+
196+
// Determine the transfer output owned by the secondary node.
197+
// This is the output that we will ignore.
198+
transferOutput := sendResp.RpcResp.Transfer.Outputs[0]
199+
if sendResp.RpcResp.Transfer.Outputs[1].Amount == sendAssetAmount {
200+
transferOutput = sendResp.RpcResp.Transfer.Outputs[1]
201+
}
202+
203+
// Ignore the asset outpoint owned by the secondary node.
204+
ignoreReq := &unirpc.IgnoreAssetOutPointRequest{
205+
AssetOutPoint: &taprpc.AssetOutPoint{
206+
AnchorOutPoint: transferOutput.Anchor.Outpoint,
207+
AssetId: rpcAsset.AssetGenesis.AssetId,
208+
ScriptKey: transferOutput.ScriptKey,
209+
},
210+
Amount: sendAssetAmount,
211+
}
212+
respIgnore, err := t.tapd.IgnoreAssetOutPoint(ctxb, ignoreReq)
213+
require.NoError(t.t, err)
214+
require.NotNil(t.t, respIgnore)
215+
require.EqualValues(t.t, sendAssetAmount, respIgnore.Leaf.RootSum)
216+
217+
// Assert that the mempool is empty.
218+
mempool := t.lndHarness.Miner().GetRawMempool()
219+
require.Empty(t.t, mempool)
220+
221+
// At this point, the supply commitment should not yet exist, as we
222+
// haven't created it after ignoring the asset outpoint.
223+
//
224+
// nolint: lll
225+
fetchRespNil, err := t.tapd.FetchSupplyCommit(
226+
ctxb, &unirpc.FetchSupplyCommitRequest{
227+
GroupKey: &unirpc.FetchSupplyCommitRequest_GroupKeyBytes{
228+
GroupKeyBytes: groupKeyBytes,
229+
},
230+
IgnoreLeafKeys: [][]byte{
231+
respIgnore.LeafKey,
232+
},
233+
},
234+
)
235+
require.Nil(t.t, fetchRespNil)
236+
require.ErrorContains(t.t, err, "supply commitment not found for "+
237+
"asset group with key")
238+
239+
t.Log("Update on-chain supply commitment for asset group")
240+
241+
// nolint: lll
242+
respUpdate, err := t.tapd.UpdateSupplyCommit(
243+
ctxb, &unirpc.UpdateSupplyCommitRequest{
244+
GroupKey: &unirpc.UpdateSupplyCommitRequest_GroupKeyBytes{
245+
GroupKeyBytes: groupKeyBytes,
246+
},
247+
},
248+
)
249+
require.NoError(t.t, err)
250+
require.NotNil(t.t, respUpdate)
251+
252+
t.Log("Mining supply commitment tx")
253+
minedBlocks := MineBlocks(t.t, t.lndHarness.Miner().Client, 1, 1)
254+
255+
t.Log("Fetch updated supply commitment")
256+
// Ensure that the supply commitment reflects the ignored asset
257+
// outpoint owned by the secondary node.
258+
var fetchResp *unirpc.FetchSupplyCommitResponse
259+
require.Eventually(t.t, func() bool {
260+
// nolint: lll
261+
fetchResp, err = t.tapd.FetchSupplyCommit(
262+
ctxb, &unirpc.FetchSupplyCommitRequest{
263+
GroupKey: &unirpc.FetchSupplyCommitRequest_GroupKeyBytes{
264+
GroupKeyBytes: groupKeyBytes,
265+
},
266+
IgnoreLeafKeys: [][]byte{
267+
respIgnore.LeafKey,
268+
},
269+
},
270+
)
271+
require.NoError(t.t, err)
272+
273+
// If the fetch response has no block height or hash,
274+
// it means that the supply commitment transaction has not
275+
// been mined yet, so we should retry.
276+
if fetchResp.BlockHeight == 0 || len(fetchResp.BlockHash) == 0 {
277+
return false
278+
}
279+
280+
// Once the ignore tree includes the ignored asset outpoint, we
281+
// know that the supply commitment has been updated.
282+
return fetchResp.IgnoreSubtreeRoot.RootNode.RootSum ==
283+
int64(sendAssetAmount)
284+
}, defaultWaitTimeout, time.Second)
285+
286+
// Verify that the supply commitment tree commits to the ignore subtree.
287+
supplyCommitRootHash := fn.ToArray[[32]byte](
288+
fetchResp.SupplyCommitmentRoot.RootHash,
289+
)
290+
291+
// Formulate the ignore leaf node as it should appear in the supply
292+
// tree.
293+
supplyTreeIgnoreLeafNode := mssmt.NewLeafNode(
294+
fetchResp.IgnoreSubtreeRoot.RootNode.RootHash,
295+
uint64(fetchResp.IgnoreSubtreeRoot.RootNode.RootSum),
296+
)
297+
298+
ignoreRootLeafKey := fn.ToArray[[32]byte](
299+
fetchResp.IgnoreSubtreeRoot.SupplyTreeLeafKey,
300+
)
301+
302+
AssertInclusionProof(
303+
t, supplyCommitRootHash,
304+
fetchResp.IgnoreSubtreeRoot.SupplyTreeInclusionProof,
305+
ignoreRootLeafKey, supplyTreeIgnoreLeafNode,
306+
)
307+
308+
// Unmarshal ignore tree leaf inclusion proof to verify that the
309+
// ignored asset outpoint is included in the ignore tree.
310+
require.Len(t.t, fetchResp.IgnoreLeafInclusionProofs, 1)
311+
inclusionProofBytes := fetchResp.IgnoreLeafInclusionProofs[0]
312+
313+
// Verify that the ignore tree root can be computed from the ignore leaf
314+
// inclusion proof.
315+
expectedIgnoreSubtreeRootHash := fn.ToArray[[32]byte](
316+
fetchResp.IgnoreSubtreeRoot.RootNode.RootHash,
317+
)
318+
319+
ignoreLeafKey := fn.ToArray[[32]byte](respIgnore.LeafKey)
320+
ignoreLeaf := unmarshalMerkleSumNode(respIgnore.Leaf)
321+
322+
AssertInclusionProof(
323+
t, expectedIgnoreSubtreeRootHash, inclusionProofBytes,
324+
ignoreLeafKey, ignoreLeaf,
325+
)
326+
327+
// Verify that the mined supply commitment transaction commits to the
328+
// supply commitment tree.
329+
require.Len(t.t, minedBlocks, 1)
330+
331+
block := minedBlocks[0]
332+
expectedBlockHash := block.BlockHash()
333+
334+
// Get block height for block.
335+
blockHash, blockHeight := t.lndHarness.Miner().GetBestBlock()
336+
require.True(t.t, blockHash.IsEqual(&expectedBlockHash))
337+
338+
// Ensure that the block hash and height matches the values in the fetch
339+
// response.
340+
fetchBlockHash, err := chainhash.NewHash(fetchResp.BlockHash)
341+
require.NoError(t.t, err)
342+
require.True(t.t, fetchBlockHash.IsEqual(blockHash))
343+
344+
require.EqualValues(t.t, blockHeight, fetchResp.BlockHeight)
345+
346+
// We expect two transactions in the block:
347+
// 1. The supply commitment transaction.
348+
// 2. The coinbase transaction.
349+
require.Len(t.t, block.Transactions, 2)
350+
351+
internalKey, err := btcec.ParsePubKey(fetchResp.AnchorTxOutInternalKey)
352+
require.NoError(t.t, err)
353+
354+
expectedTxOut, _, err := supplycommit.RootCommitTxOut(
355+
internalKey, nil, supplyCommitRootHash,
356+
)
357+
require.NoError(t.t, err)
358+
359+
foundCommitTxOut := false
360+
actualBlockTxIndex := 0
361+
for idx := range block.Transactions {
362+
tx := block.Transactions[idx]
363+
364+
for idxOut := range tx.TxOut {
365+
txOut := tx.TxOut[idxOut]
366+
367+
pkScriptMatch := bytes.Equal(
368+
txOut.PkScript, expectedTxOut.PkScript,
369+
)
370+
if txOut.Value == expectedTxOut.Value && pkScriptMatch {
371+
// Ensure that the target tx out is only present
372+
// once.
373+
if foundCommitTxOut {
374+
t.Fatalf("found multiple supply " +
375+
"commitment tx outputs in " +
376+
"block")
377+
}
378+
379+
foundCommitTxOut = true
380+
actualBlockTxIndex = idx
381+
}
382+
}
383+
}
384+
385+
require.True(t.t, foundCommitTxOut)
386+
require.EqualValues(t.t, actualBlockTxIndex, fetchResp.BlockTxIndex)
387+
388+
// If we try to ignore the same asset outpoint using the secondary
389+
// node, it should fail because the secondary node does not have access
390+
// to the supply commitment delegation key for signing.
391+
_, err = secondTapd.IgnoreAssetOutPoint(ctxb, ignoreReq)
392+
require.ErrorContains(t.t, err, "delegation key locator not found")
393+
}
394+
395+
// AssertInclusionProof checks that the inclusion proof for a given leaf key
396+
// and leaf node matches the expected root hash.
397+
func AssertInclusionProof(t *harnessTest, expectedRootHash [32]byte,
398+
inclusionProofBytes []byte, leafKey [32]byte, leafNode mssmt.Node) {
399+
400+
// Decode the inclusion proof bytes into a compressed proof.
401+
var compressedProof mssmt.CompressedProof
402+
err := compressedProof.Decode(bytes.NewReader(inclusionProofBytes))
403+
require.NoError(t.t, err)
404+
405+
// Decompress the inclusion proof to get the full proof structure.
406+
inclusionProof, err := compressedProof.Decompress()
407+
require.NoError(t.t, err)
408+
409+
// Derive the root from the inclusion proof and the leaf node.
410+
derivedRoot := inclusionProof.Root(leafKey, leafNode)
411+
derivedRootHash := fn.ByteSlice(derivedRoot.NodeHash())
412+
413+
// Verify that the derived root hash matches the expected root hash.
414+
if !bytes.Equal(expectedRootHash[:], derivedRootHash) {
415+
t.t.Fatalf("expected root hash %x, got %x",
416+
expectedRootHash[:], derivedRootHash)
417+
}
418+
}

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,10 @@ var allTestCases = []*testCase{
347347
name: "pre commit output",
348348
test: testPreCommitOutput,
349349
},
350+
{
351+
name: "supply commit ignore asset",
352+
test: testSupplyCommitIgnoreAsset,
353+
},
350354
{
351355
name: "auth mailbox message store and fetch",
352356
test: testAuthMailboxStoreAndFetchMessage,

0 commit comments

Comments
 (0)