defmodule Bitcoinex.Secp256k1.Point do
@moduledoc """
Contains the x, y, and z of an elliptic curve point.
"""
import Bitwise
alias Bitcoinex.Utils
alias Bitcoinex.Secp256k1
alias Bitcoinex.Secp256k1.Params
@p Params.curve().p
@type t :: %__MODULE__{
x: integer(),
y: integer(),
z: integer()
}
@enforce_keys [
:x,
:y
]
defstruct [:x, :y, z: 0]
defguard is_point(term)
when is_map(term) and :erlang.map_get(:__struct__, term) == __MODULE__ and
:erlang.is_map_key(:x, term) and :erlang.is_map_key(:y, term) and
:erlang.is_map_key(:z, term)
@doc """
is_inf returns whether or not point P is
the point at infinity, ie. P.x == P.y == 0
"""
@spec is_inf(t()) :: boolean
def is_inf(%__MODULE__{x: 0, y: 0}), do: true
def is_inf(_), do: false
@doc """
parse_public_key parses a public key
"""
@spec parse_public_key(binary) :: {:ok, t()} | {:error, String.t()}
def parse_public_key(<<0x04, x::binary-size(32), y::binary-size(32)>>) do
{:ok, %__MODULE__{x: :binary.decode_unsigned(x), y: :binary.decode_unsigned(y)}}
end
# Above matches with uncompressed keys. Below matches with compressed keys
def parse_public_key(<<prefix::binary-size(1), x_bytes::binary-size(32)>>) do
x = :binary.decode_unsigned(x_bytes)
case :binary.decode_unsigned(prefix) do
2 ->
case Bitcoinex.Secp256k1.get_y(x, false) do
{:ok, y} -> {:ok, %__MODULE__{x: x, y: y}}
_ -> {:error, "invalid public key"}
end
3 ->
case Bitcoinex.Secp256k1.get_y(x, true) do
{:ok, y} -> {:ok, %__MODULE__{x: x, y: y}}
_ -> {:error, "invalid public key"}
end
end
end
# Allow parse_public_key to parse SEC strings
def parse_public_key(key) do
key
|> String.downcase()
|> Base.decode16!(case: :lower)
|> parse_public_key()
end
@doc """
lift_x returns the Point P where P.x = x
and P.y is even.
"""
@spec lift_x(integer | binary) :: {:ok, t()} | {:error, String.t()}
def lift_x(x) when is_integer(x) and x >= @p, do: {:error, "invalid x value (too large)"}
def lift_x(x) when is_integer(x) do
case Secp256k1.get_y(x, false) do
{:ok, y} ->
{:ok, %__MODULE__{x: x, y: y}}
err ->
err
end
end
# parse 32-byte binary
def lift_x(<<x::binary-size(32)>>) do
x
|> :binary.decode_unsigned()
|> lift_x
end
# attempt to parse x-only pubkey from hex
def lift_x(x) when is_binary(x) do
case Utils.hex_to_bin(x) do
{:error, msg} ->
{:error, msg}
x_bytes ->
lift_x(x_bytes)
end
end
@doc """
sec serializes a compressed public key to binary
"""
@spec sec(t()) :: binary
def sec(%__MODULE__{x: x, y: y}) do
case rem(y, 2) do
0 ->
<<0x02>> <> Bitcoinex.Utils.pad(:binary.encode_unsigned(x), 32, :leading)
1 ->
<<0x03>> <> Bitcoinex.Utils.pad(:binary.encode_unsigned(x), 32, :leading)
end
end
@doc """
x_bytes returns the binary encoding of the x value of the point
"""
@spec x_bytes(t()) :: binary
def x_bytes(%__MODULE__{x: x}) do
Bitcoinex.Utils.pad(:binary.encode_unsigned(x), 32, :leading)
end
@doc """
x_hex returns the hex-encoded x value of the point
"""
@spec x_hex(t()) :: String.t()
def x_hex(p) do
p
|> x_bytes()
|> Base.encode16(case: :lower)
end
@doc """
serialize_public_key serializes a compressed public key to string
"""
@spec serialize_public_key(t()) :: String.t()
def serialize_public_key(pubkey) do
pubkey
|> sec()
|> Base.encode16(case: :lower)
end
@doc """
has_even_y returns true if y is
even and false if y is odd
"""
@spec has_even_y(t()) :: boolean
def has_even_y(%__MODULE__{y: y}) do
(y &&& 1) == 0
end
end