Skip to content

Commit 6d104fd

Browse files
authored
Merge pull request #1791 from lightninglabs/feat/burn-full-utxo
Full asset burn
2 parents 4c460da + 4a75419 commit 6d104fd

File tree

4 files changed

+130
-87
lines changed

4 files changed

+130
-87
lines changed

docs/release-notes/release-notes-0.7.0.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@
220220
`wallet.psbt-max-fee-ratio` and is introduced by
221221
[PR #1545](https://github.com/lightninglabs/taproot-assets/pull/1545).
222222

223+
- Enable [burning the full amount of an asset](https://github.com/lightninglabs/taproot-assets/pull/1791)
224+
when it is the sole one anchored to a Bitcoin UTXO.
225+
223226
## RPC Updates
224227

225228
## tapcli Updates

itest/burn_test.go

Lines changed: 123 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import (
55
"context"
66
"encoding/hex"
77

8+
"github.com/btcsuite/btcd/wire"
89
taprootassets "github.com/lightninglabs/taproot-assets"
910
"github.com/lightninglabs/taproot-assets/address"
1011
"github.com/lightninglabs/taproot-assets/asset"
11-
"github.com/lightninglabs/taproot-assets/tapfreighter"
1212
"github.com/lightninglabs/taproot-assets/tappsbt"
1313
"github.com/lightninglabs/taproot-assets/taprpc"
1414
wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
@@ -111,23 +111,7 @@ func testBurnAssets(t *harnessTest) {
111111
t.t, t.tapd, simpleAssetGen.AssetId, simpleAsset.Amount,
112112
)
113113

114-
// Test case 1: We'll now try to the exact amount of the largest output,
115-
// which should still select exactly that one largest output, which is
116-
// located alone in an anchor output. When attempting to burn this, we
117-
// should get an error saying that we cannot completely burn all assets
118-
// in an output.
119-
_, err = t.tapd.BurnAsset(ctxt, &taprpc.BurnAssetRequest{
120-
Asset: &taprpc.BurnAssetRequest_AssetId{
121-
AssetId: simpleAssetID[:],
122-
},
123-
AmountToBurn: outputAmounts[3],
124-
ConfirmationText: taprootassets.AssetBurnConfirmationText,
125-
})
126-
require.ErrorContains(
127-
t.t, err, tapfreighter.ErrFullBurnNotSupported.Error(),
128-
)
129-
130-
// Test case 2: We'll now try to burn a small amount of assets, which
114+
// Test case 1: We'll now try to burn a small amount of assets, which
131115
// should select the largest output, which is located alone in an anchor
132116
// output.
133117
const (
@@ -208,7 +192,7 @@ func testBurnAssets(t *harnessTest) {
208192
AssertNonInteractiveRecvComplete(t.t, t.tapd, 1)
209193
AssertReceiveEvents(t.t, fullSendAddr, stream)
210194

211-
// Test case 3: Burn all assets of one asset ID (in this case a single
195+
// Test case 2: Burn all assets of one asset ID (in this case a single
212196
// collectible from the original mint TX), while there are other,
213197
// passive assets in the anchor output.
214198
burnResp, err = t.tapd.BurnAsset(ctxt, &taprpc.BurnAssetRequest{
@@ -237,7 +221,7 @@ func testBurnAssets(t *harnessTest) {
237221
WithScriptKeyType(asset.ScriptKeyBurn),
238222
)
239223

240-
// Test case 4: Burn assets from multiple inputs. This will select the
224+
// Test case 3: Burn assets from multiple inputs. This will select the
241225
// two largest inputs we have, the one over 1500 we sent above and the
242226
// 1200 from the initial fan out transfer.
243227
const changeAmt = 300
@@ -283,7 +267,7 @@ func testBurnAssets(t *harnessTest) {
283267
require.NoError(t.t, err)
284268
t.Logf("All assets before last burn: %v", assets)
285269

286-
// Test case 5: Burn some units of a grouped asset. We start by making
270+
// Test case 4: Burn some units of a grouped asset. We start by making
287271
// sure we still have the full balance before burning.
288272
AssertBalanceByID(
289273
t.t, t.tapd, simpleGroupGen.AssetId, simpleGroup.Amount,
@@ -344,7 +328,7 @@ func testBurnAssets(t *harnessTest) {
344328

345329
require.Equal(t.t, groupBurn.Note, "")
346330

347-
// Test case 6: Burn the single unit of a grouped collectible. We start
331+
// Test case 5: Burn the single unit of a grouped collectible. We start
348332
// by making sure we still have the full balance before burning.
349333
AssertBalanceByID(
350334
t.t, t.tapd, simpleGroupCollectGen.AssetId,
@@ -524,3 +508,120 @@ func testBurnGroupedAssets(t *harnessTest) {
524508
require.Equal(t.t, burnNote, burn.Note)
525509
require.Equal(t.t, assetGroupKey, burn.TweakedGroupKey)
526510
}
511+
512+
// testFullBurnUTXO tests that we can burn the full amount of an asset UTXO.
513+
func testFullBurnUTXO(t *harnessTest) {
514+
minerClient := t.lndHarness.Miner().Client
515+
ctxb := context.Background()
516+
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
517+
defer cancel()
518+
519+
// Test 1: Burn the full amount of a simple asset.
520+
rpcAssets := MintAssetsConfirmBatch(
521+
t.t, minerClient, t.tapd, []*mintrpc.MintAssetRequest{
522+
simpleAssets[0],
523+
},
524+
)
525+
simpleAsset := rpcAssets[0]
526+
simpleAssetGen := simpleAsset.AssetGenesis
527+
var simpleAssetID [32]byte
528+
copy(simpleAssetID[:], simpleAssetGen.AssetId)
529+
530+
AssertBalanceByID(
531+
t.t, t.tapd, simpleAssetGen.AssetId, simpleAsset.Amount,
532+
)
533+
534+
// Perform a full burn of the asset.
535+
fullBurnAmt := simpleAsset.Amount
536+
burnResp, err := t.tapd.BurnAsset(ctxt, &taprpc.BurnAssetRequest{
537+
Asset: &taprpc.BurnAssetRequest_AssetId{
538+
AssetId: simpleAssetID[:],
539+
},
540+
AmountToBurn: fullBurnAmt,
541+
ConfirmationText: taprootassets.AssetBurnConfirmationText,
542+
})
543+
require.NoError(t.t, err)
544+
545+
AssertAssetOutboundTransferWithOutputs(
546+
t.t, minerClient, t.tapd, burnResp.BurnTransfer,
547+
[][]byte{simpleAssetGen.AssetId},
548+
[]uint64{fullBurnAmt}, 0, 1, 1, true,
549+
)
550+
AssertBalanceByID(t.t, t.tapd, simpleAssetGen.AssetId, 0)
551+
552+
// Export and verify the burn proof for the simple asset.
553+
wop, err := wire.NewOutPointFromString(
554+
burnResp.BurnTransfer.Outputs[0].Anchor.Outpoint,
555+
)
556+
require.NoError(t.t, err)
557+
outpoint := &taprpc.OutPoint{Txid: wop.Hash[:], OutputIndex: wop.Index}
558+
559+
proofResp := ExportProofFile(
560+
t.t, t.tapd,
561+
burnResp.BurnProof.Asset.AssetGenesis.AssetId,
562+
burnResp.BurnProof.Asset.ScriptKey,
563+
outpoint,
564+
)
565+
verifyResp, err := t.tapd.VerifyProof(ctxt, &taprpc.ProofFile{
566+
RawProofFile: proofResp.RawProofFile,
567+
})
568+
require.NoError(t.t, err)
569+
require.True(t.t, verifyResp.Valid)
570+
571+
// Test 2: Burn the full amount of a collectible asset.
572+
rpcAssets = MintAssetsConfirmBatch(
573+
t.t, minerClient, t.tapd, []*mintrpc.MintAssetRequest{
574+
simpleAssets[1],
575+
},
576+
)
577+
collectibleAsset := rpcAssets[0]
578+
collectibleAssetGen := collectibleAsset.AssetGenesis
579+
var collectibleAssetID [32]byte
580+
copy(collectibleAssetID[:], collectibleAssetGen.AssetId)
581+
582+
AssertBalanceByID(
583+
t.t, t.tapd, collectibleAssetGen.AssetId,
584+
collectibleAsset.Amount,
585+
)
586+
587+
fullBurnAmt = collectibleAsset.Amount
588+
burnResp, err = t.tapd.BurnAsset(ctxt, &taprpc.BurnAssetRequest{
589+
Asset: &taprpc.BurnAssetRequest_AssetId{
590+
AssetId: collectibleAssetID[:],
591+
},
592+
AmountToBurn: fullBurnAmt,
593+
ConfirmationText: taprootassets.AssetBurnConfirmationText,
594+
})
595+
require.NoError(t.t, err)
596+
597+
AssertAssetOutboundTransferWithOutputs(
598+
t.t, minerClient, t.tapd, burnResp.BurnTransfer,
599+
[][]byte{collectibleAssetID[:]},
600+
[]uint64{fullBurnAmt}, 1, 2, 1, true,
601+
)
602+
AssertBalanceByID(t.t, t.tapd, collectibleAssetID[:], 0)
603+
604+
// Export and verify the burn proof for the collectible.
605+
wop, err = wire.NewOutPointFromString(
606+
burnResp.BurnTransfer.Outputs[0].Anchor.Outpoint,
607+
)
608+
require.NoError(t.t, err)
609+
outpoint = &taprpc.OutPoint{Txid: wop.Hash[:], OutputIndex: wop.Index}
610+
611+
proofResp = ExportProofFile(
612+
t.t, t.tapd,
613+
burnResp.BurnProof.Asset.AssetGenesis.AssetId,
614+
burnResp.BurnProof.Asset.ScriptKey,
615+
outpoint,
616+
)
617+
verifyResp, err = t.tapd.VerifyProof(ctxt, &taprpc.ProofFile{
618+
RawProofFile: proofResp.RawProofFile,
619+
})
620+
require.NoError(t.t, err)
621+
require.True(t.t, verifyResp.Valid)
622+
623+
// Verify that we have 2 burns.
624+
burns := AssertNumBurns(t.t, t.tapd, 2, nil)
625+
require.Equal(t.t, simpleAssetGen.AssetId, burns[0].AssetId)
626+
require.Equal(t.t, collectibleAssetGen.AssetId, burns[1].AssetId)
627+
}

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,10 @@ var allTestCases = []*testCase{
313313
name: "burn grouped assets",
314314
test: testBurnGroupedAssets,
315315
},
316+
{
317+
name: "full burn assets",
318+
test: testFullBurnUTXO,
319+
},
316320
{
317321
name: "federation sync config",
318322
test: testFederationSyncConfig,

tapfreighter/wallet.go

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,6 @@ var (
5757
0x19, 0xde, 0xeb, 0xc0, 0x34, 0xad, 0x80, 0x66,
5858
0x4f, 0xb7, 0x4e, 0xc2, 0xad, 0x6e, 0x11, 0xd7,
5959
}
60-
61-
// ErrFullBurnNotSupported is returned when we attempt to burn all
62-
// assets of an anchor output, which is not supported.
63-
ErrFullBurnNotSupported = errors.New("burning all assets of an " +
64-
"anchor output is not supported")
6560
)
6661

6762
// Wallet is an interface for funding and signing asset transfers.
@@ -815,72 +810,12 @@ func (f *AssetWallet) FundBurn(ctx context.Context,
815810
len(fundedPkt.VPackets))
816811
}
817812

818-
// We want to avoid a BTC output being created that just sits there
819-
// without an actual commitment in it. So if we are not getting any
820-
// change or passive assets in this output, we'll not want to go through
821-
// with it.
822-
firstOut := fundedPkt.VPackets[0].Outputs[0]
823-
if len(fundedPkt.VPackets[0].Outputs) == 1 &&
824-
firstOut.Amount == fundDesc.Amount {
825-
826-
// A burn is an interactive transfer. So we don't expect there
827-
// to be a tombstone unless there are passive assets in the same
828-
// commitment, in which case the wallet has marked the change
829-
// output as tappsbt.TypePassiveSplitRoot. If that's not the
830-
// case, we'll return as burning all assets in an anchor output
831-
// is not supported.
832-
otherAssets, err := hasOtherAssets(
833-
fundedPkt.InputCommitments, fundedPkt.VPackets,
834-
)
835-
if err != nil {
836-
return nil, err
837-
}
838-
839-
if !otherAssets {
840-
return nil, ErrFullBurnNotSupported
841-
}
842-
}
843-
844813
// Don't release the coins we've selected, as so far we've been
845814
// successful.
846815
success = true
847816
return fundedPkt, nil
848817
}
849818

850-
// hasOtherAssets returns true if the given input commitments contain any other
851-
// assets than the ones given in the virtual packets.
852-
func hasOtherAssets(inputCommitments tappsbt.InputCommitments,
853-
vPackets []*tappsbt.VPacket) (bool, error) {
854-
855-
for idx := range inputCommitments {
856-
tapCommitment := inputCommitments[idx]
857-
858-
passiveCommitments, err := tapsend.RemovePacketsFromCommitment(
859-
tapCommitment, vPackets,
860-
)
861-
if err != nil {
862-
return false, err
863-
}
864-
865-
// We're trying to find out if there are any other assets in the
866-
// commitment. We don't want to count alt leaves as "assets" per
867-
// se in this context, so we trim them out, just for the next
868-
// check.
869-
trimmedPassiveCommitments, _, err := commitment.TrimAltLeaves(
870-
passiveCommitments,
871-
)
872-
if err != nil {
873-
return false, err
874-
}
875-
876-
if len(trimmedPassiveCommitments.CommittedAssets()) > 0 {
877-
return true, nil
878-
}
879-
}
880-
881-
return false, nil
882-
}
883-
884819
// SignVirtualPacketOptions is a set of functional options that allow callers to
885820
// further modify the virtual packet signing process.
886821
type SignVirtualPacketOptions struct {

0 commit comments

Comments
 (0)