Skip to content

Commit

Permalink
itest: add burn tests
Browse files Browse the repository at this point in the history
  • Loading branch information
guggero committed Sep 4, 2023
1 parent 54222e8 commit 8424ea5
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 5 deletions.
48 changes: 44 additions & 4 deletions itest/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ func assetScriptKeyIsLocalCheck(isLocal bool) assetCheck {
}
}

// assetScriptKeyIsBurnCheck returns a check function that tests an asset's
// script key for being a burn key.
func assetScriptKeyIsBurnCheck(isBurn bool) assetCheck {
return func(a *taprpc.Asset) error {
if a.IsBurn != isBurn {
return fmt.Errorf("unexpected script key, wanted "+
"is_burn=%v but is is_burn=%v", isBurn,
a.IsBurn)
}

return nil
}
}

// groupAssetsByName converts an unordered list of assets to a map of lists of
// assets, where all assets in a list have the same name.
func groupAssetsByName(assets []*taprpc.Asset) map[string][]*taprpc.Asset {
Expand Down Expand Up @@ -140,6 +154,32 @@ func AssertAssetState(t *testing.T, assets map[string][]*taprpc.Asset,
return a
}

// AssertAssetStateByScriptKey makes sure that an asset with the given (possibly
// non-unique!) name exists in the list of assets and then performs the given
// additional checks on that asset.
func AssertAssetStateByScriptKey(t *testing.T, assets []*taprpc.Asset,
scriptKey []byte, assetChecks ...assetCheck) *taprpc.Asset {

var a *taprpc.Asset
for _, rpcAsset := range assets {
if bytes.Equal(rpcAsset.ScriptKey, scriptKey) {
a = rpcAsset

for _, check := range assetChecks {
err := check(rpcAsset)
require.NoError(t, err)
}

break
}
}

require.NotNil(t, a, fmt.Errorf("asset with matching metadata not"+
"found in asset list"))

return a
}

// WaitForBatchState polls until the planter has reached the desired state with
// the current batch.
func WaitForBatchState(t *testing.T, ctx context.Context,
Expand Down Expand Up @@ -509,23 +549,23 @@ func confirmAndAssetOutboundTransferWithOutputs(t *harnessTest,
numTransfers, numOutputs int) *wire.MsgBlock {

return assertAssetOutboundTransferWithOutputs(
t, sender, sendResp, assetID, expectedAmounts,
t, sender, sendResp.Transfer, assetID, expectedAmounts,
currentTransferIdx, numTransfers, numOutputs, true,
)
}

// assertAssetOutboundTransferWithOutputs makes sure the given outbound transfer
// has the correct state and number of outputs.
func assertAssetOutboundTransferWithOutputs(t *harnessTest,
sender *tapdHarness, sendResp *taprpc.SendAssetResponse,
sender *tapdHarness, transfer *taprpc.AssetTransfer,
assetID []byte, expectedAmounts []uint64, currentTransferIdx,
numTransfers, numOutputs int, confirm bool) *wire.MsgBlock {

ctxb := context.Background()

// Check that we now have two new outputs, and that they differ
// in outpoints and scripts.
outputs := sendResp.Transfer.Outputs
outputs := transfer.Outputs
require.Len(t.t, outputs, numOutputs)

outpoints := make(map[string]struct{})
Expand All @@ -538,7 +578,7 @@ func assertAssetOutboundTransferWithOutputs(t *harnessTest,
scripts[string(o.ScriptKey)] = struct{}{}
}

sendRespJSON, err := formatProtoJSON(sendResp)
sendRespJSON, err := formatProtoJSON(transfer)
require.NoError(t.t, err)
t.Logf("Got response from sending assets: %v", sendRespJSON)

Expand Down
227 changes: 227 additions & 0 deletions itest/burn_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package itest

import (
"context"

taprootassets "github.com/lightninglabs/taproot-assets"
"github.com/lightninglabs/taproot-assets/address"
"github.com/lightninglabs/taproot-assets/tapfreighter"
"github.com/lightninglabs/taproot-assets/tappsbt"
"github.com/lightninglabs/taproot-assets/taprpc"
wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
"github.com/stretchr/testify/require"
)

// testBurnAssets tests that we're able to mint assets and then burn assets
// again.
func testBurnAssets(t *harnessTest) {
rpcAssets := mintAssetsConfirmBatch(
t, t.tapd, []*mintrpc.MintAssetRequest{
simpleAssets[0], simpleAssets[1], issuableAssets[1],
},
)

// We first fan out the assets we have to different outputs.
var (
chainParams = &address.RegressionNetTap
simpleAsset = rpcAssets[0]
simpleCollectible = rpcAssets[1]
simpleAssetGen = simpleAsset.AssetGenesis
simpleCollectibleGen = simpleCollectible.AssetGenesis
simpleAssetID [32]byte
)
copy(simpleAssetID[:], simpleAssetGen.AssetId)

ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
defer cancel()

// Preparation: We derive a couple of keys, so we can spread out our
// assets over several outputs, which we are going to use for the first
// couple of test cases.
scriptKey1, anchorInternalKeyDesc1 := deriveKeys(t.t, t.tapd)
scriptKey2, anchorInternalKeyDesc2 := deriveKeys(t.t, t.tapd)
scriptKey3, anchorInternalKeyDesc3 := deriveKeys(t.t, t.tapd)
scriptKey4, _ := deriveKeys(t.t, t.tapd)

// We create the following outputs:
// anchor index 0 (anchor internal key 1):
// - 1100 units to scriptKey1
// - 1200 units to scriptKey2
// anchor index 1 (anchor internal key 2):
// - 1600 units to scriptKey3
// anchor index 2 (anchor internal key 3):
// - 800 units to scriptKey4
// anchor index 3 (automatic change output):
// - 300 units to new script key
outputAmounts := []uint64{1100, 1200, 1600, 800, 300}
vPkt := tappsbt.ForInteractiveSend(
simpleAssetID, outputAmounts[0], scriptKey1, 0, anchorInternalKeyDesc1,
chainParams,
)
tappsbt.AddOutput(
vPkt, outputAmounts[1], scriptKey2, 0, anchorInternalKeyDesc1,
)
tappsbt.AddOutput(
vPkt, outputAmounts[2], scriptKey3, 1, anchorInternalKeyDesc2,
)
tappsbt.AddOutput(
vPkt, outputAmounts[3], scriptKey4, 2, anchorInternalKeyDesc3,
)

// We end up with a transfer with 5 outputs: 2 grouped into the first
// anchor output and then 3 each in their own output. So there are 4 BTC
// anchor outputs but 5 asset transfer outputs which we are now going to
// sign for and then finalize the transfer.
numOutputs := 5
fundResp := fundPacket(t, t.tapd, vPkt)
signResp, err := t.tapd.SignVirtualPsbt(
ctxt, &wrpc.SignVirtualPsbtRequest{
FundedPsbt: fundResp.FundedPsbt,
},
)
require.NoError(t.t, err)
sendResp, err := t.tapd.AnchorVirtualPsbts(
ctxt, &wrpc.AnchorVirtualPsbtsRequest{
VirtualPsbts: [][]byte{signResp.SignedPsbt},
},
)
require.NoError(t.t, err)
confirmAndAssetOutboundTransferWithOutputs(
t, t.tapd, sendResp, simpleAssetGen.AssetId, outputAmounts, 0, 1,
numOutputs,
)

// Let's make sure that we still have the original number of assets as
// seen by our wallet balance.
AssertBalanceByID(t.t, t.tapd, simpleAssetGen.AssetId, simpleAsset.Amount)

// Test case 1: We'll now try to the exact amount of the largest output,
// which should still select exactly that one largest output, which is
// located alone in an anchor output. When attempting to burn this, we
// should get an error saying that we cannot completely burn all assets
// in an output.
_, err = t.tapd.BurnAsset(ctxt, &taprpc.BurnAssetRequest{
Asset: &taprpc.BurnAssetRequest_AssetId{
AssetId: simpleAssetID[:],
},
AmountToBurn: outputAmounts[2],
ConfirmationText: taprootassets.AssetBurnConfirmationText,
})
require.ErrorContains(
t.t, err, tapfreighter.ErrFullBurnNotSupported.Error(),
)

// Test case 2: We'll now try to burn a small amount of assets, which
// should select the largest output, which is located alone in an anchor
// output.
const burnAmt = 100
burnResp, err := t.tapd.BurnAsset(ctxt, &taprpc.BurnAssetRequest{
Asset: &taprpc.BurnAssetRequest_AssetId{
AssetId: simpleAssetID[:],
},
AmountToBurn: burnAmt,
ConfirmationText: taprootassets.AssetBurnConfirmationText,
})
require.NoError(t.t, err)

burnRespJSON, err := formatProtoJSON(burnResp)
require.NoError(t.t, err)
t.Logf("Got response from burning %d units: %v", burnAmt, burnRespJSON)

assertAssetOutboundTransferWithOutputs(
t, t.tapd, burnResp.BurnTransfer, simpleAssetGen.AssetId,
[]uint64{outputAmounts[2] - burnAmt, burnAmt}, 1, 2, 2, true,
)

// We'll now assert that the burned asset has the correct state.
burnedAsset := burnResp.BurnProof.Asset
allAssets, err := t.tapd.ListAssets(
ctxt, &taprpc.ListAssetRequest{IncludeSpent: true},
)
require.NoError(t.t, err)
AssertAssetStateByScriptKey(
t.t, allAssets.Assets, burnedAsset.ScriptKey,
assetAmountCheck(burnedAsset.Amount),
assetTypeCheck(burnedAsset.AssetType),
assetScriptKeyIsLocalCheck(false),
assetScriptKeyIsBurnCheck(true),
)

// And now our asset balance should have been decreased by the burned
// amount.
AssertBalanceByID(
t.t, t.tapd, simpleAssetGen.AssetId, simpleAsset.Amount-burnAmt,
)

// The burned asset should be pruned from the tree when we next spend
// the anchor output it was in (together with the change). So let's test
// that we can successfully spend the change output.
secondSendAmt := outputAmounts[2] - burnAmt
fullSendAddr, err := t.tapd.NewAddr(ctxt, &taprpc.NewAddrRequest{
AssetId: simpleAssetGen.AssetId,
Amt: secondSendAmt,
})
require.NoError(t.t, err)

assertAddrCreated(t.t, t.tapd, simpleAsset, fullSendAddr)
sendResp = sendAssetsToAddr(t, t.tapd, fullSendAddr)
confirmAndAssertOutboundTransfer(
t, t.tapd, sendResp, simpleAssetGen.AssetId,
[]uint64{0, secondSendAmt}, 2, 3,
)
AssertNonInteractiveRecvComplete(t.t, t.tapd, 1)

// Test case 3: Burn all assets of one asset ID (in this case a single
// collectible from the original mint TX), while there are other,
// passive assets in the anchor output.
burnResp, err = t.tapd.BurnAsset(ctxt, &taprpc.BurnAssetRequest{
Asset: &taprpc.BurnAssetRequest_AssetId{
AssetId: simpleCollectibleGen.AssetId,
},
AmountToBurn: simpleCollectible.Amount,
ConfirmationText: taprootassets.AssetBurnConfirmationText,
})
require.NoError(t.t, err)

burnRespJSON, err = formatProtoJSON(burnResp)
require.NoError(t.t, err)
t.Logf("Got response from burning all units: %v", burnRespJSON)

assertAssetOutboundTransferWithOutputs(
t, t.tapd, burnResp.BurnTransfer, simpleCollectibleGen.AssetId,
[]uint64{1}, 3, 4, 1, true,
)

// Test case 4: Burn assets from multiple inputs. This will select the
// two largest inputs we have, the one over 1500 we sent above and the
// 1200 from the initial fan out transfer.
const changeAmt = 300
multiBurnAmt := outputAmounts[1] + secondSendAmt - changeAmt
burnResp, err = t.tapd.BurnAsset(ctxt, &taprpc.BurnAssetRequest{
Asset: &taprpc.BurnAssetRequest_AssetId{
AssetId: simpleAssetGen.AssetId,
},
AmountToBurn: multiBurnAmt,
ConfirmationText: taprootassets.AssetBurnConfirmationText,
})
require.NoError(t.t, err)

burnRespJSON, err = formatProtoJSON(burnResp)
require.NoError(t.t, err)
t.Logf("Got response from burning units from multiple inputs: %v",
burnRespJSON)

assertAssetOutboundTransferWithOutputs(
t, t.tapd, burnResp.BurnTransfer, simpleAssetGen.AssetId,
[]uint64{changeAmt, multiBurnAmt}, 4, 5, 2, true,
)

// Our final asset balance should be reduced by both successful burn
// amounts of the simple asset.
AssertBalanceByID(
t.t, t.tapd, simpleAssetGen.AssetId,
simpleAsset.Amount-burnAmt-multiBurnAmt,
)
}
2 changes: 1 addition & 1 deletion itest/send_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ func testSendMultipleCoins(t *harnessTest) {

sendResp := sendAssetsToAddr(t, t.tapd, bobAddrs[i])
assertAssetOutboundTransferWithOutputs(
t, t.tapd, sendResp, genInfo.AssetId,
t, t.tapd, sendResp.Transfer, genInfo.AssetId,
[]uint64{0, unitsPerPart}, i+1, i+2, 2, false,
)
}
Expand Down
4 changes: 4 additions & 0 deletions itest/test_list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ var testCases = []*testCase{
name: "re-org mint and send",
test: testReOrgMintAndSend,
},
{
name: "burn test",
test: testBurnAssets,
},
}

var optionalTestCases = []*testCase{
Expand Down

0 comments on commit 8424ea5

Please sign in to comment.