lib/secp256k1/ecdsa.ex

defmodule Bitcoinex.Secp256k1.Ecdsa do
  alias Bitcoinex.Secp256k1
  alias Bitcoinex.Secp256k1.{Math, Params, Point, PrivateKey, Signature}

  @n Params.curve().n

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

  @doc """
    deterministic_k implements rfc6979 and returns
    a deterministic k value for signatures
  """
  @spec deterministic_k(PrivateKey.t(), non_neg_integer()) :: PrivateKey.t()
  def deterministic_k(%PrivateKey{d: d}, raw_z) do
    # RFC 6979 https://tools.ietf.org/html/rfc6979#section-3.2
    k = :binary.list_to_bin(List.duplicate(<<0x00>>, 32))
    v = :binary.list_to_bin(List.duplicate(<<0x01>>, 32))
    n = @n
    z = lower_z(raw_z, n)
    # 3.2(d) - pad z and privkey for use
    sighash = Bitcoinex.Utils.pad(:binary.encode_unsigned(z), 32, :leading)
    secret = Bitcoinex.Utils.pad(:binary.encode_unsigned(d), 32, :leading)
    # 3.2(d) - hmac with key k
    k = :crypto.mac(:hmac, :sha256, k, v <> <<0x00>> <> secret <> sighash)
    # 3.2(e) - update v
    v = :crypto.mac(:hmac, :sha256, k, v)
    # 3.2(f) - update k
    k = :crypto.mac(:hmac, :sha256, k, v <> <<0x01>> <> secret <> sighash)
    # 3.2(g) - update v
    v = :crypto.mac(:hmac, :sha256, k, v)
    # 3.2(h) - algorithm
    final_k = find_candidate(k, v, n)
    %PrivateKey{d: final_k}
  end

  defp find_candidate(k, v, n) do
    # RFC 6979 https://tools.ietf.org/html/rfc6979#section-3.2
    v = :crypto.mac(:hmac, :sha256, k, v)
    candidate = :binary.decode_unsigned(v)
    # 3.2(h).3 - check candidate in [1,n-1] and r != 0
    if candidate >= 1 and candidate < n do
      case PrivateKey.to_point(%PrivateKey{d: candidate}) do
        {:error, _msg} ->
          find_next_candidate(k, v, n)

        pk ->
          if pk.x != 0 do
            candidate
          else
            find_next_candidate(k, v, n)
          end
      end
    else
      # if candidate is invalid
      find_next_candidate(k, v, n)
    end
  end

  defp find_next_candidate(k, v, n) do
    k = :crypto.mac(:hmac, :sha256, k, v <> <<0x00>>)
    v = :crypto.mac(:hmac, :sha256, k, v)
    find_candidate(k, v, n)
  end

  defp lower_z(z, n) do
    if z > n, do: z - n, else: z
  end

  @doc """
    sign returns an ECDSA signature using the privkey and z
    where privkey is a PrivateKey object and z is an integer.
    The nonce is derived using RFC6979.
  """
  @spec sign(PrivateKey.t(), integer) :: Signature.t()
  def sign(privkey, z) do
    case PrivateKey.validate(privkey) do
      {:error, msg} ->
        {:error, msg}

      {:ok, privkey} ->
        k = deterministic_k(privkey, z)

        case PrivateKey.to_point(k) do
          {:error, msg} ->
            {:error, msg}

          pk ->
            sig_r = pk.x
            inv_k = Math.inv(k.d, @n)
            sig_s = Math.modulo((z + sig_r * privkey.d) * inv_k, @n)

            if sig_s > @n / 2 do
              %Signature{r: sig_r, s: @n - sig_s}
            else
              %Signature{r: sig_r, s: sig_s}
            end
        end
    end
  end

  @doc """
  sign_message returns an ECDSA signature using the privkey and "Bitcoin Signed Message: <msg>"
  where privkey is a PrivateKey object and msg is a binary message to be hashed.
  The message is hashed using hash256 (double SHA256) and the nonce is derived
  using RFC6979.
  """
  @spec sign_message(PrivateKey.t(), binary) :: Signature.t()
  def sign_message(privkey, msg) do
    z =
      ("Bitcoin Signed Message:\n" <> msg)
      |> Bitcoinex.Utils.double_sha256()
      |> :binary.decode_unsigned()

    sign(privkey, z)
  end

  @doc """
    verify whether the ecdsa signature is valid
    for the given message hash and public key
  """
  @spec verify_signature(Point.t(), integer, Signature.t()) :: boolean
  def verify_signature(pubkey, sighash, %Signature{r: r, s: s}) do
    s_inv = Math.inv(s, @n)
    u = Math.modulo(sighash * s_inv, @n)
    v = Math.modulo(r * s_inv, @n)
    total = Math.add(Math.multiply(@generator_point, u), Math.multiply(pubkey, v))
    total.x == r
  end

  @doc """
    ecdsa_recover_compact does ECDSA public key recovery.
  """
  @spec ecdsa_recover_compact(binary, binary, integer) ::
          {:ok, binary} | {:error, String.t()}
  def ecdsa_recover_compact(msg, compact_sig, recoveryId) do
    # Parse r and s from the signature.
    case Signature.parse_signature(compact_sig) do
      {:ok, sig} ->
        # Find the iteration.

        # R(x) = (n * i) + r
        # where n is the order of the curve and R is from the signature.
        r_x = @n * Integer.floor_div(recoveryId, 2) + sig.r

        # Check that R(x) is on the curve.
        if r_x > Params.curve().p do
          {:error, "R(x) is not on the curve"}
        else
          # Decompress to get R(y).
          case Secp256k1.get_y(r_x, rem(recoveryId, 2) == 1) do
            {:ok, r_y} ->
              # R(x,y)
              point_r = %Point{x: r_x, y: r_y}

              # Point Q is the recovered public key.
              # We satisfy this equation: Q = r^-1(sR-eG)
              inv_r = Math.inv(sig.r, @n)
              inv_r_s = (inv_r * sig.s) |> Math.modulo(@n)

              # R*s
              point_sr = Math.multiply(point_r, inv_r_s)

              # Find e using the message hash.
              e =
                :binary.decode_unsigned(msg)
                |> Kernel.*(-1)
                |> Math.modulo(@n)
                |> Kernel.*(inv_r |> Math.modulo(@n))

              # G*e
              point_ge = Math.multiply(@generator_point, e)

              # R*e * G*e
              point_q = Math.add(point_sr, point_ge)

              # Returns serialized compressed public key.
              {:ok, Point.serialize_public_key(point_q)}

            {:error, error} ->
              {:error, error}
          end
        end

      {:error, e} ->
        {:error, e}
    end
  end
end