Skip to content

Commit 3c5f6cb

Browse files
committed
itest: add grouped asset multi tranche test
1 parent 5b4d4e3 commit 3c5f6cb

File tree

3 files changed

+594
-0
lines changed

3 files changed

+594
-0
lines changed

Diff for: itest/assets_test.go

+354
Original file line numberDiff line numberDiff line change
@@ -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+
294567
func 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+
19582294
func 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.
20762430
func readMacaroon(macPath string) (grpc.DialOption, error) {

0 commit comments

Comments
 (0)