Skip to content

Commit 39e7af6

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 3076f0a commit 39e7af6

File tree

4 files changed

+254
-1
lines changed

4 files changed

+254
-1
lines changed

assets/deposit/manager.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
package deposit
22

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

910
"github.com/btcsuite/btcd/btcec/v2"
11+
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
1012
"github.com/btcsuite/btcd/chaincfg"
1113
"github.com/btcsuite/btcd/wire"
14+
"github.com/btcsuite/btcwallet/wallet"
1215
"github.com/lightninglabs/lndclient"
1316
"github.com/lightninglabs/loop/assets"
1417
"github.com/lightninglabs/loop/swapserverrpc"
1518
"github.com/lightninglabs/taproot-assets/address"
1619
"github.com/lightninglabs/taproot-assets/asset"
1720
"github.com/lightninglabs/taproot-assets/taprpc"
21+
"github.com/lightningnetwork/lnd/lntypes"
1822
)
1923

2024
var (
@@ -1169,3 +1173,68 @@ func (m *Manager) RevealDepositKeys(ctx context.Context,
11691173

11701174
return err
11711175
}
1176+
1177+
// CoSignHTLC will partially a deposit spending zero-fee HTLC and send the
1178+
// resulting signature to the swap server.
1179+
func (m *Manager) CoSignHTLC(ctx context.Context, depositID string,
1180+
serverNonce [musig2.PubNonceSize]byte, hash lntypes.Hash,
1181+
csvExpiry uint32) error {
1182+
1183+
done, err := m.scheduleNextCall()
1184+
if err != nil {
1185+
return err
1186+
}
1187+
defer done()
1188+
1189+
deposit, ok := m.deposits[depositID]
1190+
if !ok {
1191+
return fmt.Errorf("deposit %v not available", depositID)
1192+
}
1193+
1194+
_, htlcPkt, _, _, _, err := m.sweeper.GetHTLC(
1195+
ctx, deposit.Kit, deposit.Proof, deposit.Amount, hash,
1196+
csvExpiry,
1197+
)
1198+
if err != nil {
1199+
log.Errorf("Unable to get HTLC packet: %v", err)
1200+
1201+
return err
1202+
}
1203+
1204+
prevOutFetcher := wallet.PsbtPrevOutputFetcher(htlcPkt)
1205+
sigHash, err := getSigHash(htlcPkt.UnsignedTx, 0, prevOutFetcher)
1206+
if err != nil {
1207+
return err
1208+
}
1209+
1210+
nonce, partialSig, err := m.sweeper.PartialSignMuSig2(
1211+
ctx, deposit.Kit, deposit.AnchorRootHash, serverNonce, sigHash,
1212+
)
1213+
if err != nil {
1214+
log.Errorf("Unable to partial sign HTLC: %v", err)
1215+
1216+
return err
1217+
}
1218+
1219+
var pktBuf bytes.Buffer
1220+
err = htlcPkt.Serialize(&pktBuf)
1221+
if err != nil {
1222+
log.Errorf("Unable to write HTLC packet: %v", err)
1223+
return err
1224+
}
1225+
1226+
_, err = m.depositServiceClient.PushAssetDepositHtlcSigs(
1227+
ctx, &swapserverrpc.PushAssetDepositHtlcSigsReq{
1228+
PartialSigs: []*swapserverrpc.AssetDepositPartialSig{
1229+
{
1230+
DepositId: depositID,
1231+
Nonce: nonce[:],
1232+
PartialSig: partialSig,
1233+
},
1234+
},
1235+
HtlcPsbt: pktBuf.Bytes(),
1236+
},
1237+
)
1238+
1239+
return err
1240+
}

assets/deposit/server.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/lightninglabs/loop/looprpc"
99
"github.com/lightninglabs/taproot-assets/asset"
10+
"github.com/lightningnetwork/lnd/lntypes"
1011
"google.golang.org/grpc/codes"
1112
"google.golang.org/grpc/status"
1213
)
@@ -172,5 +173,18 @@ func (s *Server) TestCoSignAssetDepositHTLC(ctx context.Context,
172173
in *looprpc.TestCoSignAssetDepositHTLCRequest) (
173174
*looprpc.TestCoSignAssetDepositHTLCResponse, error) {
174175

175-
return nil, status.Error(codes.Unimplemented, "unimplemented")
176+
// Values for testing.
177+
serverNonce := secNonceToPubNonce(tmpServerSecNonce)
178+
preimage := lntypes.Preimage{1, 2, 3}
179+
swapHash := preimage.Hash()
180+
htlcExpiry := uint32(100)
181+
182+
err := s.manager.CoSignHTLC(
183+
ctx, in.DepositId, serverNonce, swapHash, htlcExpiry,
184+
)
185+
if err != nil {
186+
return nil, status.Error(codes.Internal, err.Error())
187+
}
188+
189+
return &looprpc.TestCoSignAssetDepositHTLCResponse{}, nil
176190
}

assets/deposit/sweeper.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,24 @@ import (
99

1010
"github.com/btcsuite/btcd/btcec/v2"
1111
"github.com/btcsuite/btcd/btcec/v2/schnorr"
12+
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
1213
"github.com/btcsuite/btcd/btcutil/psbt"
1314
"github.com/btcsuite/btcd/txscript"
1415
"github.com/btcsuite/btcd/wire"
1516
"github.com/btcsuite/btcwallet/wallet"
1617
"github.com/btcsuite/btcwallet/wtxmgr"
1718
"github.com/lightninglabs/lndclient"
1819
"github.com/lightninglabs/loop/assets"
20+
"github.com/lightninglabs/loop/assets/htlc"
1921
"github.com/lightninglabs/loop/utils"
2022
"github.com/lightninglabs/taproot-assets/address"
2123
"github.com/lightninglabs/taproot-assets/proof"
24+
"github.com/lightninglabs/taproot-assets/tappsbt"
2225
"github.com/lightninglabs/taproot-assets/taprpc"
26+
"github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
2327
"github.com/lightningnetwork/lnd/input"
28+
"github.com/lightningnetwork/lnd/keychain"
29+
"github.com/lightningnetwork/lnd/lntypes"
2430
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
2531
)
2632

@@ -333,3 +339,110 @@ func getSigHash(tx *wire.MsgTx, idx int,
333339

334340
return sigHash, nil
335341
}
342+
343+
// GetHTLC creates a new zero-fee HTLC packet to be able to partially sign it
344+
// and send it to the server for further processing.
345+
//
346+
// TODO(bhandras): add support for spending multiple deposits into the HTLC.
347+
func (s *Sweeper) GetHTLC(ctx context.Context, deposit *Kit,
348+
depositProof *proof.Proof, amount uint64, hash lntypes.Hash,
349+
csvExpiry uint32) (*htlc.SwapKit, *psbt.Packet, []*tappsbt.VPacket,
350+
[]*tappsbt.VPacket, *assetwalletrpc.CommitVirtualPsbtsResponse, error) {
351+
352+
// Genearate the HTLC address that will be used to sweep the deposit to
353+
// in case the client is uncooperative.
354+
rpcHtlcAddr, swapKit, err := deposit.NewHtlcAddr(
355+
ctx, s.tapdClient, amount, hash, csvExpiry,
356+
)
357+
if err != nil {
358+
return nil, nil, nil, nil, nil, fmt.Errorf("unable to create "+
359+
"htlc addr: %v", err)
360+
}
361+
362+
htlcAddr, err := address.DecodeAddress(
363+
rpcHtlcAddr.Encoded, &s.addressParams,
364+
)
365+
if err != nil {
366+
return nil, nil, nil, nil, nil, err
367+
}
368+
369+
// Now we can create the sweep vpacket that'd sweep the deposited
370+
// assets to the HTLC output.
371+
depositSpendVpkt, err := assets.CreateOpTrueSweepVpkt(
372+
ctx, []*proof.Proof{depositProof}, htlcAddr, &s.addressParams,
373+
)
374+
if err != nil {
375+
return nil, nil, nil, nil, nil, fmt.Errorf("unable to create "+
376+
"deposit spend vpacket: %v", err)
377+
}
378+
379+
// By committing the virtual transaction to the BTC template we
380+
// created, our lnd node will fund the BTC level transaction with an
381+
// input to pay for the fees. We'll further add a change output to the
382+
// transaction that will be generated using the above key descriptor.
383+
feeRate := chainfee.SatPerVByte(0)
384+
385+
// Use an empty lock ID, as we don't need to lock any UTXOs for this
386+
// operation.
387+
lockID := wtxmgr.LockID{}
388+
389+
htlcBtcPkt, activeAssets, passiveAssets, commitResp, err :=
390+
s.tapdClient.PrepareAndCommitVirtualPsbts(
391+
ctx, depositSpendVpkt, feeRate, nil,
392+
s.addressParams.Params, nil, &lockID,
393+
time.Duration(0),
394+
)
395+
if err != nil {
396+
return nil, nil, nil, nil, nil, fmt.Errorf("deposit spend "+
397+
"HTLC prepare and commit failed: %v", err)
398+
}
399+
400+
htlcBtcPkt.UnsignedTx.Version = 3
401+
402+
return swapKit, htlcBtcPkt, activeAssets, passiveAssets, commitResp, nil
403+
}
404+
405+
// PartialSignMuSig2 is used to partially sign a message hash with the deposit's
406+
// keys.
407+
func (s *Sweeper) PartialSignMuSig2(ctx context.Context, d *Kit,
408+
anchorRootHash []byte, cosignerNonce [musig2.PubNonceSize]byte,
409+
message [32]byte) ([musig2.PubNonceSize]byte, []byte, error) {
410+
411+
signers := [][]byte{
412+
d.FunderInternalKey.SerializeCompressed(),
413+
d.CoSignerInternalKey.SerializeCompressed(),
414+
}
415+
416+
session, err := s.signer.MuSig2CreateSession(
417+
ctx, input.MuSig2Version100RC2,
418+
&keychain.KeyLocator{
419+
Family: d.KeyLocator.Family,
420+
Index: d.KeyLocator.Index,
421+
}, signers, lndclient.MuSig2TaprootTweakOpt(
422+
anchorRootHash, false,
423+
),
424+
)
425+
if err != nil {
426+
return [musig2.PubNonceSize]byte{}, nil, err
427+
}
428+
429+
_, err = s.signer.MuSig2RegisterNonces(
430+
ctx, session.SessionID,
431+
[][musig2.PubNonceSize]byte{cosignerNonce},
432+
)
433+
if err != nil {
434+
return [musig2.PubNonceSize]byte{}, nil, err
435+
}
436+
437+
clientPartialSig, err := s.signer.MuSig2Sign(
438+
ctx, session.SessionID, message, true,
439+
)
440+
if err != nil {
441+
return [musig2.PubNonceSize]byte{}, nil, err
442+
}
443+
444+
fmt.Printf("!!! client partial sig: %x\n", clientPartialSig)
445+
fmt.Printf("!!! client nonce: %x\n", session.PublicNonce)
446+
447+
return session.PublicNonce, clientPartialSig, nil
448+
}

assets/deposit/testing.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package deposit
2+
3+
import (
4+
"github.com/btcsuite/btcd/btcec/v2"
5+
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
6+
)
7+
8+
var (
9+
// tmpServerSecNonce is a secret nonce used for testing.
10+
// TODO(bhandras): remove once package spending works e2e.
11+
tmpServerSecNonce = [musig2.SecNonceSize]byte{
12+
// First 32 bytes: scalar k1
13+
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
14+
0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00,
15+
0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80,
16+
0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0, 0x01,
17+
18+
// Second 32 bytes: scalar k2
19+
0x02, 0x13, 0x24, 0x35, 0x46, 0x57, 0x68, 0x79,
20+
0x8a, 0x9b, 0xac, 0xbd, 0xce, 0xdf, 0xf1, 0x02,
21+
0x14, 0x26, 0x38, 0x4a, 0x5c, 0x6e, 0x70, 0x82,
22+
0x94, 0xa6, 0xb8, 0xca, 0xdc, 0xee, 0xf1, 0x03,
23+
}
24+
)
25+
26+
// secNonceToPubNonce takes our two secret nonces, and produces their two
27+
// corresponding EC points, serialized in compressed format.
28+
func secNonceToPubNonce(
29+
secNonce [musig2.SecNonceSize]byte) [musig2.PubNonceSize]byte {
30+
31+
var k1Mod, k2Mod btcec.ModNScalar
32+
k1Mod.SetByteSlice(secNonce[:btcec.PrivKeyBytesLen])
33+
k2Mod.SetByteSlice(secNonce[btcec.PrivKeyBytesLen:])
34+
35+
var r1, r2 btcec.JacobianPoint
36+
btcec.ScalarBaseMultNonConst(&k1Mod, &r1)
37+
btcec.ScalarBaseMultNonConst(&k2Mod, &r2)
38+
39+
// Next, we'll convert the key in jacobian format to a normal public
40+
// key expressed in affine coordinates.
41+
r1.ToAffine()
42+
r2.ToAffine()
43+
r1Pub := btcec.NewPublicKey(&r1.X, &r1.Y)
44+
r2Pub := btcec.NewPublicKey(&r2.X, &r2.Y)
45+
46+
var pubNonce [musig2.PubNonceSize]byte
47+
48+
// The public nonces are serialized as: R1 || R2, where both keys are
49+
// serialized in compressed format.
50+
copy(pubNonce[:], r1Pub.SerializeCompressed())
51+
copy(
52+
pubNonce[btcec.PubKeyBytesLenCompressed:],
53+
r2Pub.SerializeCompressed(),
54+
)
55+
56+
return pubNonce
57+
}

0 commit comments

Comments
 (0)