Skip to content

Commit 4dc9e06

Browse files
committed
tapfreighter: add call to FetchZeroValueAnchorUTXOs to retrieve zero-value inputs
1 parent 3639f16 commit 4dc9e06

File tree

6 files changed

+167
-16
lines changed

6 files changed

+167
-16
lines changed

tapfreighter/chain_porter.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,7 @@ func (p *ChainPorter) storePackageAnchorTxConf(pkg *sendPackage) error {
888888
TxIndex: int32(pkg.TransferTxConfEvent.TxIndex),
889889
FinalProofs: pkg.FinalProofs,
890890
PassiveAssetProofFiles: passiveAssetProofFiles,
891+
ZeroValueInputs: pkg.ZeroValueInputs,
891892
}, burns)
892893
if err != nil {
893894
return fmt.Errorf("unable to log parcel delivery "+
@@ -1248,9 +1249,20 @@ func (p *ChainPorter) importLocalAddresses(ctx context.Context,
12481249
for idx := range parcel.Outputs {
12491250
out := &parcel.Outputs[idx]
12501251

1251-
// Skip non-local outputs, those are going to a receiver outside
1252-
// of this daemon.
1253-
if !out.ScriptKeyLocal {
1252+
// Determine if the output should be imported into the wallet.
1253+
// We import outputs that:
1254+
// 1. Have local script keys (normal change outputs)
1255+
// 2. Tombstones (zero-value with NUMS key)
1256+
// 3. Burns (zero-value with burn key)
1257+
isTombstone := out.Amount == 0 &&
1258+
out.ScriptKey.PubKey.IsEqual(asset.NUMSPubKey)
1259+
isBurn := out.Amount == 0 && len(out.WitnessData) > 0 &&
1260+
asset.IsBurnKey(
1261+
out.ScriptKey.PubKey, out.WitnessData[0],
1262+
)
1263+
shouldImport := out.ScriptKeyLocal || isTombstone || isBurn
1264+
1265+
if !shouldImport {
12541266
continue
12551267
}
12561268

@@ -1266,6 +1278,10 @@ func (p *ChainPorter) importLocalAddresses(ctx context.Context,
12661278
if err != nil {
12671279
return err
12681280
}
1281+
log.Infof("Importing anchor output key for output %d "+
1282+
"(isTombstone=%v, isBurn=%v): outpoint=%v, key=%x",
1283+
idx, isTombstone, isBurn, out.Anchor.OutPoint,
1284+
anchorOutputKey.SerializeCompressed())
12691285

12701286
// Before we broadcast the transaction to the network, we'll
12711287
// import the new anchor output into the wallet so it watches
@@ -1274,13 +1290,11 @@ func (p *ChainPorter) importLocalAddresses(ctx context.Context,
12741290
_, err = p.cfg.Wallet.ImportTaprootOutput(ctx, anchorOutputKey)
12751291
switch {
12761292
case err == nil:
1277-
break
12781293

12791294
// On restart, we'll get an error that the output has already
12801295
// been added to the wallet, so we'll catch this now and move
12811296
// along if so.
12821297
case strings.Contains(err.Error(), "already exists"):
1283-
break
12841298

12851299
default:
12861300
return err
@@ -1446,6 +1460,7 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
14461460

14471461
currentPkg.VirtualPackets = fundSendRes.VPackets
14481462
currentPkg.InputCommitments = fundSendRes.InputCommitments
1463+
currentPkg.ZeroValueInputs = fundSendRes.ZeroValueInputs
14491464

14501465
currentPkg.SendState = SendStateVirtualSign
14511466

@@ -1591,9 +1606,10 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
15911606

15921607
anchorTx, err := wallet.AnchorVirtualTransactions(
15931608
ctx, &AnchorVTxnsParams{
1594-
FeeRate: feeRate,
1595-
ActivePackets: currentPkg.VirtualPackets,
1596-
PassivePackets: currentPkg.PassiveAssets,
1609+
FeeRate: feeRate,
1610+
ActivePackets: currentPkg.VirtualPackets,
1611+
PassivePackets: currentPkg.PassiveAssets,
1612+
ZeroValueInputs: currentPkg.ZeroValueInputs,
15971613
},
15981614
)
15991615
if err != nil {

tapfreighter/coin_select.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,15 @@ func (s *CoinSelect) selectForAmount(minTotalAmount uint64,
199199
return selectedCommitments, nil
200200
}
201201

202+
// FetchZeroValueAnchorUTXOs fetches all managed UTXOs that contain only
203+
// zero-value assets (tombstones and burns).
204+
func (s *CoinSelect) FetchZeroValueAnchorUTXOs(ctx context.Context) (
205+
[]ZeroValueInput, error) {
206+
207+
s.coinLock.Lock()
208+
defer s.coinLock.Unlock()
209+
210+
return s.coinLister.FetchZeroValueAnchorUTXOs(ctx)
211+
}
212+
202213
var _ CoinSelector = (*CoinSelect)(nil)

tapfreighter/coin_select_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ func (m *mockCoinLister) DeleteExpiredLeases(ctx context.Context) error {
6060
return nil
6161
}
6262

63+
func (m *mockCoinLister) FetchZeroValueAnchorUTXOs(
64+
context.Context) ([]ZeroValueInput, error) {
65+
66+
return nil, nil
67+
}
68+
6369
// TestCoinSelector tests that the coin selector behaves as expected.
6470
func TestCoinSelector(t *testing.T) {
6571
var (

tapfreighter/interface.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ type CoinLister interface {
168168

169169
// DeleteExpiredLeases deletes all expired leases from the database.
170170
DeleteExpiredLeases(ctx context.Context) error
171+
172+
// FetchZeroValueAnchorUTXOs fetches all managed UTXOs that contain only
173+
// zero-value assets (tombstones and burns).
174+
FetchZeroValueAnchorUTXOs(ctx context.Context) ([]ZeroValueInput, error)
171175
}
172176

173177
// MultiCommitmentSelectStrategy is an enum that describes the strategy that
@@ -195,6 +199,30 @@ type CoinSelector interface {
195199
// ReleaseCoins releases/unlocks coins that were previously leased and
196200
// makes them available for coin selection again.
197201
ReleaseCoins(ctx context.Context, utxoOutpoints ...wire.OutPoint) error
202+
203+
// FetchZeroValueAnchorUTXOs fetches all managed UTXOs that contain only
204+
// zero-value assets (tombstones and burns).
205+
FetchZeroValueAnchorUTXOs(ctx context.Context) ([]ZeroValueInput, error)
206+
}
207+
208+
// ZeroValueInput represents a zero-value UTXO that should be swept.
209+
type ZeroValueInput interface {
210+
// GetOutPoint returns the outpoint of the zero-value UTXO.
211+
GetOutPoint() wire.OutPoint
212+
213+
// GetOutputValue returns the satoshi value of the zero-value UTXO.
214+
GetOutputValue() btcutil.Amount
215+
216+
// GetInternalKey returns the internal key descriptor for the
217+
// zero-value UTXO.
218+
GetInternalKey() keychain.KeyDescriptor
219+
220+
// GetMerkleRoot returns the taproot merkle root for the zero-value
221+
// UTXO.
222+
GetMerkleRoot() []byte
223+
224+
// GetPkScript returns the pkScript of the anchor output.
225+
GetPkScript() []byte
198226
}
199227

200228
// TransferInput represents the database level input to an asset transfer.
@@ -574,6 +602,10 @@ type AssetConfirmEvent struct {
574602
// PassiveAssetProofFiles is the set of passive asset proof files that
575603
// are re-anchored during the parcel confirmation process.
576604
PassiveAssetProofFiles map[asset.ID][]*proof.AnnotatedProof
605+
606+
// ZeroValueInputs is the set of zero-value UTXOs that were swept as
607+
// additional inputs to the Bitcoin transaction.
608+
ZeroValueInputs []ZeroValueInput
577609
}
578610

579611
// ExportLog is used to track the state of outbound Taproot Asset parcels

tapfreighter/parcel.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,10 @@ type PreSignedParcel struct {
283283
// spent in the virtual transaction.
284284
inputCommitments tappsbt.InputCommitments
285285

286+
// zeroValueInputs is the list of zero-value UTXOs that should be swept
287+
// as additional inputs to the transaction.
288+
zeroValueInputs []ZeroValueInput
289+
286290
// note is a string that provides any user defined description for this
287291
// transfer.
288292
note string
@@ -295,7 +299,7 @@ var _ Parcel = (*PreSignedParcel)(nil)
295299
// NewPreSignedParcel creates a new PreSignedParcel.
296300
func NewPreSignedParcel(vPackets []*tappsbt.VPacket,
297301
inputCommitments tappsbt.InputCommitments,
298-
note string) *PreSignedParcel {
302+
zeroValueInputs []ZeroValueInput, note string) *PreSignedParcel {
299303

300304
return &PreSignedParcel{
301305
parcelKit: &parcelKit{
@@ -304,6 +308,7 @@ func NewPreSignedParcel(vPackets []*tappsbt.VPacket,
304308
},
305309
vPackets: vPackets,
306310
inputCommitments: inputCommitments,
311+
zeroValueInputs: zeroValueInputs,
307312
note: note,
308313
}
309314
}
@@ -320,6 +325,7 @@ func (p *PreSignedParcel) pkg() *sendPackage {
320325
SendState: SendStateAnchorSign,
321326
VirtualPackets: p.vPackets,
322327
InputCommitments: p.inputCommitments,
328+
ZeroValueInputs: p.zeroValueInputs,
323329
Note: p.note,
324330
}
325331
}
@@ -486,6 +492,10 @@ type sendPackage struct {
486492
// associated Taproot Asset commitment.
487493
InputCommitments tappsbt.InputCommitments
488494

495+
// ZeroValueInputs is a list of zero-value UTXOs that should be swept
496+
// as additional inputs to the transaction.
497+
ZeroValueInputs []ZeroValueInput
498+
489499
// SendManifests is a map of send manifests that need to be sent to the
490500
// auth mailbox server to complete an address V2 transfer. It is keyed
491501
// by the anchor output index.

tapfreighter/wallet.go

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ type AnchorVTxnsParams struct {
172172
// PassivePackets is a list of all the virtual transactions which
173173
// re-anchor passive assets.
174174
PassivePackets []*tappsbt.VPacket
175+
176+
// ZeroValueInputs is a list of zero-value UTXOs that should be swept
177+
// as additional inputs to the transaction.
178+
ZeroValueInputs []ZeroValueInput
175179
}
176180

177181
// WalletConfig holds the configuration for a new Wallet.
@@ -248,6 +252,10 @@ type FundedVPacket struct {
248252
// InputCommitments is a map from virtual package input index to its
249253
// associated Taproot Asset commitment.
250254
InputCommitments tappsbt.InputCommitments
255+
256+
// ZeroValueInputs is a list of zero-value UTXOs that should be swept
257+
// as additional inputs to the transaction.
258+
ZeroValueInputs []ZeroValueInput
251259
}
252260

253261
// FundAddressSend funds a virtual transaction, selecting assets to spend in
@@ -684,6 +692,17 @@ func (f *AssetWallet) FundPacket(ctx context.Context,
684692
return nil, err
685693
}
686694

695+
// Fetch zero-value UTXOs that should be swept as additional inputs.
696+
zeroValueInputs, err := f.cfg.CoinSelector.FetchZeroValueAnchorUTXOs(
697+
ctx,
698+
)
699+
if err != nil {
700+
return nil, fmt.Errorf("unable to fetch zero-value "+
701+
"UTXOs: %w", err)
702+
}
703+
704+
pkt.ZeroValueInputs = zeroValueInputs
705+
687706
success = true
688707
return pkt, nil
689708
}
@@ -810,6 +829,17 @@ func (f *AssetWallet) FundBurn(ctx context.Context,
810829
len(fundedPkt.VPackets))
811830
}
812831

832+
// Fetch zero-value UTXOs that should be swept as additional inputs.
833+
zeroValueInputs, err := f.cfg.CoinSelector.FetchZeroValueAnchorUTXOs(
834+
ctx,
835+
)
836+
if err != nil {
837+
return nil, fmt.Errorf("unable to fetch zero-value "+
838+
"UTXOs: %w", err)
839+
}
840+
841+
fundedPkt.ZeroValueInputs = zeroValueInputs
842+
813843
// Don't release the coins we've selected, as so far we've been
814844
// successful.
815845
success = true
@@ -1191,28 +1221,33 @@ func (f *AssetWallet) AnchorVirtualTransactions(ctx context.Context,
11911221
// it itself.
11921222
addAnchorPsbtInputs(sendPacket, params.ActivePackets)
11931223

1224+
// Add zero-value inputs that should be swept as additional inputs.
1225+
numZeroValueInputs := len(params.ZeroValueInputs)
1226+
if numZeroValueInputs > 0 {
1227+
log.Infof("Sweeping %d zero-value UTXOs", numZeroValueInputs)
1228+
addZeroValuePsbtInputs(
1229+
sendPacket, params.ZeroValueInputs,
1230+
f.cfg.ChainParams.HDCoinType,
1231+
)
1232+
}
1233+
11941234
// We now fund the packet, placing the change on the last output.
11951235
anchorPkt, err := f.cfg.Wallet.FundPsbt(
11961236
ctx, sendPacket, 1, params.FeeRate, -1,
11971237
)
11981238
if err != nil {
11991239
return nil, fmt.Errorf("unable to fund psbt: %w", err)
12001240
}
1201-
1202-
log.Infof("Received funded PSBT packet")
1203-
log.Tracef("Packet: %v", spew.Sdump(anchorPkt.Pkt))
1241+
log.Tracef("Got funded PSBT packet: %v", spew.Sdump(anchorPkt.Pkt))
12041242

12051243
// With all the input and output information in the packet, we
12061244
// can now ask lnd to sign it, and then extract the final
12071245
// version ourselves.
1208-
log.Debugf("Signing PSBT")
1209-
log.Tracef("PSBT: %s", spew.Sdump(anchorPkt))
12101246
signedPsbt, err := f.cfg.Wallet.SignPsbt(ctx, anchorPkt.Pkt)
12111247
if err != nil {
12121248
return nil, fmt.Errorf("unable to sign psbt: %w", err)
12131249
}
1214-
log.Debugf("Got signed PSBT")
1215-
log.Tracef("PSBT: %s", spew.Sdump(signedPsbt))
1250+
log.Tracef("Got signed PSBT: %s", spew.Sdump(signedPsbt))
12161251

12171252
// Before we finalize, we need to calculate the actual, final fees that
12181253
// we pay.
@@ -1380,3 +1415,44 @@ func addAnchorPsbtInputs(btcPkt *psbt.Packet, vPackets []*tappsbt.VPacket) {
13801415

13811416
}
13821417
}
1418+
1419+
// addZeroValuePsbtInputs adds zero-value UTXOs as inputs to the PSBT.
1420+
func addZeroValuePsbtInputs(btcPkt *psbt.Packet,
1421+
zeroValueInputs []ZeroValueInput, coinType uint32) {
1422+
1423+
for _, utxo := range zeroValueInputs {
1424+
// Check if this input is already added to avoid duplicates.
1425+
if tapsend.HasInput(btcPkt.UnsignedTx, utxo.GetOutPoint()) {
1426+
continue
1427+
}
1428+
1429+
// Create the BIP32 derivation info for signing.
1430+
bip32Derivation, trDerivation := tappsbt.
1431+
Bip32DerivationFromKeyDesc(
1432+
utxo.GetInternalKey(), coinType,
1433+
)
1434+
1435+
btcPkt.Inputs = append(btcPkt.Inputs, psbt.PInput{
1436+
WitnessUtxo: &wire.TxOut{
1437+
Value: int64(utxo.GetOutputValue()),
1438+
PkScript: utxo.GetPkScript(),
1439+
},
1440+
SighashType: txscript.SigHashDefault,
1441+
Bip32Derivation: []*psbt.Bip32Derivation{
1442+
bip32Derivation,
1443+
},
1444+
TaprootInternalKey: schnorr.SerializePubKey(
1445+
utxo.GetInternalKey().PubKey,
1446+
),
1447+
TaprootBip32Derivation: []*psbt.TaprootBip32Derivation{
1448+
trDerivation,
1449+
},
1450+
TaprootMerkleRoot: utxo.GetMerkleRoot(),
1451+
})
1452+
btcPkt.UnsignedTx.TxIn = append(
1453+
btcPkt.UnsignedTx.TxIn, &wire.TxIn{
1454+
PreviousOutPoint: utxo.GetOutPoint(),
1455+
},
1456+
)
1457+
}
1458+
}

0 commit comments

Comments
 (0)