Skip to content

Commit 6a3d018

Browse files
committed
assets: add support for co-signing a zero-fee deposit HTLC spend
This commit extends the deposit manager and sweeper to support generating a zero-fee HTLC transaction that spends a selected deposit. Once the HTLC is prepared, it is partially signed by the client and can be handed to the server as a safety measure before using the deposit in a trustless swap. This typically occurs after the client has accepted the swap payment. If the client becomes unresponsive during the swap process, the server can use the zero-fee HTLC as part of a recovery package.
1 parent b017b49 commit 6a3d018

File tree

5 files changed

+310
-0
lines changed

5 files changed

+310
-0
lines changed

assets/deposit/manager.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package deposit
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
78
"strings"
89
"time"
910

1011
"github.com/btcsuite/btcd/btcec/v2"
12+
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
1113
"github.com/btcsuite/btcd/chaincfg"
1214
"github.com/btcsuite/btcd/wire"
15+
"github.com/btcsuite/btcwallet/wallet"
1316
"github.com/decred/dcrd/dcrec/secp256k1/v4"
1417
"github.com/lightninglabs/lndclient"
1518
"github.com/lightninglabs/loop/assets"
@@ -20,6 +23,7 @@ import (
2023
"github.com/lightninglabs/taproot-assets/rpcutils"
2124
"github.com/lightninglabs/taproot-assets/taprpc"
2225
"github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
26+
"github.com/lightningnetwork/lnd/lntypes"
2327
"google.golang.org/protobuf/proto"
2428
)
2529

@@ -1244,3 +1248,86 @@ func (m *Manager) RevealDepositKeys(ctx context.Context,
12441248

12451249
return err
12461250
}
1251+
1252+
// PushHtlcSig will partially a deposit spending zero-fee HTLC and send the
1253+
// resulting signature to the swap server.
1254+
func (m *Manager) PushHtlcSig(ctx context.Context, depositID string,
1255+
serverNonce [musig2.PubNonceSize]byte, hash lntypes.Hash,
1256+
csvExpiry uint32) error {
1257+
1258+
done, err := m.scheduleNextCall()
1259+
if err != nil {
1260+
return err
1261+
}
1262+
defer done()
1263+
1264+
deposit, ok := m.deposits[depositID]
1265+
if !ok {
1266+
return fmt.Errorf("deposit %v not available", depositID)
1267+
}
1268+
1269+
_, htlcPkt, _, _, _, err := m.sweeper.GetHTLC(
1270+
ctx, deposit.Kit, deposit.Proof, deposit.Amount, hash,
1271+
csvExpiry,
1272+
)
1273+
if err != nil {
1274+
log.Errorf("Unable to get HTLC packet: %v", err)
1275+
1276+
return err
1277+
}
1278+
1279+
prevOutFetcher := wallet.PsbtPrevOutputFetcher(htlcPkt)
1280+
sigHash, err := getSigHash(htlcPkt.UnsignedTx, 0, prevOutFetcher)
1281+
if err != nil {
1282+
return err
1283+
}
1284+
1285+
funder := true
1286+
depositSigner := NewMuSig2Signer(
1287+
m.signer, deposit.Kit, funder, deposit.AnchorRootHash,
1288+
)
1289+
1290+
err = depositSigner.NewSession(ctx)
1291+
if err != nil {
1292+
return fmt.Errorf("Unable to create MuSig2 session: %w", err)
1293+
}
1294+
1295+
publicNonce, err := depositSigner.PubNonce()
1296+
if err != nil {
1297+
return err
1298+
}
1299+
1300+
partialSig, err := depositSigner.PartialSignMuSig2(
1301+
serverNonce, sigHash,
1302+
)
1303+
if err != nil {
1304+
log.Errorf("Unable to create partial deposit signature %v: %v",
1305+
deposit.ID, err)
1306+
1307+
return err
1308+
}
1309+
1310+
var pktBuf bytes.Buffer
1311+
err = htlcPkt.Serialize(&pktBuf)
1312+
if err != nil {
1313+
return err
1314+
}
1315+
1316+
// TODO(bhandras): the server should return the final signature.
1317+
_, err = m.depositServiceClient.PushAssetDepositHtlcSigs(
1318+
ctx, &swapserverrpc.PushAssetDepositHtlcSigsReq{
1319+
Hash: hash[:],
1320+
CsvExpiry: csvExpiry,
1321+
PartialSigs: []*swapserverrpc.AssetDepositPartialSig{
1322+
{
1323+
DepositId: depositID,
1324+
Nonce: publicNonce[:],
1325+
PartialSig: partialSig,
1326+
},
1327+
},
1328+
HtlcPsbt: pktBuf.Bytes(),
1329+
},
1330+
)
1331+
1332+
return err
1333+
}

assets/deposit/server.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"encoding/hex"
66
"fmt"
77

8+
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
89
"github.com/lightninglabs/loop/looprpc"
910
"github.com/lightninglabs/taproot-assets/asset"
11+
"github.com/lightningnetwork/lnd/lntypes"
1012
"google.golang.org/grpc/codes"
1113
"google.golang.org/grpc/status"
1214
)
@@ -176,3 +178,39 @@ func (s *Server) WithdrawAssetDeposits(ctx context.Context,
176178

177179
return &looprpc.WithdrawAssetDepositsResponse{}, nil
178180
}
181+
182+
// PushAssetDepositHtlcSig is the rpc endpoint for loop clients to push partial
183+
// signatures for asset deposit spending zero fee HTLCs to the server.
184+
func (s *Server) PushAssetDepositHtlcSig(ctx context.Context,
185+
in *looprpc.PushAssetDepositHtlcSigRequest) (
186+
*looprpc.PushAssetDepositHtlcSigResponse, error) {
187+
188+
if len(in.Nonce) != musig2.PubNonceSize {
189+
return nil, status.Error(codes.InvalidArgument,
190+
fmt.Sprintf("invalid nonce length: expected %d bytes, "+
191+
"got %d", musig2.PubNonceSize, len(in.Nonce)))
192+
}
193+
194+
if len(in.PreimageHash) != lntypes.HashSize {
195+
return nil, status.Error(codes.InvalidArgument,
196+
fmt.Sprintf("invalid preimage hash length: expected "+
197+
"%d bytes, got %d", lntypes.HashSize,
198+
len(in.PreimageHash)))
199+
}
200+
201+
var (
202+
nonce [musig2.PubNonceSize]byte
203+
preimageHash lntypes.Hash
204+
)
205+
copy(nonce[:], in.Nonce)
206+
copy(preimageHash[:], in.PreimageHash)
207+
208+
err := s.manager.PushHtlcSig(
209+
ctx, in.DepositId, nonce, preimageHash, in.CsvExpiry,
210+
)
211+
if err != nil {
212+
return nil, status.Error(codes.Internal, err.Error())
213+
}
214+
215+
return &looprpc.PushAssetDepositHtlcSigResponse{}, nil
216+
}

assets/deposit/signer.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package deposit
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
8+
"github.com/btcsuite/btcd/btcec/v2"
9+
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
10+
"github.com/lightninglabs/lndclient"
11+
"github.com/lightningnetwork/lnd/input"
12+
)
13+
14+
type MuSig2Signer struct {
15+
signer lndclient.SignerClient
16+
17+
scriptKey *btcec.PublicKey
18+
otherPubKey *btcec.PublicKey
19+
anchorRootHash []byte
20+
21+
session input.MuSig2Session
22+
}
23+
24+
func NewMuSig2Signer(signer lndclient.SignerClient, deposit *Kit, funder bool,
25+
anchorRootHash []byte) *MuSig2Signer {
26+
27+
scriptKey := deposit.FunderScriptKey
28+
otherPubKey := deposit.CoSignerInternalKey
29+
if !funder {
30+
scriptKey = deposit.CoSignerScriptKey
31+
otherPubKey = deposit.FunderInternalKey
32+
}
33+
34+
return &MuSig2Signer{
35+
signer: signer,
36+
scriptKey: scriptKey,
37+
otherPubKey: otherPubKey,
38+
anchorRootHash: anchorRootHash,
39+
}
40+
}
41+
42+
func (s *MuSig2Signer) NewSession(ctx context.Context) error {
43+
// Derive the internal key that will be used to sign the message.
44+
signingPubKey, signingPrivKey, err := DeriveSharedDepositKey(
45+
ctx, s.signer, s.scriptKey,
46+
)
47+
if err != nil {
48+
return err
49+
}
50+
51+
pubKeys := []*btcec.PublicKey{
52+
signingPubKey, s.otherPubKey,
53+
}
54+
55+
tweaks := &input.MuSig2Tweaks{
56+
TaprootTweak: s.anchorRootHash,
57+
}
58+
59+
_, session, err := input.MuSig2CreateContext(
60+
input.MuSig2Version100RC2, signingPrivKey, pubKeys, tweaks, nil,
61+
)
62+
if err != nil {
63+
return fmt.Errorf("error creating signing context: %w", err)
64+
}
65+
66+
s.session = session
67+
68+
return nil
69+
}
70+
71+
func (s *MuSig2Signer) Session() input.MuSig2Session {
72+
return s.session
73+
}
74+
75+
func (s *MuSig2Signer) PubNonce() ([musig2.PubNonceSize]byte, error) {
76+
// If we don't have a session, we can't return a public nonce.
77+
if s.session == nil {
78+
return [musig2.PubNonceSize]byte{}, fmt.Errorf("no session " +
79+
"available")
80+
}
81+
82+
// Return the public nonce of the current session.
83+
return s.session.PublicNonce(), nil
84+
}
85+
86+
// PartialSignMuSig2 is used to partially sign a message hash with the deposit's
87+
// keys.
88+
func (s *MuSig2Signer) PartialSignMuSig2(otherNonce [musig2.PubNonceSize]byte,
89+
message [32]byte) ([]byte, error) {
90+
91+
if s.session == nil {
92+
return nil, fmt.Errorf("no session available")
93+
}
94+
95+
_, err := s.session.RegisterPubNonce(otherNonce)
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
partialSig, err := input.MuSig2Sign(s.session, message, true)
101+
if err != nil {
102+
return nil, err
103+
}
104+
105+
var buf bytes.Buffer
106+
err = partialSig.Encode(&buf)
107+
if err != nil {
108+
return nil, fmt.Errorf("error encoding partial sig: %w", err)
109+
}
110+
111+
return buf.Bytes(), nil
112+
}

assets/deposit/sweeper.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ import (
1616
"github.com/btcsuite/btcwallet/wtxmgr"
1717
"github.com/lightninglabs/lndclient"
1818
"github.com/lightninglabs/loop/assets"
19+
"github.com/lightninglabs/loop/assets/htlc"
1920
"github.com/lightninglabs/loop/utils"
2021
"github.com/lightninglabs/taproot-assets/address"
2122
"github.com/lightninglabs/taproot-assets/asset"
2223
"github.com/lightninglabs/taproot-assets/proof"
24+
"github.com/lightninglabs/taproot-assets/tappsbt"
2325
"github.com/lightninglabs/taproot-assets/taprpc"
26+
"github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
2427
"github.com/lightningnetwork/lnd/input"
28+
"github.com/lightningnetwork/lnd/lntypes"
2529
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
2630
)
2731

@@ -346,3 +350,68 @@ func getSigHash(tx *wire.MsgTx, idx int,
346350

347351
return sigHash, nil
348352
}
353+
354+
// GetHTLC creates a new zero-fee HTLC packet to be able to partially sign it
355+
// and send it to the server for further processing.
356+
//
357+
// TODO(bhandras): add support for spending multiple deposits into the HTLC.
358+
func (s *Sweeper) GetHTLC(ctx context.Context, deposit *Kit,
359+
depositProof *proof.Proof, amount uint64, hash lntypes.Hash,
360+
csvExpiry uint32) (*htlc.SwapKit, *psbt.Packet, []*tappsbt.VPacket,
361+
[]*tappsbt.VPacket, *assetwalletrpc.CommitVirtualPsbtsResponse, error) {
362+
363+
// Genearate the HTLC address that will be used to sweep the deposit to
364+
// in case the client is uncooperative.
365+
rpcHtlcAddr, swapKit, err := deposit.NewHtlcAddr(
366+
ctx, s.tapdClient, amount, hash, csvExpiry,
367+
)
368+
if err != nil {
369+
return nil, nil, nil, nil, nil, fmt.Errorf("unable to create "+
370+
"htlc addr: %v", err)
371+
}
372+
373+
htlcAddr, err := address.DecodeAddress(
374+
rpcHtlcAddr.Encoded, &s.addressParams,
375+
)
376+
if err != nil {
377+
return nil, nil, nil, nil, nil, err
378+
}
379+
htlcScriptKey := asset.NewScriptKey(&htlcAddr.ScriptKey)
380+
381+
// Now we can create the sweep vpacket that'd sweep the deposited
382+
// assets to the HTLC output.
383+
depositSpendVpkt, err := assets.CreateOpTrueSweepVpkt(
384+
ctx, []*proof.Proof{depositProof}, htlcScriptKey,
385+
&htlcAddr.InternalKey, htlcAddr.TapscriptSibling,
386+
&s.addressParams,
387+
)
388+
if err != nil {
389+
return nil, nil, nil, nil, nil, fmt.Errorf("unable to create "+
390+
"deposit spend vpacket: %v", err)
391+
}
392+
393+
// By committing the virtual transaction to the BTC template we
394+
// created, our lnd node will fund the BTC level transaction with an
395+
// input to pay for the fees. We'll further add a change output to the
396+
// transaction that will be generated using the above key descriptor.
397+
feeRate := chainfee.SatPerVByte(0)
398+
399+
// Use an empty lock ID, as we don't need to lock any UTXOs for this
400+
// operation.
401+
lockID := wtxmgr.LockID{}
402+
403+
htlcBtcPkt, activeAssets, passiveAssets, commitResp, err :=
404+
s.tapdClient.PrepareAndCommitVirtualPsbts(
405+
ctx, depositSpendVpkt, feeRate, nil,
406+
s.addressParams.Params, nil, &lockID,
407+
time.Duration(0),
408+
)
409+
if err != nil {
410+
return nil, nil, nil, nil, nil, fmt.Errorf("deposit spend "+
411+
"HTLC prepare and commit failed: %v", err)
412+
}
413+
414+
htlcBtcPkt.UnsignedTx.Version = 3
415+
416+
return swapKit, htlcBtcPkt, activeAssets, passiveAssets, commitResp, nil
417+
}

looprpc/perms.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,8 @@ var RequiredPermissions = map[string][]bakery.Op{
196196
Entity: "swap",
197197
Action: "execute",
198198
}},
199+
"/looprpc.AssetDepositClient/PushAssetDepositHtlcSig": {{
200+
Entity: "swap",
201+
Action: "execute",
202+
}},
199203
}

0 commit comments

Comments
 (0)