diff --git a/lib/bitcoin/bip324.rb b/lib/bitcoin/bip324.rb index 6032d5a..74b9e60 100644 --- a/lib/bitcoin/bip324.rb +++ b/lib/bitcoin/bip324.rb @@ -77,5 +77,20 @@ def xswiftec_inv(x, u, c) end ECDSA::Format::IntegerOctetString.encode(result, 32).bth end + + # Given a field element X on the curve, find (u, t) that encode them. + # @param [String] x coordinate with hex format. + # @return [String] ElligatorSwift public key with hex format. + def xelligatorswift(x) + loop do + u = SecureRandom.random_number(1..ECDSA::Group::Secp256k1.order).to_s(16) + c = Random.rand(0..8) + t = xswiftec_inv(x, u, c) + unless t.nil? + return (ECDSA::Format::IntegerOctetString.encode(u.hex, 32) + + ECDSA::Format::IntegerOctetString.encode(t.hex, 32)).bth + end + end + end end end diff --git a/lib/bitcoin/bip324/ell_swift_pubkey.rb b/lib/bitcoin/bip324/ell_swift_pubkey.rb index d0b3674..b08cad4 100644 --- a/lib/bitcoin/bip324/ell_swift_pubkey.rb +++ b/lib/bitcoin/bip324/ell_swift_pubkey.rb @@ -26,7 +26,7 @@ def decode u = key[0...32].bth t = key[32..-1].bth x = BIP324.xswiftec(u, t) - Bitcoin::Key.from_xonly_pubkey(x) + Bitcoin::Key.new(pubkey: "03#{x}") end end diff --git a/lib/bitcoin/key.rb b/lib/bitcoin/key.rb index 515a8b8..dbe27cd 100644 --- a/lib/bitcoin/key.rb +++ b/lib/bitcoin/key.rb @@ -344,6 +344,17 @@ def fully_valid_pubkey?(allow_hybrid = false) valid_pubkey? && secp256k1_module.parse_ec_pubkey?(pubkey, allow_hybrid) end + # Create an ellswift-encoded public key for this key, with specified entropy. + # @return [Bitcoin::BIP324::EllSwiftPubkey] + # @raise ArgumentError If ent32 does not 32 bytes. + def create_ell_pubkey + if secp256k1_module.is_a?(Bitcoin::Secp256k1::Native) + Bitcoin::BIP324::EllSwiftPubkey.new(secp256k1_module.ellswift_create(priv_key)) + else + Bitcoin::BIP324::EllSwiftPubkey.new(Bitcoin::BIP324.xelligatorswift(xonly_pubkey)) + end + end + private def self.compare_big_endian(c1, c2) diff --git a/lib/bitcoin/secp256k1/native.rb b/lib/bitcoin/secp256k1/native.rb index 7d7da99..6721fbc 100644 --- a/lib/bitcoin/secp256k1/native.rb +++ b/lib/bitcoin/secp256k1/native.rb @@ -59,6 +59,7 @@ def load_functions attach_function(:secp256k1_ecdsa_recover, [:pointer, :pointer, :pointer, :pointer], :int) attach_function(:secp256k1_ecdsa_recoverable_signature_parse_compact, [:pointer, :pointer, :pointer, :int], :int) attach_function(:secp256k1_ellswift_decode, [:pointer, :pointer, :pointer], :int) + attach_function(:secp256k1_ellswift_create, [:pointer, :pointer, :pointer, :pointer], :int) end def with_context(flags: (SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN)) @@ -227,7 +228,7 @@ def valid_xonly_pubkey?(pub_key) # Decode ellswift public key. # @param [String] ell_key ElligatorSwift key with binary format. - # @return [String] + # @return [String] Decoded public key with hex format def ellswift_decode(ell_key) with_context do |context| ell64 = FFI::MemoryPointer.new(:uchar, ell_key.bytesize).put_bytes(0, ell_key) @@ -238,6 +239,19 @@ def ellswift_decode(ell_key) end end + # Compute an ElligatorSwift public key for a secret key. + # @param [String] priv_key private key with hex format + # @return [String] ElligatorSwift public key with hex format. + def ellswift_create(priv_key) + with_context(flags: SECP256K1_CONTEXT_SIGN) do |context| + ell64 = FFI::MemoryPointer.new(:uchar, 64) + seckey32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, priv_key.htb) + result = secp256k1_ellswift_create(context, ell64, seckey32, nil) + raise ArgumentError, 'Failed to create ElligatorSwift public key.' unless result == 1 + ell64.read_string(64).bth + end + end + private # Calculate full public key(64 bytes) from public key(32 bytes). diff --git a/spec/bitcoin/key_spec.rb b/spec/bitcoin/key_spec.rb index 90f2117..db073fa 100644 --- a/spec/bitcoin/key_spec.rb +++ b/spec/bitcoin/key_spec.rb @@ -2,6 +2,11 @@ describe Bitcoin::Key do + let(:secret1) { '5HxWvvfubhXpYYpS3tJkw6fq9jE9j18THftkZjHHfmFiWtmAbrj' } + let(:secret2) { '5KC4ejrDjv152FGwP386VD1i2NYc5KkfSMyv1nGy1VGDxGHqVY3' } + let(:secret3) { 'Kwr371tjA9u2rFSMZjTNun2PXXP3WPZu2afRHTcta6KxEUdm1vEw' } + let(:secret4) { 'L3Hq7a8FEQwJkW1M2GNKDW28546Vp5miewcCzSqUD9kCAXrJdS3g' } + describe "initialize" do it do expect{described_class.new(priv_key: '6f3acb5b7ac66dacf87910bb0b04bed78284b9b50c0d061705a44447a947ff')}. @@ -340,4 +345,21 @@ def test_decompress expect(uncompressed_key.decompress_pubkey).to eq(uncompressed_key.pubkey) end end + + describe "#create_ell_pubkey", network: :mainnet do + context "native", use_secp256k1: true do + it { test_ell_pubkey } + end + context "ruby" do + it { test_ell_pubkey } + end + def test_ell_pubkey + [secret1, secret2, secret3, secret4].each do |wif| + key = Bitcoin::Key.from_wif(wif) + ellswift = key.create_ell_pubkey + key = Bitcoin::Key.new(pubkey: key.to_point.to_hex(true)) unless key.compressed? + expect(ellswift.decode.pubkey).to eq(key.pubkey) + end + end + end end