Skip to content

Commit f08d8b2

Browse files
committed
Add aggregation of MuSig2 signatures including adaptor signatures
1 parent bb324de commit f08d8b2

17 files changed

+437
-82
lines changed

coinlib/bin/build_wasm.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ RUN ${WASI_SDK_PATH}/bin/wasm-ld \
9090
--export secp256k1_musig_partial_sig_parse \
9191
--export secp256k1_musig_partial_sig_serialize \
9292
--export secp256k1_musig_partial_sig_verify \
93+
--export secp256k1_musig_partial_sig_agg \
94+
--export secp256k1_musig_nonce_parity \
95+
--export secp256k1_musig_adapt \
96+
--export secp256k1_musig_extract_adaptor \
9397
# The secp256k1 library file
9498
build/lib/libsecp256k1.a \
9599
# Need to include libc for wasi here as it isn't done for us

coinlib/lib/src/coinlib_base.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export 'package:coinlib/src/crypto/hd_key.dart';
1414
export 'package:coinlib/src/crypto/message_signature.dart';
1515
export 'package:coinlib/src/crypto/nums_public_key.dart';
1616
export 'package:coinlib/src/crypto/random.dart';
17+
export 'package:coinlib/src/crypto/schnorr_adaptor_signature.dart';
1718
export 'package:coinlib/src/crypto/schnorr_signature.dart';
1819

1920
export 'package:coinlib/src/encode/base58.dart';

coinlib/lib/src/crypto/ecdsa_signature.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'dart:typed_data';
22
import 'package:coinlib/src/secp256k1/secp256k1.dart';
3-
import 'package:coinlib/src/secp256k1/secp256k1_base.dart';
43
import 'package:coinlib/src/common/bytes.dart';
54
import 'package:coinlib/src/common/hex.dart';
65
import 'ec_private_key.dart';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'ec_private_key.dart';
2+
import 'schnorr_signature.dart';
3+
import 'package:coinlib/src/secp256k1/secp256k1.dart';
4+
5+
/// A Schnorr signature where the nonce has been adapted by a point. The
6+
/// signature can be adapted (decrypted) using [adapt] with the discrete-log of
7+
/// the point (private key to a public key). The signature will be complete and
8+
/// valid if the correct adaptor was given.
9+
class SchnorrAdaptorSignature {
10+
11+
/// The signature that contains the adapted nonce but requires the adaptor
12+
/// scalar
13+
final SchnorrSignature preSig;
14+
/// True when the nonce y-coord is odd
15+
final bool parity;
16+
17+
SchnorrAdaptorSignature(this.preSig, this.parity);
18+
19+
/// Adapts the adaptor signature with the discrete log to the adaptor point
20+
/// given as an [ECPrivateKey]. The resulting signature is not verified.
21+
/// Either the [adaptorScalar] should be known to be correct or the signature
22+
/// can be verified afterwards.
23+
///
24+
/// If the signature has malformed data, this may throw
25+
/// [InvalidSchnorrSignature].
26+
SchnorrSignature adapt(ECPrivateKey adaptorScalar) {
27+
try {
28+
return SchnorrSignature(
29+
secp256k1.adaptSchnorr(preSig.data, adaptorScalar.data, parity),
30+
);
31+
} on Secp256k1Exception {
32+
throw InvalidSchnorrSignature();
33+
}
34+
}
35+
36+
/// Extracts the adaptor scalar using the [completeSig]. The scalar is
37+
/// provided as an [ECPrivateKey].
38+
///
39+
/// The [completeSig] must be a different signature or else
40+
/// [InvalidPrivateKey] will be thrown. It should also be verified as the
41+
/// correct valid signature to obtain the correct adaptor scalar.
42+
///
43+
/// If either signature has malformed data, this may also throw
44+
/// [InvalidSchnorrSignature].
45+
ECPrivateKey extract(SchnorrSignature completeSig) {
46+
try {
47+
return ECPrivateKey(
48+
secp256k1.extractSchnorrAdaptor(preSig.data, completeSig.data, parity),
49+
);
50+
} on Secp256k1Exception {
51+
throw InvalidSchnorrSignature();
52+
}
53+
}
54+
55+
}

coinlib/lib/src/musig/library.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ import 'dart:typed_data';
44
import 'package:coinlib/src/common/bytes.dart';
55
import 'package:coinlib/src/crypto/ec_private_key.dart';
66
import 'package:coinlib/src/crypto/ec_public_key.dart';
7+
import 'package:coinlib/src/crypto/schnorr_adaptor_signature.dart';
8+
import 'package:coinlib/src/crypto/schnorr_signature.dart';
79
import 'package:coinlib/src/secp256k1/secp256k1.dart';
8-
import 'package:coinlib/src/secp256k1/secp256k1_base.dart';
910

1011
part "keys.dart";
12+
part "partial_sig.dart";
13+
part "public_nonce.dart";
14+
part "result.dart";
1115
part "signing.dart";
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
part of "library.dart";
2+
3+
class InvalidMuSigPartialSig implements Exception {}
4+
5+
/// The partial signature from a participant that needs to be shared.
6+
class MuSigPartialSig {
7+
8+
final OpaqueMuSigPartialSig _underlying;
9+
final Uint8List bytes;
10+
11+
MuSigPartialSig._(this._underlying, this.bytes);
12+
MuSigPartialSig._fromUnderlying(this._underlying)
13+
: bytes = secp256k1.muSigSerialisePartialSig(_underlying);
14+
15+
/// Creates the partial signature from the [bytes]. If the [bytes] are
16+
/// invalid, [InvalidMuSigPartialSig] will be thrown.
17+
factory MuSigPartialSig.fromBytes(Uint8List bytes) {
18+
try {
19+
return MuSigPartialSig._(secp256k1.muSigParsePartialSig(bytes), bytes);
20+
} on Secp256k1Exception {
21+
throw InvalidMuSigPartialSig();
22+
}
23+
}
24+
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
part of "library.dart";
2+
3+
class InvalidMuSigPublicNonce implements Exception {}
4+
5+
/// The public nonce of a participant for a single signing session only.
6+
class MuSigPublicNonce {
7+
8+
final OpaqueMuSigPublicNonce _underlying;
9+
/// The serialised bytes that can be shared with other signers
10+
final Uint8List bytes;
11+
12+
MuSigPublicNonce._(this._underlying, this.bytes);
13+
MuSigPublicNonce._fromUnderlying(this._underlying)
14+
: bytes = secp256k1.muSigSerialisePublicNonce(_underlying);
15+
16+
/// Creates the public nonce from the [bytes]. If the [bytes] are invalid,
17+
/// [InvalidMuSigPublicNonce] will be thrown.
18+
factory MuSigPublicNonce.fromBytes(Uint8List bytes) {
19+
try {
20+
return MuSigPublicNonce._(secp256k1.muSigParsePublicNonce(bytes), bytes);
21+
} on Secp256k1Exception {
22+
throw InvalidMuSigPublicNonce();
23+
}
24+
}
25+
26+
}

coinlib/lib/src/musig/result.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
part of "library.dart";
2+
3+
sealed class MuSigResult {}
4+
5+
/// A MuSig result with a completed [signature].
6+
class MuSigResultComplete extends MuSigResult {
7+
final SchnorrSignature signature;
8+
MuSigResultComplete._(this.signature);
9+
}
10+
11+
/// A MuSig result with an [adaptorSignature] that requires decrypting with the
12+
/// adaptor discrete-log scalar.
13+
class MuSigResultAdaptor extends MuSigResult {
14+
final SchnorrAdaptorSignature adaptorSignature;
15+
MuSigResultAdaptor._(this.adaptorSignature);
16+
}

coinlib/lib/src/musig/signing.dart

Lines changed: 40 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,6 @@ part of "library.dart";
33
/// Maps the public key of a participant to their public nonce
44
typedef KeyToNonceMap = Map<ECPublicKey, MuSigPublicNonce>;
55

6-
class InvalidMuSigPublicNonce implements Exception {}
7-
8-
/// The public nonce of a participant for a single signing session only.
9-
class MuSigPublicNonce {
10-
11-
final OpaqueMuSigPublicNonce _underlying;
12-
/// The serialised bytes that can be shared with other signers
13-
final Uint8List bytes;
14-
15-
MuSigPublicNonce._(this._underlying, this.bytes);
16-
MuSigPublicNonce._fromUnderlying(this._underlying)
17-
: bytes = secp256k1.muSigSerialisePublicNonce(_underlying);
18-
19-
/// Creates the public nonce from the [bytes]. If the [bytes] are invalid,
20-
/// [InvalidMuSigPublicNonce] will be thrown.
21-
factory MuSigPublicNonce.fromBytes(Uint8List bytes) {
22-
try {
23-
return MuSigPublicNonce._(secp256k1.muSigParsePublicNonce(bytes), bytes);
24-
} on Secp256k1Exception {
25-
throw InvalidMuSigPublicNonce();
26-
}
27-
}
28-
29-
}
30-
31-
class InvalidMuSigPartialSig implements Exception {}
32-
33-
/// The partial signature from a participant that needs to be shared.
34-
class MuSigPartialSig {
35-
36-
final OpaqueMuSigPartialSig _underlying;
37-
final Uint8List bytes;
38-
39-
MuSigPartialSig._(this._underlying, this.bytes);
40-
MuSigPartialSig._fromUnderlying(this._underlying)
41-
: bytes = secp256k1.muSigSerialisePartialSig(_underlying);
42-
43-
/// Creates the partial signature from the [bytes]. If the [bytes] are
44-
/// invalid, [InvalidMuSigPartialSig] will be thrown.
45-
factory MuSigPartialSig.fromBytes(Uint8List bytes) {
46-
try {
47-
return MuSigPartialSig._(secp256k1.muSigParsePartialSig(bytes), bytes);
48-
} on Secp256k1Exception {
49-
throw InvalidMuSigPartialSig();
50-
}
51-
}
52-
53-
}
54-
556
/// A MuSig signing session state to be used for one signing session only.
567
///
578
/// This class is stateful unlike most of the classes in the library. This is to
@@ -69,8 +20,10 @@ class MuSigStatefulSigningSession {
6920
late final OpaqueMuSigSecretNonce _ourSecretNonce;
7021

7122
OpaqueMuSigSession? _underlyingSession;
23+
bool _isAdaptor = false;
7224
KeyToNonceMap? _otherNonces;
73-
Map<ECPublicKey, MuSigPartialSig> _partialSigs = {};
25+
OpaqueMuSigPartialSig? _ourPartialSig;
26+
final Map<ECPublicKey, OpaqueMuSigPartialSig> _partialSigs = {};
7427

7528
/// Starts a signing session with the MuSig [keys] and specifying the public
7629
/// key for the signer with [ourPublicKey].
@@ -156,13 +109,13 @@ class MuSigStatefulSigningSession {
156109
adaptor?.data,
157110
);
158111
_otherNonces = otherNonces;
112+
_isAdaptor = adaptor != null;
159113

160114
// Produce partial signature
161-
return MuSigPartialSig._fromUnderlying(
162-
secp256k1.muSigPartialSign(
163-
_ourSecretNonce, privKey.data, keys._aggCache, _underlyingSession!,
164-
),
115+
_ourPartialSig = secp256k1.muSigPartialSign(
116+
_ourSecretNonce, privKey.data, keys._aggCache, _underlyingSession!,
165117
);
118+
return MuSigPartialSig._fromUnderlying(_ourPartialSig!);
166119

167120
}
168121

@@ -209,7 +162,7 @@ class MuSigStatefulSigningSession {
209162
);
210163

211164
if (valid) {
212-
_partialSigs[participantKey] = partialSig;
165+
_partialSigs[participantKey] = partialSig._underlying;
213166
}
214167

215168
return valid;
@@ -221,4 +174,36 @@ class MuSigStatefulSigningSession {
221174
bool havePartialSignature(ECPublicKey participantKey) =>
222175
_partialSigs.containsKey(participantKey);
223176

177+
MuSigResult finish() {
178+
179+
if (_underlyingSession == null) {
180+
throw StateError("Need to call sign before finishing");
181+
}
182+
183+
final actualSigs = _partialSigs.length;
184+
final reqSigs = keys.pubKeys.length - 1;
185+
if (actualSigs != reqSigs) {
186+
throw StateError(
187+
"Need $reqSigs partial signatures. Only have $actualSigs",
188+
);
189+
}
190+
191+
final sig = SchnorrSignature(
192+
secp256k1.muSigSignatureAggregate(
193+
{ _ourPartialSig!, ..._partialSigs.values },
194+
_underlyingSession!,
195+
),
196+
);
197+
198+
return _isAdaptor
199+
? MuSigResultAdaptor._(
200+
SchnorrAdaptorSignature(
201+
sig,
202+
secp256k1.muSigNonceParity(_underlyingSession!),
203+
),
204+
)
205+
: MuSigResultComplete._(sig);
206+
207+
}
208+
224209
}

coinlib/lib/src/secp256k1/secp256k1.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ import 'secp256k1_web.dart' if (dart.library.io) 'secp256k1_io.dart';
22
export 'secp256k1_web.dart' if (dart.library.io) 'secp256k1_io.dart'
33
show OpaqueMuSigCache, OpaqueMuSigSecretNonce, OpaqueMuSigPublicNonce,
44
OpaqueMuSigSession, OpaqueMuSigPartialSig;
5+
export 'secp256k1_base.dart' show Secp256k1Exception;
56

67
final secp256k1 = Secp256k1();

0 commit comments

Comments
 (0)