Skip to content

Commit 8ad77bc

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 e599936 commit 8ad77bc

File tree

1 file changed

+335
-0
lines changed

1 file changed

+335
-0
lines changed

assets/deposit/sweeper.go

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

0 commit comments

Comments
 (0)