Skip to content

Commit aeeaf25

Browse files
committed
tapfreighter: add call to FetchZeroValueAnchorUTXOs to retrieve zero-value inputs
1 parent 223d830 commit aeeaf25

File tree

8 files changed

+302
-51
lines changed

8 files changed

+302
-51
lines changed

tapfreighter/chain_porter.go

Lines changed: 50 additions & 24 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,8 @@ 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+
if !out.IsSpendable() {
12541254
continue
12551255
}
12561256

@@ -1267,23 +1267,30 @@ func (p *ChainPorter) importLocalAddresses(ctx context.Context,
12671267
return err
12681268
}
12691269

1270+
log.Infof("Importing anchor output key for output %d "+
1271+
"(isTombstone=%v, isBurn=%v): outpoint=%v, key=%x",
1272+
idx, out.IsTombstone(), out.IsBurn(),
1273+
out.Anchor.OutPoint,
1274+
anchorOutputKey.SerializeCompressed())
1275+
12701276
// Before we broadcast the transaction to the network, we'll
12711277
// import the new anchor output into the wallet so it watches
12721278
// it for spends and also takes account of the BTC we used in
12731279
// the transfer.
12741280
_, err = p.cfg.Wallet.ImportTaprootOutput(ctx, anchorOutputKey)
1275-
switch {
1276-
case err == nil:
1277-
break
1278-
1279-
// On restart, we'll get an error that the output has already
1280-
// been added to the wallet, so we'll catch this now and move
1281-
// along if so.
1282-
case strings.Contains(err.Error(), "already exists"):
1283-
break
1281+
if err != nil {
1282+
// On restart, we'll get an error that the output has
1283+
// already been added to the wallet, so we'll catch this
1284+
// now and move along if so.
1285+
if strings.Contains(err.Error(), "already exists") {
1286+
log.Tracef("Anchor output key already exists "+
1287+
"(outpoint=%v): %w",
1288+
out.Anchor.OutPoint, err)
1289+
continue
1290+
}
12841291

1285-
default:
1286-
return err
1292+
return fmt.Errorf("unable to import anchor output "+
1293+
"key: %w", err)
12871294
}
12881295
}
12891296

@@ -1446,6 +1453,7 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
14461453

14471454
currentPkg.VirtualPackets = fundSendRes.VPackets
14481455
currentPkg.InputCommitments = fundSendRes.InputCommitments
1456+
currentPkg.ZeroValueInputs = fundSendRes.ZeroValueInputs
14491457

14501458
currentPkg.SendState = SendStateVirtualSign
14511459

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

15921600
anchorTx, err := wallet.AnchorVirtualTransactions(
15931601
ctx, &AnchorVTxnsParams{
1594-
FeeRate: feeRate,
1595-
ActivePackets: currentPkg.VirtualPackets,
1596-
PassivePackets: currentPkg.PassiveAssets,
1602+
FeeRate: feeRate,
1603+
ActivePackets: currentPkg.VirtualPackets,
1604+
PassivePackets: currentPkg.PassiveAssets,
1605+
ZeroValueInputs: currentPkg.ZeroValueInputs,
15971606
},
15981607
)
15991608
if err != nil {
@@ -1695,8 +1704,8 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
16951704
parcel, err := ConvertToTransfer(
16961705
currentHeight, currentPkg.VirtualPackets,
16971706
currentPkg.AnchorTx, currentPkg.PassiveAssets,
1698-
isLocalKey, currentPkg.Label,
1699-
currentPkg.SkipAnchorTxBroadcast,
1707+
currentPkg.ZeroValueInputs, isLocalKey,
1708+
currentPkg.Label, currentPkg.SkipAnchorTxBroadcast,
17001709
)
17011710
if err != nil {
17021711
p.unlockInputs(ctx, &currentPkg)
@@ -1715,6 +1724,8 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
17151724
// Write the parcel to disk as a pending parcel. This step also
17161725
// records the transfer details (e.g., reference to the anchor
17171726
// transaction ID, transfer outputs and inputs) to the database.
1727+
// This will also extend the leases for both asset inputs and
1728+
// zero-value UTXOs to prevent them from being used elsewhere.
17181729
err = p.cfg.ExportLog.LogPendingParcel(
17191730
ctx, parcel, defaultWalletLeaseIdentifier,
17201731
time.Now().Add(defaultBroadcastCoinLeaseDuration),
@@ -1883,18 +1894,33 @@ func (p *ChainPorter) unlockInputs(ctx context.Context, pkg *sendPackage) {
18831894
// sanity-check that we have known input commitments to unlock, since
18841895
// that might not always be the case (for example if another party
18851896
// contributes inputs).
1886-
if pkg.SendState < SendStateStorePreBroadcast &&
1887-
len(pkg.InputCommitments) > 0 {
1897+
// Also unlock any zero-value UTXOs that were leased for this package.
1898+
if pkg.SendState < SendStateStorePreBroadcast {
1899+
// Gather all outpoints to unlock in a single array
1900+
var outpoints []wire.OutPoint
18881901

1902+
// Add input commitment outpoints
18891903
for prevID := range pkg.InputCommitments {
18901904
log.Debugf("Unlocking input %v", prevID.OutPoint)
1905+
outpoints = append(outpoints, prevID.OutPoint)
1906+
}
1907+
1908+
// Add zero-value inputs
1909+
zeroValueOutpoints := fn.Map(
1910+
pkg.ZeroValueInputs,
1911+
func(z *ZeroValueInput) wire.OutPoint {
1912+
return z.OutPoint
1913+
},
1914+
)
1915+
outpoints = append(outpoints, zeroValueOutpoints...)
18911916

1917+
// Release all coins in a single call
1918+
if len(outpoints) > 0 {
18921919
err := p.cfg.AssetWallet.ReleaseCoins(
1893-
ctx, prevID.OutPoint,
1920+
ctx, outpoints...,
18941921
)
18951922
if err != nil {
1896-
log.Warnf("Unable to unlock input %v: %v",
1897-
prevID.OutPoint, err)
1923+
log.Warnf("Unable to unlock inputs: %v", err)
18981924
}
18991925
}
19001926
}

tapfreighter/coin_select.go

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -114,20 +114,6 @@ func (s *CoinSelect) SelectCoins(ctx context.Context,
114114
return selectedCoins, nil
115115
}
116116

117-
// LeaseCoins leases/locks/reserves coins for the given lease owner until the
118-
// given expiry. This is used to prevent multiple concurrent coin selection
119-
// attempts from selecting the same coin(s).
120-
func (s *CoinSelect) LeaseCoins(ctx context.Context, leaseOwner [32]byte,
121-
expiry time.Time, utxoOutpoints ...wire.OutPoint) error {
122-
123-
s.coinLock.Lock()
124-
defer s.coinLock.Unlock()
125-
126-
return s.coinLister.LeaseCoins(
127-
ctx, leaseOwner, expiry, utxoOutpoints...,
128-
)
129-
}
130-
131117
// ReleaseCoins releases/unlocks coins that were previously leased and makes
132118
// them available for coin selection again.
133119
func (s *CoinSelect) ReleaseCoins(ctx context.Context,
@@ -199,4 +185,46 @@ func (s *CoinSelect) selectForAmount(minTotalAmount uint64,
199185
return selectedCommitments, nil
200186
}
201187

188+
// SelectZeroValueCoins fetches all managed UTXOs that contain only
189+
// zero-value assets (tombstones and burns). The selected UTXOs are
190+
// leased for the default lease duration.
191+
func (s *CoinSelect) SelectZeroValueCoins(ctx context.Context) (
192+
[]*ZeroValueInput, error) {
193+
194+
s.coinLock.Lock()
195+
defer s.coinLock.Unlock()
196+
197+
// Fetch all zero-value UTXOs that are eligible for sweeping.
198+
zeroValueInputs, err := s.coinLister.FetchZeroValueAnchorUTXOs(ctx)
199+
if err != nil {
200+
return nil, fmt.Errorf("unable to fetch zero-value UTXOs: %w",
201+
err)
202+
}
203+
204+
// We now need to lock/lease/reserve those selected coins so
205+
// that they can't be used by other processes.
206+
if len(zeroValueInputs) > 0 {
207+
expiry := time.Now().Add(defaultCoinLeaseDuration)
208+
zeroValueOutpoints := fn.Map(
209+
zeroValueInputs,
210+
func(z *ZeroValueInput) wire.OutPoint {
211+
return z.OutPoint
212+
},
213+
)
214+
err = s.coinLister.LeaseCoins(
215+
ctx, defaultWalletLeaseIdentifier, expiry,
216+
zeroValueOutpoints...,
217+
)
218+
if err != nil {
219+
return nil, fmt.Errorf("unable to lease zero-value "+
220+
"UTXOs: %w", err)
221+
}
222+
223+
log.Debugf("Selected and leased %d zero-value UTXOs",
224+
len(zeroValueInputs))
225+
}
226+
227+
return zeroValueInputs, nil
228+
}
229+
202230
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/fund.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import (
2626
func createFundedPacketWithInputs(ctx context.Context, exporter proof.Exporter,
2727
keyRing KeyRing, addrBook AddrBook, fundDesc *tapsend.FundingDescriptor,
2828
vPktTemplate *tappsbt.VPacket,
29-
selectedCommitments []*AnchoredCommitment) (*FundedVPacket, error) {
29+
selectedCommitments []*AnchoredCommitment,
30+
zeroValueInputs []*ZeroValueInput) (*FundedVPacket, error) {
3031

3132
if vPktTemplate.ChainParams == nil {
3233
return nil, errors.New("chain params not set in virtual packet")
@@ -150,6 +151,7 @@ func createFundedPacketWithInputs(ctx context.Context, exporter proof.Exporter,
150151
return &FundedVPacket{
151152
VPackets: allPackets,
152153
InputCommitments: inputCommitments,
154+
ZeroValueInputs: zeroValueInputs,
153155
}, nil
154156
}
155157

tapfreighter/fund_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ func TestFundPacket(t *testing.T) {
321321
vPkt *tappsbt.VPacket
322322
inputProofs []*proof.Proof
323323
selectedCommitments []*AnchoredCommitment
324+
zeroValueInputs []*ZeroValueInput
324325
keysDerived int
325326
expectedErr string
326327
expectedInputCommitments tappsbt.InputCommitments
@@ -350,6 +351,28 @@ func TestFundPacket(t *testing.T) {
350351
Commitment: inputCommitment,
351352
Asset: &inputAsset,
352353
}},
354+
zeroValueInputs: []*ZeroValueInput{
355+
{
356+
OutPoint: wire.OutPoint{
357+
Hash: test.RandHash(),
358+
Index: 1,
359+
},
360+
OutputValue: 1000,
361+
InternalKey: internalKey,
362+
MerkleRoot: test.RandBytes(32),
363+
PkScript: test.RandBytes(34),
364+
},
365+
{
366+
OutPoint: wire.OutPoint{
367+
Hash: test.RandHash(),
368+
Index: 0,
369+
},
370+
OutputValue: 546,
371+
InternalKey: internalKey,
372+
MerkleRoot: test.RandBytes(32),
373+
PkScript: test.RandBytes(34),
374+
},
375+
},
353376
keysDerived: 3,
354377
expectedInputCommitments: tappsbt.InputCommitments{
355378
inputPrevID: inputCommitment,
@@ -765,6 +788,7 @@ func TestFundPacket(t *testing.T) {
765788
result, err := createFundedPacketWithInputs(
766789
ctx, exporter, keyRing, addrBook,
767790
tc.fundDesc, tc.vPkt, tc.selectedCommitments,
791+
tc.zeroValueInputs,
768792
)
769793

770794
keyRing.AssertNumberOfCalls(
@@ -788,6 +812,13 @@ func TestFundPacket(t *testing.T) {
788812
tt, result.VPackets,
789813
tc.expectedOutputs(tt, keyRing),
790814
)
815+
816+
// Verify zero-value inputs are correctly added to the
817+
// result.
818+
require.Len(
819+
tt, result.ZeroValueInputs,
820+
len(tc.zeroValueInputs),
821+
)
791822
})
792823
}
793824
}

0 commit comments

Comments
 (0)