Skip to content

Commit 2024791

Browse files
authored
Merge pull request #784 from starius/sweepbatcher-custom-musig2
sweepbatcher: add option WithCustomSignMuSig2
2 parents 99d3fd5 + c06b619 commit 2024791

File tree

3 files changed

+560
-217
lines changed

3 files changed

+560
-217
lines changed

sweepbatcher/sweep_batch.go

Lines changed: 157 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ type batchConfig struct {
137137
// the caller has to update it in the source of SweepInfo (interface
138138
// SweepFetcher) and re-add the sweep by calling AddSweep.
139139
noBumping bool
140+
141+
// customMuSig2Signer is a custom signer. If it is set, it is used to
142+
// create musig2 signatures instead of musig2SignSweep and signerClient.
143+
// Note that musig2SignSweep must be nil in this case, however signer
144+
// client must still be provided, as it is used for non-coop spendings.
145+
customMuSig2Signer SignMuSig2
140146
}
141147

142148
// rbfCache stores data related to our last fee bump.
@@ -503,9 +509,12 @@ func (b *batch) Run(ctx context.Context) error {
503509
close(b.finished)
504510
}()
505511

506-
if b.muSig2SignSweep == nil {
512+
if b.muSig2SignSweep == nil && b.cfg.customMuSig2Signer == nil {
507513
return fmt.Errorf("no musig2 signer available")
508514
}
515+
if b.muSig2SignSweep != nil && b.cfg.customMuSig2Signer != nil {
516+
return fmt.Errorf("both musig2 signers provided")
517+
}
509518

510519
blockChan, blockErrChan, err :=
511520
b.chainNotifier.RegisterBlockEpochNtfn(runCtx)
@@ -727,10 +736,11 @@ func (b *batch) publishBatch(ctx context.Context) (btcutil.Amount, error) {
727736

728737
weightEstimate.AddP2TROutput()
729738

730-
fee = b.rbfCache.FeeRate.FeeForWeight(weightEstimate.Weight())
739+
weight := weightEstimate.Weight()
740+
feeForWeight := b.rbfCache.FeeRate.FeeForWeight(weight)
731741

732742
// Clamp the calculated fee to the max allowed fee amount for the batch.
733-
fee = clampBatchFee(fee, batchAmt)
743+
fee = clampBatchFee(feeForWeight, batchAmt)
734744

735745
// Add the batch transaction output, which excludes the fees paid to
736746
// miners.
@@ -761,8 +771,9 @@ func (b *batch) publishBatch(ctx context.Context) (btcutil.Amount, error) {
761771
}
762772

763773
b.log.Infof("attempting to publish non-coop tx=%v with feerate=%v, "+
764-
"totalfee=%v, sweeps=%d, destAddr=%s", batchTx.TxHash(),
765-
b.rbfCache.FeeRate, fee, len(batchTx.TxIn), address)
774+
"weight=%v, feeForWeight=%v, fee=%v, sweeps=%d, destAddr=%s",
775+
batchTx.TxHash(), b.rbfCache.FeeRate, weight, feeForWeight, fee,
776+
len(batchTx.TxIn), address)
766777

767778
b.debugLogTx("serialized non-coop sweep", batchTx)
768779

@@ -857,10 +868,11 @@ func (b *batch) publishBatchCoop(ctx context.Context) (btcutil.Amount,
857868

858869
weightEstimate.AddP2TROutput()
859870

860-
fee = b.rbfCache.FeeRate.FeeForWeight(weightEstimate.Weight())
871+
weight := weightEstimate.Weight()
872+
feeForWeight := b.rbfCache.FeeRate.FeeForWeight(weight)
861873

862874
// Clamp the calculated fee to the max allowed fee amount for the batch.
863-
fee = clampBatchFee(fee, batchAmt)
875+
fee = clampBatchFee(feeForWeight, batchAmt)
864876

865877
// Add the batch transaction output, which excludes the fees paid to
866878
// miners.
@@ -896,19 +908,18 @@ func (b *batch) publishBatchCoop(ctx context.Context) (btcutil.Amount,
896908
return fee, err, false
897909
}
898910

899-
prevOutputFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
900-
901911
// Attempt to cooperatively sign the batch tx with the server.
902912
err = b.coopSignBatchTx(
903-
ctx, packet, sweeps, prevOutputFetcher, prevOuts, psbtBuf,
913+
ctx, packet, sweeps, prevOuts, psbtBuf.Bytes(),
904914
)
905915
if err != nil {
906916
return fee, err, false
907917
}
908918

909919
b.log.Infof("attempting to publish coop tx=%v with feerate=%v, "+
910-
"totalfee=%v, sweeps=%d, destAddr=%s", batchTx.TxHash(),
911-
b.rbfCache.FeeRate, fee, len(batchTx.TxIn), address)
920+
"weight=%v, feeForWeight=%v, fee=%v, sweeps=%d, destAddr=%s",
921+
batchTx.TxHash(), b.rbfCache.FeeRate, weight, feeForWeight, fee,
922+
len(batchTx.TxIn), address)
912923

913924
b.debugLogTx("serialized coop sweep", batchTx)
914925

@@ -942,119 +953,84 @@ func (b *batch) debugLogTx(msg string, tx *wire.MsgTx) {
942953
// coopSignBatchTx collects the necessary signatures from the server in order
943954
// to cooperatively sweep the funds.
944955
func (b *batch) coopSignBatchTx(ctx context.Context, packet *psbt.Packet,
945-
sweeps []sweep, prevOutputFetcher *txscript.MultiPrevOutFetcher,
946-
prevOuts map[wire.OutPoint]*wire.TxOut, psbtBuf bytes.Buffer) error {
956+
sweeps []sweep, prevOuts map[wire.OutPoint]*wire.TxOut,
957+
psbt []byte) error {
947958

948959
for i, sweep := range sweeps {
949960
sweep := sweep
950961

951-
sigHashes := txscript.NewTxSigHashes(
952-
packet.UnsignedTx, prevOutputFetcher,
953-
)
954-
955-
sigHash, err := txscript.CalcTaprootSignatureHash(
956-
sigHashes, txscript.SigHashDefault, packet.UnsignedTx,
957-
i, prevOutputFetcher,
962+
finalSig, err := b.musig2sign(
963+
ctx, i, sweep, packet.UnsignedTx, prevOuts, psbt,
958964
)
959965
if err != nil {
960966
return err
961967
}
962968

963-
var (
964-
signers [][]byte
965-
muSig2Version input.MuSig2Version
966-
)
967-
968-
// Depending on the MuSig2 version we either pass 32 byte
969-
// Schnorr public keys or normal 33 byte public keys.
970-
if sweep.protocolVersion >= loopdb.ProtocolVersionMuSig2 {
971-
muSig2Version = input.MuSig2Version100RC2
972-
signers = [][]byte{
973-
sweep.htlcKeys.SenderInternalPubKey[:],
974-
sweep.htlcKeys.ReceiverInternalPubKey[:],
975-
}
976-
} else {
977-
muSig2Version = input.MuSig2Version040
978-
signers = [][]byte{
979-
sweep.htlcKeys.SenderInternalPubKey[1:],
980-
sweep.htlcKeys.ReceiverInternalPubKey[1:],
981-
}
969+
packet.UnsignedTx.TxIn[i].Witness = wire.TxWitness{
970+
finalSig,
982971
}
972+
}
983973

984-
htlcScript, ok := sweep.htlc.HtlcScript.(*swap.HtlcScriptV3)
985-
if !ok {
986-
return fmt.Errorf("invalid htlc script version")
987-
}
974+
return nil
975+
}
988976

989-
// Now we're creating a local MuSig2 session using the receiver
990-
// key's key locator and the htlc's root hash.
991-
musig2SessionInfo, err := b.signerClient.MuSig2CreateSession(
992-
ctx, muSig2Version,
993-
&sweep.htlcKeys.ClientScriptKeyLocator, signers,
994-
lndclient.MuSig2TaprootTweakOpt(
995-
htlcScript.RootHash[:], false,
996-
),
997-
)
998-
if err != nil {
999-
return err
1000-
}
977+
// musig2sign signs one sweep using musig2.
978+
func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep,
979+
unsignedTx *wire.MsgTx, prevOuts map[wire.OutPoint]*wire.TxOut,
980+
psbt []byte) ([]byte, error) {
1001981

1002-
// With the session active, we can now send the server our
1003-
// public nonce and the sig hash, so that it can create it's own
1004-
// MuSig2 session and return the server side nonce and partial
1005-
// signature.
1006-
serverNonce, serverSig, err := b.muSig2SignSweep(
1007-
ctx, sweep.protocolVersion, sweep.swapHash,
1008-
sweep.swapInvoicePaymentAddr,
1009-
musig2SessionInfo.PublicNonce[:], psbtBuf.Bytes(),
1010-
prevOuts,
1011-
)
1012-
if err != nil {
1013-
return err
1014-
}
982+
prevOutputFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
1015983

1016-
var serverPublicNonce [musig2.PubNonceSize]byte
1017-
copy(serverPublicNonce[:], serverNonce)
984+
sigHashes := txscript.NewTxSigHashes(unsignedTx, prevOutputFetcher)
1018985

1019-
// Register the server's nonce before attempting to create our
1020-
// partial signature.
1021-
haveAllNonces, err := b.signerClient.MuSig2RegisterNonces(
1022-
ctx, musig2SessionInfo.SessionID,
1023-
[][musig2.PubNonceSize]byte{serverPublicNonce},
1024-
)
1025-
if err != nil {
1026-
return err
1027-
}
986+
sigHash, err := txscript.CalcTaprootSignatureHash(
987+
sigHashes, txscript.SigHashDefault, unsignedTx, inputIndex,
988+
prevOutputFetcher,
989+
)
990+
if err != nil {
991+
return nil, err
992+
}
993+
994+
var (
995+
signers [][]byte
996+
muSig2Version input.MuSig2Version
997+
)
1028998

1029-
// Sanity check that we have all the nonces.
1030-
if !haveAllNonces {
1031-
return fmt.Errorf("invalid MuSig2 session: " +
1032-
"nonces missing")
999+
// Depending on the MuSig2 version we either pass 32 byte
1000+
// Schnorr public keys or normal 33 byte public keys.
1001+
if sweep.protocolVersion >= loopdb.ProtocolVersionMuSig2 {
1002+
muSig2Version = input.MuSig2Version100RC2
1003+
signers = [][]byte{
1004+
sweep.htlcKeys.SenderInternalPubKey[:],
1005+
sweep.htlcKeys.ReceiverInternalPubKey[:],
1006+
}
1007+
} else {
1008+
muSig2Version = input.MuSig2Version040
1009+
signers = [][]byte{
1010+
sweep.htlcKeys.SenderInternalPubKey[1:],
1011+
sweep.htlcKeys.ReceiverInternalPubKey[1:],
10331012
}
1013+
}
10341014

1035-
var digest [32]byte
1036-
copy(digest[:], sigHash)
1015+
htlcScript, ok := sweep.htlc.HtlcScript.(*swap.HtlcScriptV3)
1016+
if !ok {
1017+
return nil, fmt.Errorf("invalid htlc script version")
1018+
}
10371019

1038-
// Since our MuSig2 session has all nonces, we can now create
1039-
// the local partial signature by signing the sig hash.
1040-
_, err = b.signerClient.MuSig2Sign(
1041-
ctx, musig2SessionInfo.SessionID, digest, false,
1042-
)
1043-
if err != nil {
1044-
return err
1045-
}
1020+
var digest [32]byte
1021+
copy(digest[:], sigHash)
10461022

1047-
// Now combine the partial signatures to use the final combined
1048-
// signature in the sweep transaction's witness.
1049-
haveAllSigs, finalSig, err := b.signerClient.MuSig2CombineSig(
1050-
ctx, musig2SessionInfo.SessionID, [][]byte{serverSig},
1023+
// If a custom signer is installed, use it instead of b.signerClient
1024+
// and b.muSig2SignSweep.
1025+
if b.cfg.customMuSig2Signer != nil {
1026+
// Produce a signature.
1027+
finalSig, err := b.cfg.customMuSig2Signer(
1028+
ctx, muSig2Version, sweep.swapHash,
1029+
htlcScript.RootHash, digest,
10511030
)
10521031
if err != nil {
1053-
return err
1054-
}
1055-
1056-
if !haveAllSigs {
1057-
return fmt.Errorf("failed to combine signatures")
1032+
return nil, fmt.Errorf("customMuSig2Signer failed: %w",
1033+
err)
10581034
}
10591035

10601036
// To be sure that we're good, parse and validate that the
@@ -1064,15 +1040,88 @@ func (b *batch) coopSignBatchTx(ctx context.Context, packet *psbt.Packet,
10641040
htlcScript.TaprootKey, sigHash, finalSig,
10651041
)
10661042
if err != nil {
1067-
return err
1043+
return nil, fmt.Errorf("verifySchnorrSig failed: %w",
1044+
err)
10681045
}
10691046

1070-
packet.UnsignedTx.TxIn[i].Witness = wire.TxWitness{
1071-
finalSig,
1072-
}
1047+
return finalSig, nil
10731048
}
10741049

1075-
return nil
1050+
// Now we're creating a local MuSig2 session using the receiver key's
1051+
// key locator and the htlc's root hash.
1052+
keyLocator := &sweep.htlcKeys.ClientScriptKeyLocator
1053+
musig2SessionInfo, err := b.signerClient.MuSig2CreateSession(
1054+
ctx, muSig2Version, keyLocator, signers,
1055+
lndclient.MuSig2TaprootTweakOpt(htlcScript.RootHash[:], false),
1056+
)
1057+
if err != nil {
1058+
return nil, fmt.Errorf("signerClient.MuSig2CreateSession "+
1059+
"failed: %w", err)
1060+
}
1061+
1062+
// With the session active, we can now send the server our
1063+
// public nonce and the sig hash, so that it can create it's own
1064+
// MuSig2 session and return the server side nonce and partial
1065+
// signature.
1066+
serverNonce, serverSig, err := b.muSig2SignSweep(
1067+
ctx, sweep.protocolVersion, sweep.swapHash,
1068+
sweep.swapInvoicePaymentAddr,
1069+
musig2SessionInfo.PublicNonce[:], psbt, prevOuts,
1070+
)
1071+
if err != nil {
1072+
return nil, err
1073+
}
1074+
1075+
var serverPublicNonce [musig2.PubNonceSize]byte
1076+
copy(serverPublicNonce[:], serverNonce)
1077+
1078+
// Register the server's nonce before attempting to create our
1079+
// partial signature.
1080+
haveAllNonces, err := b.signerClient.MuSig2RegisterNonces(
1081+
ctx, musig2SessionInfo.SessionID,
1082+
[][musig2.PubNonceSize]byte{serverPublicNonce},
1083+
)
1084+
if err != nil {
1085+
return nil, err
1086+
}
1087+
1088+
// Sanity check that we have all the nonces.
1089+
if !haveAllNonces {
1090+
return nil, fmt.Errorf("invalid MuSig2 session: " +
1091+
"nonces missing")
1092+
}
1093+
1094+
// Since our MuSig2 session has all nonces, we can now create
1095+
// the local partial signature by signing the sig hash.
1096+
_, err = b.signerClient.MuSig2Sign(
1097+
ctx, musig2SessionInfo.SessionID, digest, false,
1098+
)
1099+
if err != nil {
1100+
return nil, err
1101+
}
1102+
1103+
// Now combine the partial signatures to use the final combined
1104+
// signature in the sweep transaction's witness.
1105+
haveAllSigs, finalSig, err := b.signerClient.MuSig2CombineSig(
1106+
ctx, musig2SessionInfo.SessionID, [][]byte{serverSig},
1107+
)
1108+
if err != nil {
1109+
return nil, err
1110+
}
1111+
1112+
if !haveAllSigs {
1113+
return nil, fmt.Errorf("failed to combine signatures")
1114+
}
1115+
1116+
// To be sure that we're good, parse and validate that the
1117+
// combined signature is indeed valid for the sig hash and the
1118+
// internal pubkey.
1119+
err = b.verifySchnorrSig(htlcScript.TaprootKey, sigHash, finalSig)
1120+
if err != nil {
1121+
return nil, err
1122+
}
1123+
1124+
return finalSig, nil
10761125
}
10771126

10781127
// updateRbfRate updates the fee rate we should use for the new batch

0 commit comments

Comments
 (0)