lib/secp256k1/secp256k1.ex

defmodule Bitcoinex.Secp256k1 do
  @moduledoc """
  General Secp256k1 curve operations.
  libsecp256k1: https://github.com/bitcoin-core/secp256k1

  Currently supports ECDSA public key recovery.

  In the future, we will NIF for critical operations. However, it is more portable to have a native elixir version.
  """
  import Bitwise
  alias Bitcoinex.Secp256k1.{Math, Params, Point, PrivateKey}

  defmodule Signature do
    @moduledoc """
    Contains r,s in signature.
    """
    alias Bitcoinex.Utils

    @type t :: %__MODULE__{
            r: pos_integer(),
            s: pos_integer()
          }

    @enforce_keys [
      :r,
      :s
    ]
    defstruct [:r, :s]

    @spec parse_signature(binary) ::
            {:ok, t()} | {:error, String.t()}
    @doc """
    accepts a compact signature and returns a Signature containing r,s
    """
    def parse_signature(<<r::binary-size(32), s::binary-size(32)>>) do
      # Get r,s from signature.
      r = :binary.decode_unsigned(r)
      s = :binary.decode_unsigned(s)

      # Verify that r,s are integers in [1, n-1] where n is the integer order of G.
      cond do
        r < 1 ->
          {:error, "invalid signature"}

        r > Params.curve().n - 1 ->
          {:error, "invalid signature"}

        s < 1 ->
          {:error, "invalid signature"}

        s > Params.curve().n - 1 ->
          {:error, "invalid signature"}

        true ->
          {:ok, %Signature{r: r, s: s}}
      end
    end

    # attempt to parse 64-byte string
    def parse_signature(compact_sig) when is_binary(compact_sig) do
      case Utils.hex_to_bin(compact_sig) do
        {:error, msg} ->
          {:error, msg}

        sig_bytes ->
          parse_signature(sig_bytes)
      end
    end

    def parse_signature(_), do: {:error, "invalid signature size"}

    @doc """
    der_parse_signature parses a DER binary to a Signature
    """
    # @spec der_parse_signature(binary) :: {:ok, Signature.()} | {:error, String.t()}
    def der_parse_signature(<<0x30>> <> der_sig) when is_binary(der_sig) do
      sig_len = :binary.at(der_sig, 0)

      if sig_len + 1 != byte_size(der_sig) do
        {:error, "invalid signature length"}
      else
        case parse_sig_key(der_sig, 1) do
          {:error, err} ->
            {:error, err}

          {r, s_pos} ->
            case parse_sig_key(der_sig, s_pos) do
              {:error, err} ->
                {:error, err}

              {s, sig_len} ->
                if sig_len != byte_size(der_sig) do
                  {:error, "invalid signature: signature is too long"}
                else
                  {:ok, %Signature{r: r, s: s}}
                end
            end
        end
      end
    end

    def der_parse_signature(_), do: {:error, "invalid signature"}

    defp parse_sig_key(data, pos) do
      if :binary.at(data, pos) != 0x02 do
        {:error, "invalid signature key marker"}
      else
        k_len = :binary.at(data, pos + 1)
        len_k = :binary.part(data, pos + 2, k_len)
        {:binary.decode_unsigned(len_k), pos + 2 + k_len}
      end
    end

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

    @doc """
    der_serialize_signature returns the DER serialization of an ecdsa signature
    """
    @spec der_serialize_signature(Signature.t()) :: binary
    def der_serialize_signature(%Signature{r: r, s: s}) do
      r_bytes = serialize_sig_key(r)
      s_bytes = serialize_sig_key(s)
      <<0x30>> <> len_as_bytes(r_bytes <> s_bytes) <> r_bytes <> s_bytes
    end

    def der_serialize_signature(_), do: {:error, "Signature object required"}

    defp serialize_sig_key(k) do
      k
      |> :binary.encode_unsigned()
      |> lstrip(<<0x00>>)
      |> add_high_bit()
      |> prefix_key()
    end

    defp len_as_bytes(data), do: :binary.encode_unsigned(byte_size(data))

    defp lstrip(<<head::binary-size(1)>> <> tail, val) do
      if head == val, do: lstrip(tail, val), else: head <> tail
    end

    defp add_high_bit(k_bytes) do
      unless (:binary.at(k_bytes, 0) &&& 0x80) == 0 do
        <<0x00>> <> k_bytes
      else
        k_bytes
      end
    end

    defp prefix_key(k_bytes), do: <<0x02>> <> len_as_bytes(k_bytes) <> k_bytes
  end

  @doc """
  Returns the y-coordinate of a secp256k1 curve point (P) using the x-coordinate.
  To get P(y), we solve for y in this equation: y^2 = x^3 + 7.
  """
  @spec get_y(integer, boolean) :: {:ok, integer} | {:error, String.t()}
  def get_y(x, is_y_odd) do
    # x^3 + 7
    y_sq =
      :crypto.mod_pow(x, 3, Params.curve().p)
      |> :binary.decode_unsigned()
      |> Kernel.+(7 |> Math.modulo(Params.curve().p))

    # Solve for y.
    y =
      :crypto.mod_pow(y_sq, Integer.floor_div(Params.curve().p + 1, 4), Params.curve().p)
      |> :binary.decode_unsigned()

    y =
      case rem(y, 2) == 1 do
        ^is_y_odd ->
          y

        _ ->
          Params.curve().p - y
      end

    # Check.
    if y_sq != :crypto.mod_pow(y, 2, Params.curve().p) |> :binary.decode_unsigned() do
      {:error, "invalid sq root"}
    else
      {:ok, y}
    end
  end

  @doc """
    force_even_y returns the negated private key
    if the associated Point has an odd y. Otherwise
    it returns the private key
  """
  @spec force_even_y(PrivateKey.t()) :: PrivateKey.t() | {:error, String.t()}
  def force_even_y(%PrivateKey{} = privkey) do
    case PrivateKey.to_point(privkey) do
      {:error, msg} ->
        {:error, msg}

      pubkey ->
        if Point.is_inf(pubkey) do
          {:error, "pubkey is infinity. bad luck"}
        else
          if Point.has_even_y(pubkey) do
            privkey
          else
            %PrivateKey{d: Params.curve().n - privkey.d}
          end
        end
    end
  end

  @doc """
    verify_point verifies that a given point is on the secp256k1
    curve
  """
  @spec verify_point(Point.t()) :: bool
  def verify_point(%Point{x: x, y: y}) do
    y_odd = rem(y, 2) == 1
    {:ok, new_y} = get_y(x, y_odd)
    y == new_y
  end
end