Skip to content

Commit 07af5c5

Browse files
committed
assets: add asset deposit sweeper
This commit adds a sweeper tailored for asset deposits, capable of sweeping a TAP deposit (a Kit) using either the timeout path or a revealed co-signer key. This lays the groundwork for implementing client-specific sweeping logic.
1 parent ad18b17 commit 07af5c5

File tree

1 file changed

+348
-0
lines changed

1 file changed

+348
-0
lines changed

assets/deposit/sweeper.go

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
package deposit
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"strings"
8+
"time"
9+
10+
"github.com/btcsuite/btcd/btcec/v2"
11+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
12+
"github.com/btcsuite/btcd/btcutil/psbt"
13+
"github.com/btcsuite/btcd/txscript"
14+
"github.com/btcsuite/btcd/wire"
15+
"github.com/btcsuite/btcwallet/wallet"
16+
"github.com/btcsuite/btcwallet/wtxmgr"
17+
"github.com/lightninglabs/lndclient"
18+
"github.com/lightninglabs/loop/assets"
19+
"github.com/lightninglabs/loop/utils"
20+
"github.com/lightninglabs/taproot-assets/address"
21+
"github.com/lightninglabs/taproot-assets/asset"
22+
"github.com/lightninglabs/taproot-assets/proof"
23+
"github.com/lightninglabs/taproot-assets/taprpc"
24+
"github.com/lightningnetwork/lnd/input"
25+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
26+
)
27+
28+
// Sweeper is a higher level type that provides methods to sweep asset deposits.
29+
type Sweeper struct {
30+
tapdClient *assets.TapdClient
31+
walletKit lndclient.WalletKitClient
32+
signer lndclient.SignerClient
33+
34+
addressParams address.ChainParams
35+
}
36+
37+
// NewSweeper creates a new Sweeper instance.
38+
func NewSweeper(tapdClient *assets.TapdClient,
39+
walletKit lndclient.WalletKitClient, signer lndclient.SignerClient,
40+
addressParams address.ChainParams) *Sweeper {
41+
42+
return &Sweeper{
43+
tapdClient: tapdClient,
44+
walletKit: walletKit,
45+
signer: signer,
46+
addressParams: addressParams,
47+
}
48+
}
49+
50+
// PublishDepositSweepMuSig2 publishes an interactive deposit sweep using the
51+
// MuSig2 keyspend path.
52+
func (s *Sweeper) PublishDepositSweepMuSig2(ctx context.Context, deposit *Kit,
53+
funder bool, depositProof *proof.Proof,
54+
otherInternalKey *btcec.PrivateKey, sweepScriptKey asset.ScriptKey,
55+
sweepInternalKey *btcec.PublicKey, label string,
56+
feeRate chainfee.SatPerVByte, lockID wtxmgr.LockID,
57+
lockDuration time.Duration) (*taprpc.SendAssetResponse, error) {
58+
59+
// Verify that the proof is valid for the deposit and get the root hash
60+
// which we will be using as our taproot tweak.
61+
rootHash, err := deposit.VerifyProof(depositProof)
62+
if err != nil {
63+
log.Errorf("failed to verify deposit proof: %v", err)
64+
65+
return nil, err
66+
}
67+
68+
// Create the sweep vpacket which is simply sweeping the asset on the
69+
// OP_TRUE output to a new output with the provided script and internal
70+
// keys.
71+
sweepVpkt, err := assets.CreateOpTrueSweepVpkt(
72+
ctx, []*proof.Proof{depositProof}, sweepScriptKey,
73+
sweepInternalKey, nil, &s.addressParams,
74+
)
75+
if err != nil {
76+
return nil, err
77+
}
78+
79+
// Gather the list of leased UTXOs that are used for the deposit sweep.
80+
// This is needed to ensure that the UTXOs are correctly reused if we
81+
// re-publish the deposit sweep.
82+
leases, err := s.walletKit.ListLeases(ctx)
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
var leasedUtxos []lndclient.LeaseDescriptor
88+
for _, lease := range leases {
89+
if lease.LockID == lockID {
90+
leasedUtxos = append(leasedUtxos, lease)
91+
}
92+
}
93+
94+
// By committing the virtual transaction to the BTC template we created,
95+
// the underlying lnd node will fund the BTC level transaction with an
96+
// input to pay for the fees (and it will also add a change output).
97+
sweepBtcPkt, activeAssets, passiveAssets, commitResp, err :=
98+
s.tapdClient.PrepareAndCommitVirtualPsbts(
99+
ctx, sweepVpkt, feeRate, nil, s.addressParams.Params,
100+
leasedUtxos, &lockID, lockDuration,
101+
)
102+
if err != nil {
103+
return nil, err
104+
}
105+
106+
prevOutFetcher := wallet.PsbtPrevOutputFetcher(sweepBtcPkt)
107+
sigHash, err := getSigHash(sweepBtcPkt.UnsignedTx, 0, prevOutFetcher)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
tweaks := &input.MuSig2Tweaks{
113+
TaprootTweak: rootHash,
114+
}
115+
116+
pubKey := deposit.FunderScriptKey
117+
otherInternalPubKey := deposit.CoSignerInternalKey
118+
if !funder {
119+
pubKey = deposit.CoSignerScriptKey
120+
otherInternalPubKey = deposit.FunderInternalKey
121+
}
122+
123+
internalPubKey, internalKey, err := DeriveSharedDepositKey(
124+
ctx, s.signer, pubKey,
125+
)
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
finalSig, err := utils.MuSig2Sign(
131+
input.MuSig2Version100RC2,
132+
[]*btcec.PrivateKey{
133+
internalKey, otherInternalKey,
134+
},
135+
[]*btcec.PublicKey{
136+
internalPubKey, otherInternalPubKey,
137+
},
138+
tweaks, sigHash,
139+
)
140+
if err != nil {
141+
return nil, err
142+
}
143+
144+
// Make sure that the signature is valid for the tx sighash and deposit
145+
// internal key.
146+
schnorrSig, err := schnorr.ParseSignature(finalSig)
147+
if err != nil {
148+
return nil, err
149+
}
150+
151+
// Calculate the final, tweaked MuSig2 output key.
152+
taprootOutputKey := txscript.ComputeTaprootOutputKey(
153+
deposit.MuSig2Key.PreTweakedKey, rootHash,
154+
)
155+
156+
// Make sure we always return the parity stripped key.
157+
taprootOutputKey, _ = schnorr.ParsePubKey(schnorr.SerializePubKey(
158+
taprootOutputKey,
159+
))
160+
161+
// Finally, verify that the signature is valid for the sighash and
162+
// tweaked MuSig2 output key.
163+
if !schnorrSig.Verify(sigHash[:], taprootOutputKey) {
164+
return nil, fmt.Errorf("invalid signature")
165+
}
166+
167+
// Create the witness and add it to the sweep packet.
168+
var buf bytes.Buffer
169+
err = psbt.WriteTxWitness(&buf, wire.TxWitness{finalSig})
170+
if err != nil {
171+
return nil, err
172+
}
173+
174+
sweepBtcPkt.Inputs[0].FinalScriptWitness = buf.Bytes()
175+
176+
// Sign and finalize the sweep packet.
177+
signedBtcPacket, err := s.walletKit.SignPsbt(ctx, sweepBtcPkt)
178+
if err != nil {
179+
return nil, err
180+
}
181+
182+
finalizedBtcPacket, _, err := s.walletKit.FinalizePsbt(
183+
ctx, signedBtcPacket, "",
184+
)
185+
if err != nil {
186+
return nil, err
187+
}
188+
189+
// Finally publish the sweep and log the transfer.
190+
skipBroadcast := false
191+
sendAssetResp, err := s.tapdClient.LogAndPublish(
192+
ctx, finalizedBtcPacket, activeAssets, passiveAssets,
193+
commitResp, skipBroadcast, label,
194+
)
195+
196+
return sendAssetResp, err
197+
}
198+
199+
// PublishDepositTimeoutSweep publishes a deposit timeout sweep using the
200+
// timeout script spend path.
201+
func (s *Sweeper) PublishDepositTimeoutSweep(ctx context.Context, deposit *Kit,
202+
depositProof *proof.Proof, sweepScriptKey asset.ScriptKey,
203+
sweepInternalKey *btcec.PublicKey, label string,
204+
feeRate chainfee.SatPerVByte, lockID wtxmgr.LockID,
205+
lockDuration time.Duration) (*taprpc.SendAssetResponse, error) {
206+
207+
// Create the sweep vpacket which is simply sweeping the asset on the
208+
// OP_TRUE output to a new output with the provided script and internal
209+
// keys.
210+
sweepVpkt, err := assets.CreateOpTrueSweepVpkt(
211+
ctx, []*proof.Proof{depositProof}, sweepScriptKey,
212+
sweepInternalKey, nil, &s.addressParams,
213+
)
214+
if err != nil {
215+
log.Errorf("Unable to create timeout sweep vpkt: %v", err)
216+
217+
return nil, err
218+
}
219+
220+
// Gather the list of leased UTXOs that are used for the deposit sweep.
221+
// This is needed to ensure that the UTXOs are correctly reused if we
222+
// re-publish the deposit sweep.
223+
leases, err := s.walletKit.ListLeases(ctx)
224+
if err != nil {
225+
log.Errorf("Unable to list leases: %v", err)
226+
227+
return nil, err
228+
}
229+
230+
var leasedUtxos []lndclient.LeaseDescriptor
231+
for _, lease := range leases {
232+
if lease.LockID == lockID {
233+
leasedUtxos = append(leasedUtxos, lease)
234+
}
235+
}
236+
237+
// By committing the virtual transaction to the BTC template we created,
238+
// the underlying lnd node will fund the BTC level transaction with an
239+
// input to pay for the fees (and it will also add a change output).
240+
timeoutSweepBtcPkt, activeAssets, passiveAssets, commitResp, err :=
241+
s.tapdClient.PrepareAndCommitVirtualPsbts(
242+
ctx, sweepVpkt, feeRate, nil,
243+
s.addressParams.Params, leasedUtxos,
244+
&lockID, lockDuration,
245+
)
246+
if err != nil {
247+
log.Errorf("Unable to prepare and commit virtual psbt: %v",
248+
err)
249+
}
250+
251+
// Create the witness for the timeout sweep.
252+
witness, err := deposit.CreateTimeoutWitness(
253+
ctx, s.signer, depositProof, timeoutSweepBtcPkt,
254+
)
255+
if err != nil {
256+
log.Errorf("Unable to create timeout witness: %v", err)
257+
258+
return nil, err
259+
}
260+
261+
// Now add the witness to the sweep packet.
262+
var buf bytes.Buffer
263+
err = psbt.WriteTxWitness(&buf, witness)
264+
if err != nil {
265+
log.Errorf("Unable to write witness to buffer: %v", err)
266+
267+
return nil, err
268+
}
269+
270+
timeoutSweepBtcPkt.Inputs[0].SighashType = txscript.SigHashDefault
271+
timeoutSweepBtcPkt.Inputs[0].FinalScriptWitness = buf.Bytes()
272+
273+
// Sign and finalize the sweep packet.
274+
signedBtcPacket, err := s.walletKit.SignPsbt(ctx, timeoutSweepBtcPkt)
275+
if err != nil {
276+
log.Errorf("Unable to sign timeout sweep packet: %v", err)
277+
278+
return nil, err
279+
}
280+
281+
finalizedBtcPacket, _, err := s.walletKit.FinalizePsbt(
282+
ctx, signedBtcPacket, "",
283+
)
284+
if err != nil {
285+
log.Errorf("Unable to finalize timeout sweep packet: %v", err)
286+
287+
return nil, err
288+
}
289+
290+
anchorTxHash := depositProof.AnchorTx.TxHash()
291+
depositOutIdx := depositProof.InclusionProof.OutputIndex
292+
293+
// Register the deposit transfer. This essentially materializes an asset
294+
// "out of thin air" to ensure that LogAndPublish succeeds and the asset
295+
// balance will be updated correctly.
296+
depositScriptKey := depositProof.Asset.ScriptKey.PubKey
297+
_, err = s.tapdClient.RegisterTransfer(
298+
ctx, &taprpc.RegisterTransferRequest{
299+
AssetId: deposit.AssetID[:],
300+
GroupKey: nil,
301+
ScriptKey: depositScriptKey.SerializeCompressed(),
302+
Outpoint: &taprpc.OutPoint{
303+
Txid: anchorTxHash[:],
304+
OutputIndex: depositOutIdx,
305+
},
306+
},
307+
)
308+
if err != nil {
309+
if !strings.Contains(err.Error(), "proof already exists") {
310+
log.Errorf("Unable to register deposit transfer: %v",
311+
err)
312+
313+
return nil, err
314+
}
315+
}
316+
317+
// Publish the timeout sweep and log the transfer.
318+
sendAssetResp, err := s.tapdClient.LogAndPublish(
319+
ctx, finalizedBtcPacket, activeAssets, passiveAssets,
320+
commitResp, false, label,
321+
)
322+
if err != nil {
323+
log.Errorf("Failed to publish timeout sweep: %v", err)
324+
325+
return nil, err
326+
}
327+
328+
return sendAssetResp, nil
329+
}
330+
331+
// getSigHash calculates the signature hash for the given transaction.
332+
func getSigHash(tx *wire.MsgTx, idx int,
333+
prevOutFetcher txscript.PrevOutputFetcher) ([32]byte, error) {
334+
335+
var sigHash [32]byte
336+
337+
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
338+
taprootSigHash, err := txscript.CalcTaprootSignatureHash(
339+
sigHashes, txscript.SigHashDefault, tx, idx, prevOutFetcher,
340+
)
341+
if err != nil {
342+
return sigHash, err
343+
}
344+
345+
copy(sigHash[:], taprootSigHash)
346+
347+
return sigHash, nil
348+
}

0 commit comments

Comments
 (0)