Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Adaptor Signatures #62

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion lib/secp256k1/point.ex
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ defmodule Bitcoinex.Secp256k1.Point do
end
end

@doc """
negate returns the pubkey with the same x but the other y.
It does this by passing y % 2 == 0 as y_is_odd to Secp256k1.get_y.
"""
@spec negate(t()) :: t()
def negate(%__MODULE__{x: x, y: y}) do
{:ok, y} = Secp256k1.get_y(x, (y &&& 1) == 0)
%__MODULE__{x: x, y: y}
end

@doc """
sec serializes a compressed public key to binary
"""
Expand All @@ -124,7 +134,7 @@ defmodule Bitcoinex.Secp256k1.Point do
"""
@spec x_bytes(t()) :: binary
def x_bytes(%__MODULE__{x: x}) do
Bitcoinex.Utils.pad(:binary.encode_unsigned(x), 32, :leading)
Utils.int_to_big(x, 32)
end

@doc """
Expand Down
5 changes: 5 additions & 0 deletions lib/secp256k1/privatekey.ex
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ defmodule Bitcoinex.Secp256k1.PrivateKey do
end
end

@spec negate(t()) :: t()
def negate(%__MODULE__{d: d}) do
%__MODULE__{d: @n - d}
end

@doc """
serialize_private_key serializes a private key into hex
"""
Expand Down
237 changes: 198 additions & 39 deletions lib/secp256k1/schnorr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,35 +35,20 @@ defmodule Bitcoinex.Secp256k1.Schnorr do
tagged_aux_hash = tagged_hash_aux(aux_bytes)
t = Utils.xor_bytes(d_bytes, tagged_aux_hash)

{:ok, k0} =
tagged_hash_nonce(t <> Point.x_bytes(d_point) <> z_bytes)
|> :binary.decode_unsigned()
|> Math.modulo(@n)
|> PrivateKey.new()

if k0.d == 0 do
{:error, "invalid aux randomness"}
else
r_point = PrivateKey.to_point(k0)

case Secp256k1.force_even_y(k0) do
{:error, msg} ->
{:error, msg}

k ->
e =
tagged_hash_challenge(
Point.x_bytes(r_point) <> Point.x_bytes(d_point) <> z_bytes
)
|> :binary.decode_unsigned()
|> Math.modulo(@n)

sig_s =
(k.d + d.d * e)
|> Math.modulo(@n)

{:ok, %Signature{r: r_point.x, s: sig_s}}
end
case calculate_k(t, d_point, z_bytes) do
{:ok, k0} ->
r_point = PrivateKey.to_point(k0)

case Secp256k1.force_even_y(k0) do
{:error, msg} ->
{:error, msg}

k ->
e = calculate_e(Point.x_bytes(r_point), Point.x_bytes(d_point), z_bytes)
sig_s = calculate_s(k, d, e)

{:ok, %Signature{r: r_point.x, s: sig_s}}
end
end
end
end
Expand All @@ -73,8 +58,72 @@ defmodule Bitcoinex.Secp256k1.Schnorr do
defp tagged_hash_nonce(nonce), do: Utils.tagged_hash("BIP0340/nonce", nonce)
defp tagged_hash_challenge(chal), do: Utils.tagged_hash("BIP0340/challenge", chal)

defp calculate_r(pubkey, s, e) do
@generator_point
|> Math.multiply(s)
|> Math.add(Math.multiply(pubkey, Params.curve().n - e))
end

defp calculate_s(k, d, e) do
(k.d + d.d * e)
|> Math.modulo(@n)
end

defp calculate_k(t, d_point, z_bytes) do
{:ok, k0} =
tagged_hash_nonce(t <> Point.x_bytes(d_point) <> z_bytes)
|> :binary.decode_unsigned()
|> Math.modulo(@n)
|> PrivateKey.new()

if k0.d == 0 do
{:error, "invalid aux randomness"}
else
{:ok, Secp256k1.force_even_y(k0)}
end
end

defp calculate_e(nonce_bytes, pubkey_bytes, msg_bytes) do
tagged_hash_challenge(nonce_bytes <> pubkey_bytes <> msg_bytes)
|> :binary.decode_unsigned()
|> Math.modulo(@n)
end

# this is just like validate_r but without the R.y evenness check
defp partial_validate_r(r_point, rx) do
cond do
Point.is_inf(r_point) ->
{:error, "R point is infinite"}

r_point.x != rx ->
{:error, "x's do not match #{r_point.x} vs #{rx}"}

true ->
true
end
end

defp validate_r(r_point, rx) do
cond do
Point.is_inf(r_point) ->
# {:error, "R point is infinite"}
false

!Point.has_even_y(r_point) ->
# {:error, "R point is not even"}
false

r_point.x != rx ->
# {:error, "x's do not match #{r_point.x} vs #{rx}"}
false

true ->
true
end
end

@doc """
verify whether the schnorr signature is valid for the given message hash and public key
verify_signature verifies whether the Schnorr signature is valid for the given message hash and public key
"""
@spec verify_signature(Point.t(), non_neg_integer, Signature.t()) ::
boolean | {:error, String.t()}
Expand All @@ -85,17 +134,127 @@ defmodule Bitcoinex.Secp256k1.Schnorr do
def verify_signature(pubkey, z, %Signature{r: r, s: s}) do
r_bytes = Utils.int_to_big(r, 32)
z_bytes = Utils.int_to_big(z, 32)
e = calculate_e(r_bytes, Point.x_bytes(pubkey), z_bytes)

e =
tagged_hash_challenge(r_bytes <> Point.x_bytes(pubkey) <> z_bytes)
|> :binary.decode_unsigned()
|> Math.modulo(@n)
r_point = calculate_r(pubkey, s, e)

validate_r(r_point, r)
end

# negate a secret
defp conditional_negate(d, true), do: %PrivateKey{d: d} |> PrivateKey.negate()
defp conditional_negate(d, false), do: %PrivateKey{d: d}

r_point =
@generator_point
|> Math.multiply(s)
|> Math.add(Math.multiply(pubkey, @n - e))
# negate a point (switches parity of P.y)
defp conditional_negate_point(point, true), do: Point.negate(point)
defp conditional_negate_point(point, false), do: point

# Adaptor/Encrypted Signatures

@doc """
encrypted_sign signs a message hash z with Private Key sk but encrypts the signature using the tweak_point
as the encryption key. The signer need not know the decryption key / tweak itself, which can later be used
to decrypt the signature into a valid Schnorr signature. This produces an Adaptor Signature.
"""
@spec encrypted_sign(PrivateKey.t(), non_neg_integer(), non_neg_integer(), Point.t()) ::
{:ok, Signature.t(), boolean}
def encrypted_sign(sk = %PrivateKey{}, z, aux, tweak_point = %Point{}) do
z_bytes = Utils.int_to_big(z, 32)
aux_bytes = Utils.int_to_big(aux, 32)
d_point = PrivateKey.to_point(sk)

case Secp256k1.force_even_y(sk) do
{:error, msg} ->
{:error, msg}

d ->
d_bytes = Utils.int_to_big(d.d, 32)
tagged_aux_hash = tagged_hash_aux(aux_bytes)
t = Utils.xor_bytes(d_bytes, tagged_aux_hash)
# TODO always add tweak_point to the nonce to commit to it as well
case calculate_k(t, d_point, z_bytes) do
{:ok, k0} ->
r_point = PrivateKey.to_point(k0)
# ensure that tweak_point has even Y
tweaked_r_point = Math.add(r_point, tweak_point)
# ensure (R+T).y is even, if not, negate it, negate k, and set was_negated = true
{tweaked_r_point, was_negated} = make_point_even(tweaked_r_point)
k = conditional_negate(k0.d, was_negated)

e = calculate_e(Point.x_bytes(tweaked_r_point), Point.x_bytes(d_point), z_bytes)
s = calculate_s(k, d, e)
# we return Signature{R+T,s}, not a valid signature since s is untweaked.
{:ok, %Signature{r: tweaked_r_point.x, s: s}, was_negated}
end
end
end

@doc """
verify_encrypted_signature verifies that an encrypted signature commits to a tweak_point / encryption key.
This is different from a regular Schnorr signature verification, as encrypted signatures are not valid Schnorr Signatures.
"""
@spec verify_encrypted_signature(
Signature.t(),
Point.t(),
non_neg_integer(),
Point.t(),
boolean
) :: boolean
def verify_encrypted_signature(
%Signature{r: tweaked_r, s: s},
pk = %Point{},
z,
tweak_point = %Point{},
was_negated
) do
z_bytes = Utils.int_to_big(z, 32)

{:ok, tweaked_r_point} = Point.lift_x(tweaked_r)
# This is subtracting the tweak_point (T) from the tweaked_point (R + T) to get the original R
tweak_point = conditional_negate_point(tweak_point, !was_negated)
r_point = Math.add(tweaked_r_point, tweak_point)

e = calculate_e(Point.x_bytes(tweaked_r_point), Point.x_bytes(pk), z_bytes)
r_point2 = calculate_r(pk, s, e)
partial_validate_r(r_point, r_point2.x)
end

defp make_point_even(point) do
if Point.has_even_y(point) do
{point, false}
else
{Point.negate(point), true}
end
end

@doc """
decrypt_signature uses the tweak/decryption key to transform an
adaptor/encrypted signature into a final, valid Schnorr signature.
"""
@spec decrypt_signature(Signature.t(), PrivateKey.t(), boolean) :: Signature.t()
def decrypt_signature(%Signature{r: r, s: s}, tweak, was_negated) do
# force even on tweak is a backup. the passed tweak should already be properly negated
tweak = conditional_negate(tweak.d, was_negated)
final_s = Math.modulo(tweak.d + s, @n)
%Signature{r: r, s: final_s}
end

@doc """
recover_decryption_key recovers the tweak or decryption key by
subtracting final_sig.s - encrypted_sig.s (mod n). The tweak is
negated if the original R+T point was negated during signing.
"""
@spec recover_decryption_key(Signature.t(), Signature.t(), boolean) ::
PrivateKey.t() | {:error, String.t()}
def recover_decryption_key(%Signature{r: enc_r}, %Signature{r: r}, _, _) when enc_r != r,
do: {:error, "invalid signature pair"}

!Point.is_inf(r_point) && Point.has_even_y(r_point) && r_point.x == r
def recover_decryption_key(
_encrypted_sig = %Signature{s: enc_s},
_sig = %Signature{s: s},
was_negated
) do
t = Math.modulo(s - enc_s, @n)
conditional_negate(t, was_negated)
end
end
9 changes: 8 additions & 1 deletion lib/secp256k1/secp256k1.ex
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,14 @@ defmodule Bitcoinex.Secp256k1 do

@spec serialize_signature(t()) :: binary
def serialize_signature(%__MODULE__{r: r, s: s}) do
:binary.encode_unsigned(r) <> :binary.encode_unsigned(s)
Utils.int_to_big(r, 32) <> Utils.int_to_big(s, 32)
end

@spec to_hex(t()) :: binary
def to_hex(sig) do
sig
|> serialize_signature()
|> Base.encode16(case: :lower)
end

@doc """
Expand Down
58 changes: 58 additions & 0 deletions scripts/gen_test_vectors.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
alias Bitcoinex.Secp256k1
alias Bitcoinex.Secp256k1.{Math, PrivateKey, Point, Schnorr, Signature}
alias Bitcoinex.Utils

to_hex = fn i -> "0x" <> Integer.to_string(i, 16) end

write_row = fn file, sk, pk, tw, t_point, z, aux, ut_sig, tw_sig, err, is_tweaked_s_even, is_tweaked_s_ooo -> IO.binwrite(file,
to_hex.(sk.d) <> "," <> Point.x_hex(pk) <> "," <> to_hex.(tw.d) <> ","
<> Point.x_hex(t_point) <> "," <> to_hex.(z) <> "," <> to_hex.(aux) <> ","
<> Signature.to_hex(ut_sig) <> "," <> Signature.to_hex(tw_sig) <> ","
<> err <> "," <> to_string(is_tweaked_s_even) <> "," <> to_string(is_tweaked_s_ooo) <> "\n")
end

order_n = 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141

{:ok, good_file} = File.open("schnorr_adaptor_test_vectors-good.csv", [:write])
{:ok, bad_file} = File.open("schnorr_adaptor_test_vectors-bad.csv", [:write])

IO.binwrite(good_file, "private_key,public_key,tweak_secret,tweak_point,message_hash,aux_rand,untweaked_adaptor_signature,tweaked_signature,is_tweaked_s_even\n")
IO.binwrite(bad_file, "private_key,public_key,tweak_secret,tweak_point,message_hash,aux_rand,untweaked_adaptor_signature,tweaked_signature,is_tweaked_s_even\n")

for _ <- 1..50 do
ski = :rand.uniform(order_n-1)
{:ok, sk0} = PrivateKey.new(ski)
sk = Secp256k1.force_even_y(sk0)
pk = PrivateKey.to_point(sk)

# tweak
ti = :rand.uniform(order_n-1)
{:ok, tw} = PrivateKey.new(ti)
tw = Secp256k1.force_even_y(tw)
tw_point = PrivateKey.to_point(tw)

msg =
:rand.uniform(order_n-1)
|> :binary.encode_unsigned()
z = Utils.double_sha256(msg) |> :binary.decode_unsigned()

aux = :rand.uniform(order_n-1)

# create adaptor sig
{:ok, ut_sig, _tw_point} = Schnorr.sign_for_tweak(sk, z, aux, tw_point)
tw_sig = Schnorr.tweak_signature(ut_sig, tw.d)

# checks
tweaked_s = tw.d+ut_sig.s
is_tweaked_s_ooo = tweaked_s > order_n
{:ok, tweaked_s} = PrivateKey.new(Math.modulo(tweaked_s, order_n))
tweaked_forced_s = Secp256k1.force_even_y(tweaked_s)
is_tweaked_s_even = tweaked_forced_s == tweaked_s

case Schnorr.verify_signature(pk, z, tw_sig) do
true ->
write_row.(good_file, sk, pk, tw, tw_point, z, aux, ut_sig, tw_sig, "", is_tweaked_s_even, is_tweaked_s_ooo)
{:error, err} ->
write_row.(bad_file, sk, pk, tw, tw_point, z, aux, ut_sig, tw_sig, err, is_tweaked_s_even, is_tweaked_s_ooo)
end
end
Loading