@@ -17,6 +17,8 @@ import (
17
17
"github.com/btcsuite/btcd/chaincfg/chainhash"
18
18
"github.com/btcsuite/btcd/wire"
19
19
"github.com/davecgh/go-spew/spew"
20
+ taprootassets "github.com/lightninglabs/taproot-assets"
21
+ "github.com/lightninglabs/taproot-assets/asset"
20
22
tapfn "github.com/lightninglabs/taproot-assets/fn"
21
23
"github.com/lightninglabs/taproot-assets/itest"
22
24
"github.com/lightninglabs/taproot-assets/proof"
@@ -291,6 +293,277 @@ func createTestAssetNetwork(t *harnessTest, net *NetworkHarness, charlieTap,
291
293
return chanPointCD , chanPointDY , chanPointEF
292
294
}
293
295
296
+ // createTestAssetNetworkGroupKey sets up a test network with Charlie, Dave,
297
+ // Erin and Fabia and creates asset channels between Charlie->Dave and
298
+ // Erin-Fabia in a way that there are two equally sized asset pieces for each
299
+ // minted asset (currently limited to exactly two assets). The channels are then
300
+ // confirmed and balances asserted.
301
+ func createTestAssetNetworkGroupKey (ctx context.Context , t * harnessTest ,
302
+ net * NetworkHarness , charlieTap , daveTap , erinTap , fabiaTap ,
303
+ universeTap * tapClient , mintedAssets []* taprpc.Asset ,
304
+ charlieFundingAmount , erinFundingAmount uint64 ,
305
+ pushSat int64 ) (* lnrpc.ChannelPoint , * lnrpc.ChannelPoint ) {
306
+
307
+ var groupKey []byte
308
+ for _ , mintedAsset := range mintedAssets {
309
+ require .NotNil (t .t , mintedAsset .AssetGroup )
310
+
311
+ if groupKey == nil {
312
+ groupKey = mintedAsset .AssetGroup .TweakedGroupKey
313
+
314
+ continue
315
+ }
316
+
317
+ require .Equal (
318
+ t .t , groupKey , mintedAsset .AssetGroup .TweakedGroupKey ,
319
+ )
320
+ }
321
+
322
+ // We first do a transfer to Charlie by itself, so we get the correct
323
+ // asset pieces that we want for the channel funding.
324
+ sendAssetsAndAssert (
325
+ ctx , t , charlieTap , charlieTap , universeTap , mintedAssets [0 ],
326
+ charlieFundingAmount / 2 , 0 , 1 , 0 ,
327
+ )
328
+ sendAssetsAndAssert (
329
+ ctx , t , charlieTap , charlieTap , universeTap , mintedAssets [1 ],
330
+ charlieFundingAmount / 2 , 1 , 2 , 0 ,
331
+ )
332
+
333
+ // We need to send some assets to Erin, so he can fund an asset channel
334
+ // with Fabia.
335
+ sendAssetsAndAssert (
336
+ ctx , t , erinTap , charlieTap , universeTap , mintedAssets [0 ],
337
+ erinFundingAmount / 2 , 2 , 1 , charlieFundingAmount / 2 ,
338
+ )
339
+ sendAssetsAndAssert (
340
+ ctx , t , erinTap , charlieTap , universeTap , mintedAssets [1 ],
341
+ erinFundingAmount / 2 , 3 , 2 , charlieFundingAmount / 2 ,
342
+ )
343
+
344
+ // Then we burn everything but a single asset piece.
345
+ assetID1 := mintedAssets [0 ].AssetGenesis .AssetId
346
+ assetID2 := mintedAssets [1 ].AssetGenesis .AssetId
347
+ burnAmount1 := mintedAssets [0 ].Amount - charlieFundingAmount / 2 -
348
+ erinFundingAmount / 2 - 1
349
+ _ , err := charlieTap .BurnAsset (ctx , & taprpc.BurnAssetRequest {
350
+ Asset : & taprpc.BurnAssetRequest_AssetId {
351
+ AssetId : assetID1 ,
352
+ },
353
+ AmountToBurn : burnAmount1 ,
354
+ ConfirmationText : taprootassets .AssetBurnConfirmationText ,
355
+ })
356
+ require .NoError (t .t , err )
357
+
358
+ mineBlocks (t , net , 1 , 1 )
359
+
360
+ burnAmount2 := mintedAssets [1 ].Amount - charlieFundingAmount / 2 -
361
+ erinFundingAmount / 2 - 1
362
+ _ , err = charlieTap .BurnAsset (ctx , & taprpc.BurnAssetRequest {
363
+ Asset : & taprpc.BurnAssetRequest_AssetId {
364
+ AssetId : assetID2 ,
365
+ },
366
+ AmountToBurn : burnAmount2 ,
367
+ ConfirmationText : taprootassets .AssetBurnConfirmationText ,
368
+ })
369
+ require .NoError (t .t , err )
370
+
371
+ mineBlocks (t , net , 1 , 1 )
372
+
373
+ t .Logf ("Opening asset channels..." )
374
+
375
+ // The first channel we create has a push amount, so Charlie can receive
376
+ // payments immediately and not run into the channel reserve issue.
377
+ fundRespCD , err := charlieTap .FundChannel (
378
+ ctx , & tchrpc.FundChannelRequest {
379
+ AssetAmount : charlieFundingAmount ,
380
+ GroupKey : groupKey ,
381
+ PeerPubkey : daveTap .node .PubKey [:],
382
+ FeeRateSatPerVbyte : 5 ,
383
+ PushSat : pushSat ,
384
+ },
385
+ )
386
+ require .NoError (t .t , err )
387
+ t .Logf ("Funded channel between Charlie and Dave: %v" , fundRespCD )
388
+
389
+ fundRespEF , err := erinTap .FundChannel (
390
+ ctx , & tchrpc.FundChannelRequest {
391
+ AssetAmount : erinFundingAmount ,
392
+ GroupKey : groupKey ,
393
+ PeerPubkey : fabiaTap .node .PubKey [:],
394
+ FeeRateSatPerVbyte : 5 ,
395
+ PushSat : pushSat ,
396
+ },
397
+ )
398
+ require .NoError (t .t , err )
399
+ t .Logf ("Funded channel between Erin and Fabia: %v" , fundRespEF )
400
+
401
+ // Make sure the pending channel shows up in the list and has the
402
+ // custom records set as JSON.
403
+ assertPendingChannels (
404
+ t .t , charlieTap .node , mintedAssets [0 ], 1 ,
405
+ charlieFundingAmount / 2 , 0 ,
406
+ )
407
+ assertPendingChannels (
408
+ t .t , charlieTap .node , mintedAssets [1 ], 1 ,
409
+ charlieFundingAmount / 2 , 0 ,
410
+ )
411
+ assertPendingChannels (
412
+ t .t , erinTap .node , mintedAssets [0 ], 1 , erinFundingAmount / 2 , 0 ,
413
+ )
414
+ assertPendingChannels (
415
+ t .t , erinTap .node , mintedAssets [1 ], 1 , erinFundingAmount / 2 , 0 ,
416
+ )
417
+
418
+ // Now that we've looked at the pending channels, let's actually confirm
419
+ // all three of them.
420
+ mineBlocks (t , net , 6 , 2 )
421
+
422
+ var id1 , id2 asset.ID
423
+ copy (id1 [:], assetID1 )
424
+ copy (id2 [:], assetID2 )
425
+
426
+ fundingTree1 , err := tapscript .NewChannelFundingScriptTreeUniqueID (
427
+ id1 ,
428
+ )
429
+ require .NoError (t .t , err )
430
+ fundingScriptKey1 := fundingTree1 .TaprootKey
431
+ fundingScriptTreeBytes1 := fundingScriptKey1 .SerializeCompressed ()
432
+
433
+ fundingTree2 , err := tapscript .NewChannelFundingScriptTreeUniqueID (
434
+ id2 ,
435
+ )
436
+ require .NoError (t .t , err )
437
+ fundingScriptKey2 := fundingTree2 .TaprootKey
438
+ fundingScriptTreeBytes2 := fundingScriptKey2 .SerializeCompressed ()
439
+
440
+ // TODO(guggero): Those asset balances should be 1, 1, 0, 0
441
+ // respectively, but because we now have unique script keys, we need
442
+ // https://github.com/lightninglabs/taproot-assets/pull/1198 first.
443
+ assertAssetBalance (t .t , charlieTap , assetID1 , 25001 )
444
+ assertAssetBalance (t .t , charlieTap , assetID2 , 25001 )
445
+ assertAssetBalance (t .t , erinTap , assetID1 , 25000 )
446
+ assertAssetBalance (t .t , erinTap , assetID2 , 25000 )
447
+
448
+ // There should be two asset pieces for Charlie for both asset IDs, one
449
+ // in the channel and one with a single unit from the burn.
450
+ assertNumAssetOutputs (t .t , charlieTap , assetID1 , 2 )
451
+ assertNumAssetOutputs (t .t , charlieTap , assetID2 , 2 )
452
+ assertAssetExists (
453
+ t .t , charlieTap , assetID1 , charlieFundingAmount / 2 ,
454
+ fundingScriptKey1 , false , true , true ,
455
+ )
456
+ assertAssetExists (
457
+ t .t , charlieTap , assetID1 , 1 , nil , true , false , false ,
458
+ )
459
+ assertAssetExists (
460
+ t .t , charlieTap , assetID2 , charlieFundingAmount / 2 ,
461
+ fundingScriptKey2 , false , true , true ,
462
+ )
463
+ assertAssetExists (
464
+ t .t , charlieTap , assetID2 , 1 , nil , true , false , false ,
465
+ )
466
+
467
+ // Erin should just have one output for each asset ID, the one in the
468
+ // channel.
469
+ assertNumAssetOutputs (t .t , erinTap , assetID1 , 1 )
470
+ assertNumAssetOutputs (t .t , erinTap , assetID2 , 1 )
471
+ assertAssetExists (
472
+ t .t , erinTap , assetID1 , erinFundingAmount / 2 , fundingScriptKey1 ,
473
+ false , true , true ,
474
+ )
475
+ assertAssetExists (
476
+ t .t , erinTap , assetID2 , erinFundingAmount / 2 , fundingScriptKey2 ,
477
+ false , true , true ,
478
+ )
479
+
480
+ // Assert that the proofs for both channels has been uploaded to the
481
+ // designated Universe server.
482
+ assertUniverseProofExists (
483
+ t .t , universeTap , assetID1 , groupKey , fundingScriptTreeBytes1 ,
484
+ fmt .Sprintf ("%v:%v" , fundRespCD .Txid , fundRespCD .OutputIndex ),
485
+ )
486
+ assertUniverseProofExists (
487
+ t .t , universeTap , assetID2 , groupKey , fundingScriptTreeBytes2 ,
488
+ fmt .Sprintf ("%v:%v" , fundRespCD .Txid , fundRespCD .OutputIndex ),
489
+ )
490
+ assertUniverseProofExists (
491
+ t .t , universeTap , assetID1 , groupKey , fundingScriptTreeBytes1 ,
492
+ fmt .Sprintf ("%v:%v" , fundRespEF .Txid , fundRespEF .OutputIndex ),
493
+ )
494
+ assertUniverseProofExists (
495
+ t .t , universeTap , assetID2 , groupKey , fundingScriptTreeBytes2 ,
496
+ fmt .Sprintf ("%v:%v" , fundRespEF .Txid , fundRespEF .OutputIndex ),
497
+ )
498
+
499
+ // Make sure the channel shows the correct asset information.
500
+ assertAssetChan (
501
+ t .t , charlieTap .node , daveTap .node , charlieFundingAmount ,
502
+ mintedAssets ,
503
+ )
504
+ assertAssetChan (
505
+ t .t , erinTap .node , fabiaTap .node , erinFundingAmount ,
506
+ mintedAssets ,
507
+ )
508
+
509
+ chanPointCD := & lnrpc.ChannelPoint {
510
+ OutputIndex : uint32 (fundRespCD .OutputIndex ),
511
+ FundingTxid : & lnrpc.ChannelPoint_FundingTxidStr {
512
+ FundingTxidStr : fundRespCD .Txid ,
513
+ },
514
+ }
515
+ chanPointEF := & lnrpc.ChannelPoint {
516
+ OutputIndex : uint32 (fundRespEF .OutputIndex ),
517
+ FundingTxid : & lnrpc.ChannelPoint_FundingTxidStr {
518
+ FundingTxidStr : fundRespEF .Txid ,
519
+ },
520
+ }
521
+
522
+ return chanPointCD , chanPointEF
523
+ }
524
+
525
+ // sendAssetsAndAssert sends the given amount of assets to the recipient and
526
+ // asserts that the transfer was successful. It also checks that the asset
527
+ // balance of the sender and recipient is as expected.
528
+ func sendAssetsAndAssert (ctx context.Context , t * harnessTest ,
529
+ recipient , sender , universe * tapClient , mintedAsset * taprpc.Asset ,
530
+ assetSendAmount uint64 , idx , numTransfers int ,
531
+ previousSentAmount uint64 ) {
532
+
533
+ assetID := mintedAsset .AssetGenesis .AssetId
534
+ recipientAddr , err := recipient .NewAddr (ctx , & taprpc.NewAddrRequest {
535
+ Amt : assetSendAmount ,
536
+ AssetId : assetID ,
537
+ ProofCourierAddr : fmt .Sprintf (
538
+ "%s://%s" , proof .UniverseRpcCourierType ,
539
+ universe .node .Cfg .LitAddr (),
540
+ ),
541
+ })
542
+ require .NoError (t .t , err )
543
+
544
+ t .Logf ("Sending %v asset units to %s..." , assetSendAmount ,
545
+ recipient .node .Cfg .Name )
546
+
547
+ // We assume that we sent the same size in a previous send.
548
+ totalSent := assetSendAmount + previousSentAmount
549
+
550
+ // Send the assets to recipient.
551
+ itest .AssertAddrCreated (
552
+ t .t , recipient , mintedAsset , recipientAddr ,
553
+ )
554
+ sendResp , err := sender .SendAsset (ctx , & taprpc.SendAssetRequest {
555
+ TapAddrs : []string {recipientAddr .Encoded },
556
+ })
557
+ require .NoError (t .t , err )
558
+ itest .ConfirmAndAssertOutboundTransfer (
559
+ t .t , t .lndHarness .Miner .Client , sender , sendResp ,
560
+ assetID ,
561
+ []uint64 {mintedAsset .Amount - totalSent , assetSendAmount },
562
+ idx , idx + 1 ,
563
+ )
564
+ itest .AssertNonInteractiveRecvComplete (t .t , recipient , numTransfers )
565
+ }
566
+
294
567
func assertNumAssetUTXOs (t * testing.T , tapdClient * tapClient ,
295
568
numUTXOs int ) * taprpc.ListUtxosResponse {
296
569
@@ -1955,6 +2228,69 @@ func assertSpendableBalance(t *testing.T, client *tapClient, assetID []byte,
1955
2228
}
1956
2229
}
1957
2230
2231
+ // assertSpendableBalanceGroup differs from assertAssetBalance in that it
2232
+ // asserts that the entire balance is spendable. We consider something spendable
2233
+ // if we have a local script key for it.
2234
+ func assertSpendableBalanceGroup (t * testing.T , client * tapClient ,
2235
+ gropupKey []byte , expectedBalance uint64 ) {
2236
+
2237
+ t .Helper ()
2238
+
2239
+ ctxb := context .Background ()
2240
+ ctxt , cancel := context .WithTimeout (ctxb , shortTimeout )
2241
+ defer cancel ()
2242
+
2243
+ err := wait .NoError (func () error {
2244
+ utxos , err := client .ListUtxos (ctxt , & taprpc.ListUtxosRequest {})
2245
+ if err != nil {
2246
+ return err
2247
+ }
2248
+
2249
+ assets := tapfn .FlatMap (
2250
+ maps .Values (utxos .ManagedUtxos ),
2251
+ func (utxo * taprpc.ManagedUtxo ) []* taprpc.Asset {
2252
+ return utxo .Assets
2253
+ },
2254
+ )
2255
+
2256
+ relevantAssets := fn .Filter (func (utxo * taprpc.Asset ) bool {
2257
+ return bytes .Equal (
2258
+ utxo .AssetGroup .TweakedGroupKey , gropupKey ,
2259
+ )
2260
+ }, assets )
2261
+
2262
+ var assetSum uint64
2263
+ for _ , asset := range relevantAssets {
2264
+ if asset .ScriptKeyIsLocal {
2265
+ assetSum += asset .Amount
2266
+ }
2267
+ }
2268
+
2269
+ if assetSum != expectedBalance {
2270
+ return fmt .Errorf ("expected balance %d, got %d" ,
2271
+ expectedBalance , assetSum )
2272
+ }
2273
+
2274
+ return nil
2275
+ }, shortTimeout )
2276
+ if err != nil {
2277
+ r , err2 := client .ListAssets (ctxb , & taprpc.ListAssetRequest {})
2278
+ require .NoError (t , err2 )
2279
+
2280
+ t .Logf ("Failed to assert expected balance of %d, current " +
2281
+ "assets: %v" , expectedBalance , toProtoJSON (t , r ))
2282
+
2283
+ utxos , err3 := client .ListUtxos (
2284
+ ctxb , & taprpc.ListUtxosRequest {},
2285
+ )
2286
+ require .NoError (t , err3 )
2287
+
2288
+ t .Logf ("Current UTXOs: %v" , toProtoJSON (t , utxos ))
2289
+
2290
+ t .Fatalf ("Failed to assert balance: %v" , err )
2291
+ }
2292
+ }
2293
+
1958
2294
func assertNumAssetOutputs (t * testing.T , client * tapClient , assetID []byte ,
1959
2295
numPieces int ) {
1960
2296
@@ -2071,6 +2407,24 @@ func logBalance(t *testing.T, nodes []*HarnessNode, assetID []byte,
2071
2407
}
2072
2408
}
2073
2409
2410
+ func logBalanceGroup (t * testing.T , nodes []* HarnessNode , assetIDs [][]byte ,
2411
+ occasion string ) {
2412
+
2413
+ t .Helper ()
2414
+
2415
+ time .Sleep (time .Millisecond * 250 )
2416
+
2417
+ for _ , node := range nodes {
2418
+ local , remote , localSat , remoteSat := getAssetChannelBalance (
2419
+ t , node , assetIDs , false ,
2420
+ )
2421
+
2422
+ t .Logf ("%-7s balance: local=%-9d remote=%-9d, localSat=%-9d, " +
2423
+ "remoteSat=%-9d (%v)" , node .Cfg .Name , local , remote ,
2424
+ localSat , remoteSat , occasion )
2425
+ }
2426
+ }
2427
+
2074
2428
// readMacaroon tries to read the macaroon file at the specified path and create
2075
2429
// gRPC dial options from it.
2076
2430
func readMacaroon (macPath string ) (grpc.DialOption , error ) {
0 commit comments