Skip to content

Commit 3035704

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 4c44708 commit 3035704

File tree

1 file changed

+345
-0
lines changed

1 file changed

+345
-0
lines changed

assets/deposit/sweeper.go

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
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+
127+
finalSig, err := utils.MuSig2Sign(
128+
input.MuSig2Version100RC2,
129+
[]*btcec.PrivateKey{
130+
internalKey, otherInternalKey,
131+
},
132+
[]*btcec.PublicKey{
133+
internalPubKey, otherInternalPubKey,
134+
},
135+
tweaks, sigHash,
136+
)
137+
if err != nil {
138+
return nil, err
139+
}
140+
141+
// Make sure that the signature is valid for the tx sighash and deposit
142+
// internal key.
143+
schnorrSig, err := schnorr.ParseSignature(finalSig)
144+
if err != nil {
145+
return nil, err
146+
}
147+
148+
// Calculate the final, tweaked MuSig2 output key.
149+
taprootOutputKey := txscript.ComputeTaprootOutputKey(
150+
deposit.MuSig2Key.PreTweakedKey, rootHash[:],
151+
)
152+
153+
// Make sure we always return the parity stripped key.
154+
taprootOutputKey, _ = schnorr.ParsePubKey(schnorr.SerializePubKey(
155+
taprootOutputKey,
156+
))
157+
158+
// Finally, verify that the signature is valid for the sighash and
159+
// tweaked MuSig2 output key.
160+
if !schnorrSig.Verify(sigHash[:], taprootOutputKey) {
161+
return nil, fmt.Errorf("invalid signature")
162+
}
163+
164+
// Create the witness and add it to the sweep packet.
165+
var buf bytes.Buffer
166+
err = psbt.WriteTxWitness(&buf, wire.TxWitness{finalSig})
167+
if err != nil {
168+
return nil, err
169+
}
170+
171+
sweepBtcPkt.Inputs[0].FinalScriptWitness = buf.Bytes()
172+
173+
// Sign and finalize the sweep packet.
174+
signedBtcPacket, err := s.walletKit.SignPsbt(ctx, sweepBtcPkt)
175+
if err != nil {
176+
return nil, err
177+
}
178+
179+
finalizedBtcPacket, _, err := s.walletKit.FinalizePsbt(
180+
ctx, signedBtcPacket, "",
181+
)
182+
if err != nil {
183+
return nil, err
184+
}
185+
186+
// Finally publish the sweep and log the transfer.
187+
skipBroadcast := false
188+
sendAssetResp, err := s.tapdClient.LogAndPublish(
189+
ctx, finalizedBtcPacket, activeAssets, passiveAssets,
190+
commitResp, skipBroadcast, label,
191+
)
192+
193+
return sendAssetResp, err
194+
}
195+
196+
// PublishDepositTimeoutSweep publishes a deposit timeout sweep using the
197+
// timeout script spend path.
198+
func (s *Sweeper) PublishDepositTimeoutSweep(ctx context.Context, deposit *Kit,
199+
depositProof *proof.Proof, sweepScriptKey asset.ScriptKey,
200+
sweepInternalKey *btcec.PublicKey, label string,
201+
feeRate chainfee.SatPerVByte, lockID wtxmgr.LockID,
202+
lockDuration time.Duration) (*taprpc.SendAssetResponse, error) {
203+
204+
// Create the sweep vpacket which is simply sweeping the asset on the
205+
// OP_TRUE output to a new output with the provided script and internal
206+
// keys.
207+
sweepVpkt, err := assets.CreateOpTrueSweepVpkt(
208+
ctx, []*proof.Proof{depositProof}, sweepScriptKey,
209+
sweepInternalKey, nil, &s.addressParams,
210+
)
211+
if err != nil {
212+
log.Errorf("Unable to create timeout sweep vpkt: %v", err)
213+
214+
return nil, err
215+
}
216+
217+
// Gather the list of leased UTXOs that are used for the deposit sweep.
218+
// This is needed to ensure that the UTXOs are correctly reused if we
219+
// re-publish the deposit sweep.
220+
leases, err := s.walletKit.ListLeases(ctx)
221+
if err != nil {
222+
log.Errorf("Unable to list leases: %v", err)
223+
224+
return nil, err
225+
}
226+
227+
var leasedUtxos []lndclient.LeaseDescriptor
228+
for _, lease := range leases {
229+
if lease.LockID == lockID {
230+
leasedUtxos = append(leasedUtxos, lease)
231+
}
232+
}
233+
234+
// By committing the virtual transaction to the BTC template we created,
235+
// the underlying lnd node will fund the BTC level transaction with an
236+
// input to pay for the fees (and it will also add a change output).
237+
timeoutSweepBtcPkt, activeAssets, passiveAssets, commitResp, err :=
238+
s.tapdClient.PrepareAndCommitVirtualPsbts(
239+
ctx, sweepVpkt, feeRate, nil,
240+
s.addressParams.Params, leasedUtxos,
241+
&lockID, lockDuration,
242+
)
243+
if err != nil {
244+
log.Errorf("Unable to prepare and commit virtual psbt: %v",
245+
err)
246+
}
247+
248+
// Create the witness for the timeout sweep.
249+
witness, err := deposit.CreateTimeoutWitness(
250+
ctx, s.signer, depositProof, timeoutSweepBtcPkt,
251+
)
252+
if err != nil {
253+
log.Errorf("Unable to create timeout witness: %v", err)
254+
255+
return nil, err
256+
}
257+
258+
// Now add the witness to the sweep packet.
259+
var buf bytes.Buffer
260+
err = psbt.WriteTxWitness(&buf, witness)
261+
if err != nil {
262+
log.Errorf("Unable to write witness to buffer: %v", err)
263+
264+
return nil, err
265+
}
266+
267+
timeoutSweepBtcPkt.Inputs[0].SighashType = txscript.SigHashDefault
268+
timeoutSweepBtcPkt.Inputs[0].FinalScriptWitness = buf.Bytes()
269+
270+
// Sign and finalize the sweep packet.
271+
signedBtcPacket, err := s.walletKit.SignPsbt(ctx, timeoutSweepBtcPkt)
272+
if err != nil {
273+
log.Errorf("Unable to sign timeout sweep packet: %v", err)
274+
275+
return nil, err
276+
}
277+
278+
finalizedBtcPacket, _, err := s.walletKit.FinalizePsbt(
279+
ctx, signedBtcPacket, "",
280+
)
281+
if err != nil {
282+
log.Errorf("Unable to finalize timeout sweep packet: %v", err)
283+
284+
return nil, err
285+
}
286+
287+
anchorTxHash := depositProof.AnchorTx.TxHash()
288+
depositOutIdx := depositProof.InclusionProof.OutputIndex
289+
290+
// Register the deposit transfer. This essentially materializes an asset
291+
// "out of thin air" to ensure that LogAndPublish succeeds and the asset
292+
// balance will be updated correctly.
293+
depositScriptKey := depositProof.Asset.ScriptKey.PubKey
294+
_, err = s.tapdClient.RegisterTransfer(
295+
ctx, &taprpc.RegisterTransferRequest{
296+
AssetId: deposit.AssetID[:],
297+
GroupKey: nil,
298+
ScriptKey: depositScriptKey.SerializeCompressed(),
299+
Outpoint: &taprpc.OutPoint{
300+
Txid: anchorTxHash[:],
301+
OutputIndex: depositOutIdx,
302+
},
303+
},
304+
)
305+
if err != nil {
306+
if !strings.Contains(err.Error(), "proof already exists") {
307+
log.Errorf("Unable to register deposit transfer: %v",
308+
err)
309+
310+
return nil, err
311+
}
312+
}
313+
314+
// Publish the timeout sweep and log the transfer.
315+
sendAssetResp, err := s.tapdClient.LogAndPublish(
316+
ctx, finalizedBtcPacket, activeAssets, passiveAssets,
317+
commitResp, false, label,
318+
)
319+
if err != nil {
320+
log.Errorf("Failed to publish timeout sweep: %v", err)
321+
322+
return nil, err
323+
}
324+
325+
return sendAssetResp, nil
326+
}
327+
328+
// getSigHash calculates the signature hash for the given transaction.
329+
func getSigHash(tx *wire.MsgTx, idx int,
330+
prevOutFetcher txscript.PrevOutputFetcher) ([32]byte, error) {
331+
332+
var sigHash [32]byte
333+
334+
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
335+
taprootSigHash, err := txscript.CalcTaprootSignatureHash(
336+
sigHashes, txscript.SigHashDefault, tx, idx, prevOutFetcher,
337+
)
338+
if err != nil {
339+
return sigHash, err
340+
}
341+
342+
copy(sigHash[:], taprootSigHash)
343+
344+
return sigHash, nil
345+
}

0 commit comments

Comments
 (0)