@@ -3,13 +3,18 @@ package itest
3
3
import (
4
4
"bytes"
5
5
"context"
6
+ "time"
6
7
7
8
"github.com/btcsuite/btcd/btcec/v2"
9
+ "github.com/btcsuite/btcd/chaincfg/chainhash"
8
10
"github.com/btcsuite/btcd/wire"
9
11
"github.com/lightninglabs/taproot-assets/fn"
12
+ "github.com/lightninglabs/taproot-assets/mssmt"
10
13
"github.com/lightninglabs/taproot-assets/tapgarden"
11
14
"github.com/lightninglabs/taproot-assets/taprpc"
12
15
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
16
+ unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc"
17
+ "github.com/lightninglabs/taproot-assets/universe/supplycommit"
13
18
"github.com/stretchr/testify/require"
14
19
)
15
20
@@ -132,3 +137,282 @@ func testPreCommitOutput(t *harnessTest) {
132
137
// Ensure that the second tranche asset is part of the same group.
133
138
require .EqualValues (t .t , tweakedGroupKey , secondAssetGroupKey )
134
139
}
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
+ }
0 commit comments