@@ -17,6 +17,8 @@ import (
1717 "github.com/btcsuite/btcd/chaincfg/chainhash"
1818 "github.com/btcsuite/btcd/wire"
1919 "github.com/davecgh/go-spew/spew"
20+ taprootassets "github.com/lightninglabs/taproot-assets"
21+ "github.com/lightninglabs/taproot-assets/asset"
2022 tapfn "github.com/lightninglabs/taproot-assets/fn"
2123 "github.com/lightninglabs/taproot-assets/itest"
2224 "github.com/lightninglabs/taproot-assets/proof"
@@ -291,6 +293,277 @@ func createTestAssetNetwork(t *harnessTest, net *NetworkHarness, charlieTap,
291293 return chanPointCD , chanPointDY , chanPointEF
292294}
293295
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+
294567func assertNumAssetUTXOs (t * testing.T , tapdClient * tapClient ,
295568 numUTXOs int ) * taprpc.ListUtxosResponse {
296569
@@ -1955,6 +2228,69 @@ func assertSpendableBalance(t *testing.T, client *tapClient, assetID []byte,
19552228 }
19562229}
19572230
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+
19582294func assertNumAssetOutputs (t * testing.T , client * tapClient , assetID []byte ,
19592295 numPieces int ) {
19602296
@@ -2071,6 +2407,24 @@ func logBalance(t *testing.T, nodes []*HarnessNode, assetID []byte,
20712407 }
20722408}
20732409
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+
20742428// readMacaroon tries to read the macaroon file at the specified path and create
20752429// gRPC dial options from it.
20762430func readMacaroon (macPath string ) (grpc.DialOption , error ) {
0 commit comments