Skip to content

Commit 0137d22

Browse files
authored
Merge pull request #1181 from lightninglabs/enforce-unique-script-key
[tapsend]: Enforce unique script keys
2 parents e3a0aa1 + ae69e0a commit 0137d22

File tree

11 files changed

+478
-21
lines changed

11 files changed

+478
-21
lines changed

itest/assertions.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1108,7 +1108,10 @@ func AssertAssetOutboundTransferWithOutputs(t *testing.T,
11081108
out := transfer.Outputs[idx]
11091109
require.Contains(t, outpoints, out.Anchor.Outpoint)
11101110
require.Contains(t, scripts, string(out.ScriptKey))
1111-
require.Equal(t, expectedAmounts[idx], out.Amount)
1111+
require.Equalf(
1112+
t, expectedAmounts[idx], out.Amount,
1113+
"expected amounts: %v, transfer: %v",
1114+
expectedAmounts, toJSON(t, transfer))
11121115
}
11131116

11141117
firstIn := transfer.Inputs[0]

itest/fee_estimation_test.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,11 @@ func testFeeEstimation(t *harnessTest) {
6666
rpcAssets := MintAssetsConfirmBatch(
6767
t.t, t.lndHarness.Miner().Client, t.tapd, simpleAssets,
6868
)
69+
normalAsset := rpcAssets[0]
70+
normalAssetId := normalAsset.AssetGenesis.AssetId
6971

7072
// Check the final fee rate of the mint TX.
71-
rpcMintOutpoint := rpcAssets[0].ChainAnchor.AnchorOutpoint
73+
rpcMintOutpoint := normalAsset.ChainAnchor.AnchorOutpoint
7274
mintOutpoint, err := wire.NewOutPointFromString(rpcMintOutpoint)
7375
require.NoError(t.t, err)
7476

@@ -81,16 +83,15 @@ func testFeeEstimation(t *harnessTest) {
8183
)
8284

8385
// Split the normal asset to create a transfer with two anchor outputs.
84-
normalAssetId := rpcAssets[0].AssetGenesis.AssetId
85-
splitAmount := rpcAssets[0].Amount / 2
86+
splitAmount := normalAsset.Amount / 2
8687
addr, stream := NewAddrWithEventStream(
8788
t.t, t.tapd, &taprpc.NewAddrRequest{
8889
AssetId: normalAssetId,
8990
Amt: splitAmount,
9091
},
9192
)
9293

93-
AssertAddrCreated(t.t, t.tapd, rpcAssets[0], addr)
94+
AssertAddrCreated(t.t, t.tapd, normalAsset, addr)
9495
sendResp, sendEvents := sendAssetsToAddr(t, t.tapd, addr)
9596

9697
transferIdx := 0
@@ -121,7 +122,7 @@ func testFeeEstimation(t *harnessTest) {
121122
},
122123
)
123124

124-
AssertAddrCreated(t.t, t.tapd, rpcAssets[0], addr2)
125+
AssertAddrCreated(t.t, t.tapd, normalAsset, addr2)
125126
sendResp, sendEvents = sendAssetsToAddr(t, t.tapd, addr2)
126127

127128
ConfirmAndAssertOutboundTransfer(
@@ -157,7 +158,7 @@ func testFeeEstimation(t *harnessTest) {
157158
[]*UTXORequest{initialUTXOs[3]},
158159
)
159160

160-
AssertAddrCreated(t.t, t.tapd, rpcAssets[0], addr3)
161+
AssertAddrCreated(t.t, t.tapd, normalAsset, addr3)
161162
_, err = t.tapd.SendAsset(ctxt, &taprpc.SendAssetRequest{
162163
TapAddrs: []string{addr3.Encoded},
163164
})
@@ -166,23 +167,24 @@ func testFeeEstimation(t *harnessTest) {
166167
)
167168

168169
// The transfer should also be rejected if the manually-specified
169-
// feerate fails the sanity check against the fee estimator's fee floor
170+
// fee rate fails the sanity check against the fee estimator's fee floor
170171
// of 253 sat/kw, or 1.012 sat/vB.
171172
_, err = t.tapd.SendAsset(ctxt, &taprpc.SendAssetRequest{
172173
TapAddrs: []string{addr3.Encoded},
173174
FeeRate: uint32(chainfee.FeePerKwFloor) - 1,
174175
})
175176
require.ErrorContains(t.t, err, "manual fee rate below floor")
176177

177-
// After failure at the high feerate, we should still be able to make a
178-
// transfer at a very low feerate.
178+
// After failure at the high fee rate, we should still be able to make a
179+
// transfer at a very low fee rate.
179180
t.lndHarness.SetFeeEstimateWithConf(lowFeeRate, 6)
180181
sendResp, sendEvents = sendAssetsToAddr(t, t.tapd, addr3)
181182

182183
ConfirmAndAssertOutboundTransfer(
183184
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp,
184-
normalAssetId, []uint64{thirdSplitAmount, thirdSplitAmount},
185-
transferIdx, transferIdx+1,
185+
normalAssetId, []uint64{
186+
splitAmount - thirdSplitAmount, thirdSplitAmount,
187+
}, transferIdx, transferIdx+1,
186188
)
187189
transferIdx += 1
188190
AssertNonInteractiveRecvComplete(t.t, t.tapd, transferIdx)

itest/send_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
1818
"github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc"
1919
unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc"
20+
"github.com/lightninglabs/taproot-assets/tapsend"
2021
"github.com/lightningnetwork/lnd/lntest/wait"
2122
"github.com/stretchr/testify/require"
2223
)
@@ -102,6 +103,16 @@ func testBasicSendUnidirectional(t *harnessTest) {
102103
})
103104
require.NoError(t.t, err)
104105

106+
// Before we start sending, we test that we aren't allowed to send to
107+
// the same address more than once within the same transfer.
108+
_, err = t.tapd.SendAsset(ctxb, &taprpc.SendAssetRequest{
109+
TapAddrs: []string{
110+
bobAddr.Encoded,
111+
bobAddr.Encoded,
112+
},
113+
})
114+
require.ErrorContains(t.t, err, tapsend.ErrDuplicateScriptKeys.Error())
115+
105116
for i := 0; i < numSends; i++ {
106117
t.t.Logf("Performing send procedure: %d", i)
107118

proof/proof_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,8 @@ func TestProofVerification(t *testing.T) {
758758

759759
t.Logf("Proof asset JSON: %s", assetJSON)
760760

761+
// If we have a challenge witness, we can verify that without having the
762+
// previous proof.
761763
if len(p.ChallengeWitness) > 0 {
762764
_, err = p.Verify(
763765
context.Background(), nil, MockHeaderVerifier,
@@ -766,6 +768,11 @@ func TestProofVerification(t *testing.T) {
766768
require.NoError(t, err)
767769
}
768770

771+
// Verifying the inclusion and exclusion proofs can also be done without
772+
// the previous proof.
773+
_, err = p.VerifyProofs()
774+
require.NoError(t, err)
775+
769776
// Ensure that verification of a proof of unknown version fails.
770777
p.Version = TransitionVersion(212)
771778

tapfreighter/chain_porter.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1075,9 +1075,14 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
10751075
// At this point, we have everything we need to sign our _virtual_
10761076
// transaction on the Taproot Asset layer.
10771077
case SendStateVirtualSign:
1078+
ctx, cancel := p.WithCtxQuitNoTimeout()
1079+
defer cancel()
1080+
10781081
vPackets := currentPkg.VirtualPackets
10791082
err := tapsend.ValidateVPacketVersions(vPackets)
10801083
if err != nil {
1084+
p.unlockInputs(ctx, &currentPkg)
1085+
10811086
return nil, err
10821087
}
10831088

@@ -1091,6 +1096,8 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
10911096

10921097
_, err := p.cfg.AssetWallet.SignVirtualPacket(vPkt)
10931098
if err != nil {
1099+
p.unlockInputs(ctx, &currentPkg)
1100+
10941101
return nil, fmt.Errorf("unable to sign and "+
10951102
"commit virtual packet: %w", err)
10961103
}
@@ -1125,6 +1132,8 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
11251132
ctx, tapsend.SendConfTarget,
11261133
)
11271134
if err != nil {
1135+
p.unlockInputs(ctx, &currentPkg)
1136+
11281137
return nil, fmt.Errorf("unable to estimate "+
11291138
"fee: %w", err)
11301139
}
@@ -1148,6 +1157,8 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
11481157
currentPkg.InputCommitments,
11491158
)
11501159
if err != nil {
1160+
p.unlockInputs(ctx, &currentPkg)
1161+
11511162
return nil, fmt.Errorf("unable to create passive "+
11521163
"assets: %w", err)
11531164
}
@@ -1156,6 +1167,8 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
11561167
len(currentPkg.PassiveAssets))
11571168
err = wallet.SignPassiveAssets(currentPkg.PassiveAssets)
11581169
if err != nil {
1170+
p.unlockInputs(ctx, &currentPkg)
1171+
11591172
return nil, fmt.Errorf("unable to sign passive "+
11601173
"assets: %w", err)
11611174
}
@@ -1168,6 +1181,8 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
11681181
},
11691182
)
11701183
if err != nil {
1184+
p.unlockInputs(ctx, &currentPkg)
1185+
11711186
return nil, fmt.Errorf("unable to anchor virtual "+
11721187
"transactions: %w", err)
11731188
}
@@ -1382,10 +1397,44 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
13821397

13831398
// unlockInputs unlocks the inputs that were locked for the given package.
13841399
func (p *ChainPorter) unlockInputs(ctx context.Context, pkg *sendPackage) {
1385-
if pkg == nil || pkg.AnchorTx == nil || pkg.AnchorTx.FundedPsbt == nil {
1400+
// Impossible state, but catch it anyway.
1401+
if pkg == nil {
1402+
return
1403+
}
1404+
1405+
// If we haven't even attempted to broadcast yet, we're still in a state
1406+
// where we give feedback to the user synchronously, as we haven't
1407+
// created an on-chain transaction that we need to await confirmation.
1408+
// We also haven't written the transfer to disk yet, so we can just
1409+
// release/unlock the _asset_ level UTXOs so the user can try again. We
1410+
// sanity-check that we have known input commitments to unlock, since
1411+
// that might not always be the case (for example if another party
1412+
// contributes inputs).
1413+
if pkg.SendState < SendStateStorePreBroadcast &&
1414+
len(pkg.InputCommitments) > 0 {
1415+
1416+
for prevID := range pkg.InputCommitments {
1417+
log.Debugf("Unlocking input %v", prevID.OutPoint)
1418+
1419+
err := p.cfg.AssetWallet.ReleaseCoins(
1420+
ctx, prevID.OutPoint,
1421+
)
1422+
if err != nil {
1423+
log.Warnf("Unable to unlock input %v: %v",
1424+
prevID.OutPoint, err)
1425+
}
1426+
}
1427+
}
1428+
1429+
// If we're in another state, the anchor transaction has been created,
1430+
// and we can't simply unlock the asset level inputs. This will likely
1431+
// require manual intervention.
1432+
if pkg.AnchorTx == nil || pkg.AnchorTx.FundedPsbt == nil {
13861433
return
13871434
}
13881435

1436+
// We need to unlock any _BTC_ level inputs we locked for the anchor
1437+
// transaction.
13891438
for _, op := range pkg.AnchorTx.FundedPsbt.LockedUTXOs {
13901439
err := p.cfg.Wallet.UnlockInput(ctx, op)
13911440
if err != nil {

tapfreighter/coin_select.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,14 @@ func (s *CoinSelect) SelectCoins(ctx context.Context,
6767
return nil, ErrMatchingAssetsNotFound
6868
}
6969

70-
log.Infof("Identified %v eligible asset inputs for send of %d to %v",
71-
len(eligibleCommitments), constraints.MinAmt, constraints)
70+
anchorInputs := fn.Map(
71+
eligibleCommitments, func(c *AnchoredCommitment) string {
72+
return c.AnchorPoint.String()
73+
},
74+
)
75+
log.Infof("Identified %v eligible asset inputs for send of %d to %v: "+
76+
"%v", len(anchorInputs), constraints.MinAmt,
77+
constraints.String(), anchorInputs)
7278

7379
// Only select coins anchored in a compatible commitment.
7480
compatibleCommitments := fn.Filter(

tapfreighter/wallet.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/lightninglabs/taproot-assets/tapsend"
2626
"github.com/lightningnetwork/lnd/keychain"
2727
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
28+
"golang.org/x/exp/maps"
2829
)
2930

3031
const (
@@ -129,6 +130,10 @@ type Wallet interface {
129130
// address.ErrInternalKeyNotFound is returned.
130131
FetchInternalKeyLocator(ctx context.Context,
131132
rawKey *btcec.PublicKey) (keychain.KeyLocator, error)
133+
134+
// ReleaseCoins releases/unlocks coins that were previously leased and
135+
// makes them available for coin selection again.
136+
ReleaseCoins(ctx context.Context, utxoOutpoints ...wire.OutPoint) error
132137
}
133138

134139
// AddrBook is an interface that provides access to the address book.
@@ -752,7 +757,7 @@ func (f *AssetWallet) fundPacketWithInputs(ctx context.Context,
752757
}
753758

754759
if err := tapsend.PrepareOutputAssets(ctx, vPkt); err != nil {
755-
return nil, fmt.Errorf("unable to create split commit: %w", err)
760+
return nil, fmt.Errorf("unable to prepare outputs: %w", err)
756761
}
757762

758763
return &FundedVPacket{
@@ -1206,7 +1211,7 @@ func (f *AssetWallet) CreatePassiveAssets(ctx context.Context,
12061211
}
12071212

12081213
// Gather passive assets found in each input Taproot Asset commitment.
1209-
var passivePackets []*tappsbt.VPacket
1214+
passivePackets := make(map[asset.PrevID]*tappsbt.VPacket)
12101215
for prevID := range inputCommitments {
12111216
tapCommitment := inputCommitments[prevID]
12121217

@@ -1244,6 +1249,16 @@ func (f *AssetWallet) CreatePassiveAssets(ctx context.Context,
12441249
"proof: %w", err)
12451250
}
12461251

1252+
scriptKey := passiveAsset.ScriptKey.PubKey
1253+
passivePrevID := asset.PrevID{
1254+
OutPoint: prevID.OutPoint,
1255+
ID: passiveAsset.ID(),
1256+
ScriptKey: asset.ToSerialized(scriptKey),
1257+
}
1258+
log.Tracef("Adding passive packet for asset_id=%v, "+
1259+
"script_key=%x", passiveAsset.ID().String(),
1260+
scriptKey.SerializeCompressed())
1261+
12471262
passivePacket, err := createPassivePacket(
12481263
f.cfg.ChainParams, passiveAsset, activePackets,
12491264
anchorOutIdx, *anchorOutDesc, prevID.OutPoint,
@@ -1254,11 +1269,11 @@ func (f *AssetWallet) CreatePassiveAssets(ctx context.Context,
12541269
"passive packet: %w", err)
12551270
}
12561271

1257-
passivePackets = append(passivePackets, passivePacket)
1272+
passivePackets[passivePrevID] = passivePacket
12581273
}
12591274
}
12601275

1261-
return passivePackets, nil
1276+
return maps.Values(passivePackets), nil
12621277
}
12631278

12641279
// SignPassiveAssets signs the given passive asset packets.
@@ -1462,6 +1477,14 @@ func (f *AssetWallet) FetchInternalKeyLocator(ctx context.Context,
14621477
return f.cfg.AddrBook.FetchInternalKeyLocator(ctx, rawKey)
14631478
}
14641479

1480+
// ReleaseCoins releases/unlocks coins that were previously leased and makes
1481+
// them available for coin selection again.
1482+
func (f *AssetWallet) ReleaseCoins(ctx context.Context,
1483+
utxoOutpoints ...wire.OutPoint) error {
1484+
1485+
return f.cfg.CoinSelector.ReleaseCoins(ctx, utxoOutpoints...)
1486+
}
1487+
14651488
// addAnchorPsbtInputs adds anchor information from all inputs to the PSBT
14661489
// packet. This is called after the PSBT has been funded, but before signing.
14671490
func addAnchorPsbtInputs(btcPkt *psbt.Packet, vPackets []*tappsbt.VPacket) {

tappsbt/interface.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,25 @@ func (p *VPacket) AssetID() (asset.ID, error) {
321321
return firstID, nil
322322
}
323323

324+
// AssetSpecifier returns the asset specifier for the asset being spent by the
325+
// first input of the virtual transaction. It returns an error if the asset ID
326+
// of the virtual transaction is not set or if the asset of the first input is
327+
// not set.
328+
func (p *VPacket) AssetSpecifier() (asset.Specifier, error) {
329+
assetID, err := p.AssetID()
330+
if err != nil {
331+
return asset.Specifier{}, err
332+
}
333+
334+
if p.Inputs[0].Asset() == nil {
335+
return asset.Specifier{}, fmt.Errorf("no asset set for input 0")
336+
}
337+
338+
return asset.NewSpecifier(
339+
&assetID, nil, p.Inputs[0].Asset().GroupKey, true,
340+
)
341+
}
342+
324343
// Anchor is a struct that contains all the information about an anchor output.
325344
type Anchor struct {
326345
// Value is output value of the anchor output.

0 commit comments

Comments
 (0)