diff --git a/coinlib/lib/src/coinlib_base.dart b/coinlib/lib/src/coinlib_base.dart index 5361a44..bc5c12e 100644 --- a/coinlib/lib/src/coinlib_base.dart +++ b/coinlib/lib/src/coinlib_base.dart @@ -4,6 +4,7 @@ export 'package:coinlib/src/common/bytes.dart'; export 'package:coinlib/src/common/hex.dart'; export 'package:coinlib/src/common/serial.dart'; +export 'package:coinlib/src/crypto/ec_compressed_public_key.dart'; export 'package:coinlib/src/crypto/ec_private_key.dart'; export 'package:coinlib/src/crypto/ec_public_key.dart'; export 'package:coinlib/src/crypto/ecdsa_signature.dart'; diff --git a/coinlib/lib/src/crypto/ec_compressed_public_key.dart b/coinlib/lib/src/crypto/ec_compressed_public_key.dart new file mode 100644 index 0000000..224bede --- /dev/null +++ b/coinlib/lib/src/crypto/ec_compressed_public_key.dart @@ -0,0 +1,31 @@ +import 'dart:typed_data'; + +import 'package:coinlib/src/common/hex.dart'; +import 'ec_public_key.dart'; + +/// Represents an [ECPublicKey] that must be in a compressed format, or else a +/// [InvalidPublicKey] will be thrown. +class ECCompressedPublicKey extends ECPublicKey { + + ECCompressedPublicKey(super.data) { + if (data.length != 33) throw InvalidPublicKey(); + } + ECCompressedPublicKey.fromHex(String hex) : this(hexToBytes(hex)); + ECCompressedPublicKey.fromXOnly(super.xcoord) : super.fromXOnly(); + ECCompressedPublicKey.fromXOnlyHex(super.hex) : super.fromXOnlyHex(); + ECCompressedPublicKey.fromPubkey(ECPublicKey key) : this( + key.compressed + ? key.data + : Uint8List.fromList([key.yIsEven ? 2 : 3, ...key.x]), + ); + + @override + ECCompressedPublicKey? tweak(Uint8List scalar) { + final tweaked = super.tweak(scalar); + return tweaked == null ? null : ECCompressedPublicKey.fromPubkey(tweaked); + } + + @override + ECCompressedPublicKey get xonly => ECCompressedPublicKey.fromXOnly(x); + +} diff --git a/coinlib/test/crypto/ec_compressed_public_key_test.dart b/coinlib/test/crypto/ec_compressed_public_key_test.dart new file mode 100644 index 0000000..353a714 --- /dev/null +++ b/coinlib/test/crypto/ec_compressed_public_key_test.dart @@ -0,0 +1,65 @@ +import 'package:coinlib/coinlib.dart'; +import 'package:test/test.dart'; +import '../vectors/keys.dart'; + +void main() { + + group("ECCompressedPublicKey", () { + + setUpAll(loadCoinlib); + + test("requires 33 bytes", () { + + for (final failing in [ + // Too small + pubkeyVec.substring(0, 32*2), + // Too large + longPubkeyVec, + "${pubkeyVec}ff", + ]) { + expect( + () => ECCompressedPublicKey.fromHex(failing), + throwsA(isA()), + ); + } + + }); + + test("accepts compressed types", () { + for (final vec in validPubKeys) { + if (!vec.compressed) continue; + final pk = ECCompressedPublicKey.fromHex(vec.hex); + expect(pk.hex, vec.hex); + expect(pk.compressed, true); + expect(pk.yIsEven, vec.evenY); + } + }); + + test(".fromXOnly", () => expect( + ECCompressedPublicKey.fromXOnlyHex(xOnlyPubkeyVec).hex, + "02$xOnlyPubkeyVec", + ),); + + test(".fromPubkey", () { + + void expectCompressedKey(String pubkey, String compressed) => expect( + ECCompressedPublicKey.fromPubkey(ECPublicKey.fromHex(pubkey)).hex, + compressed, + ); + + expectCompressedKey(longPubkeyVec, pubkeyVec); + expectCompressedKey(pubkeyVec, pubkeyVec); + expectCompressedKey( + "06ef164284e2c3abc32b310eb62904af0d49196c51087bdf4038998f8818787c882433ae83422904f48ad36dcf351ac9a37e6b00e57cf40b469b650ec850640efe", + "02ef164284e2c3abc32b310eb62904af0d49196c51087bdf4038998f8818787c88", + ); + expectCompressedKey( + "07576168b540f6f80e4d2a325f8cbd420ceb170ff42cd07e96bffc5e6a4a4ea04b1208f618306fd629cd2972cea45aa81ae7b24a64bf2e86704d7a63d82fd97a8f", + "03576168b540f6f80e4d2a325f8cbd420ceb170ff42cd07e96bffc5e6a4a4ea04b", + ); + + }); + + }); + +} diff --git a/coinlib/test/crypto/ec_public_key_test.dart b/coinlib/test/crypto/ec_public_key_test.dart index e4c4b27..4955124 100644 --- a/coinlib/test/crypto/ec_public_key_test.dart +++ b/coinlib/test/crypto/ec_public_key_test.dart @@ -14,11 +14,11 @@ void main() { for (final failing in [ // Too small - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + pubkeyVec.substring(0, 32*2), + longPubkeyVec.substring(0, 32*2), // Too large - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021", - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021", + "${pubkeyVec}ff", + "${longPubkeyVec}ff", ]) { expect( () => ECPublicKey.fromHex(failing), @@ -46,10 +46,8 @@ void main() { test(".fromXOnly", () { expect( - ECPublicKey.fromXOnlyHex( - "d69c3509bb99e412e68b0fe8544e72837dfa30746d8be2aa65975f29d22dc7b9", - ).hex, - "02d69c3509bb99e412e68b0fe8544e72837dfa30746d8be2aa65975f29d22dc7b9", + ECCompressedPublicKey.fromXOnlyHex(xOnlyPubkeyVec).hex, + "02$xOnlyPubkeyVec", ); for (final invalid in [ diff --git a/coinlib/test/vectors/keys.dart b/coinlib/test/vectors/keys.dart index f065900..be39abc 100644 --- a/coinlib/test/vectors/keys.dart +++ b/coinlib/test/vectors/keys.dart @@ -20,6 +20,7 @@ class KeyTestVector { final pubkeyVec = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; final longPubkeyVec = "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"; final pubkeyhashVec = "751e76e8199196d454941c45d1b3a323f1433bd6"; +final xOnlyPubkeyVec = "d69c3509bb99e412e68b0fe8544e72837dfa30746d8be2aa65975f29d22dc7b9"; final keyPairVectors = [ KeyTestVector(