lib/secp256k1/schnorr.ex

defmodule Bitcoinex.Secp256k1.Schnorr do
  @moduledoc """
  Schnorr-specific secp256k1 operations
  """
  alias Bitcoinex.Secp256k1
  alias Bitcoinex.Secp256k1.{Math, Params, Point, PrivateKey, Signature}
  alias Bitcoinex.Utils

  @n Params.curve().n
  @p Params.curve().p

  @generator_point %Point{
    x: Params.curve().g_x,
    y: Params.curve().g_y
  }

  @spec sign(PrivateKey.t(), non_neg_integer(), non_neg_integer()) ::
          {:ok, Signature.t()} | {:error, String.t()}
  def sign(privkey, z, aux) do
    case PrivateKey.validate(privkey) do
      {:error, msg} ->
        {:error, msg}

      {:ok, privkey} ->
        z_bytes = Utils.int_to_big(z, 32)
        aux_bytes = Utils.int_to_big(aux, 32)
        d_point = PrivateKey.to_point(privkey)

        case Secp256k1.force_even_y(privkey) 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)

            {: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
            end
        end
    end
  end

  defp tagged_hash_aux(aux), do: Utils.tagged_hash("BIP0340/aux", aux)
  defp tagged_hash_nonce(nonce), do: Utils.tagged_hash("BIP0340/nonce", nonce)
  defp tagged_hash_challenge(chal), do: Utils.tagged_hash("BIP0340/challenge", chal)

  @doc """
    verify 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()}
  def verify_signature(_pubkey, _z, %Signature{r: r, s: s})
      when r >= @p or s >= @n,
      do: {:error, "invalid signature"}

  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 =
      tagged_hash_challenge(r_bytes <> Point.x_bytes(pubkey) <> z_bytes)
      |> :binary.decode_unsigned()
      |> Math.modulo(@n)

    r_point =
      @generator_point
      |> Math.multiply(s)
      |> Math.add(Math.multiply(pubkey, @n - e))

    !Point.is_inf(r_point) && Point.has_even_y(r_point) && r_point.x == r
  end
end