lib/key/private_key/child_from_index.ex

defmodule BitcoinLib.Key.PrivateKey.ChildFromIndex do
  @moduledoc """
  Calculates direct childs from a private key based on a given index, and maybe a hardened flag
  """

  alias BitcoinLib.Key.PrivateKey
  alias BitcoinLib.Key.HD.{Fingerprint, Hmac}

  @max_index 0x7FFFFFFF
  @hardened 0x80000000

  # this is n, as found here https://en.bitcoin.it/wiki/Secp256k1
  @order_of_the_curve 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141

  @doc """
  Calculates a direct child from a private key based on a given index, and maybe a hardened flag

  ## Examples
      iex> %BitcoinLib.Key.PrivateKey{
      ...>  key: <<0xF79BB0D317B310B261A55A8AB393B4C8A1ABA6FA4D08AEF379CABA502D5D67F9::256>>,
      ...>  chain_code: <<0x463223AAC10FB13F291A1BC76BC26003D98DA661CB76DF61E750C139826DEA8B::256>>
      ...> }
      ...> |> BitcoinLib.Key.PrivateKey.ChildFromIndex.get(0)
      {
        :ok,
        %BitcoinLib.Key.PrivateKey{
          key: <<0x39F329FEDBA2A68E2A804FCD9AEEA4104ACE9080212A52CE8B52C1FB89850C72::256>>,
          chain_code: <<0x05AAE71D7C080474EFAAB01FA79E96F4C6CFE243237780B0DF4BC36106228E31::256>>,
          depth: 1,
          index: 0,
          parent_fingerprint: <<0x18C1259::32>>
        }
      }
  """
  @spec get(%PrivateKey{}, integer(), boolean()) ::
          {:ok, %PrivateKey{}} | {:error, binary()}
  def get(private_key, index, hardened? \\ false)

  def get(_, index, _) when is_integer(index) and index > @max_index do
    {:error, "#{index} is too large of an index"}
  end

  def get(%PrivateKey{} = private_key, index, hardened?) when is_integer(index) do
    index =
      case hardened? do
        true -> @hardened + index
        false -> index
      end

    %{child_private_key: child_private_key} =
      %{parent_private_key: private_key, index: index, hardened?: hardened?}
      |> compute_hmac
      |> compute_parent_fingerprint
      |> compute_child_private_key

    {:ok, child_private_key}
  end

  # hmac_left_part and hmac_right_part are Il and Ir in slip-0010 as found here
  # https://github.com/satoshilabs/slips/blob/master/slip-0010.md#master-key-generation
  defp compute_hmac(
         %{index: index, parent_private_key: parent_private_key, hardened?: hardened?} = hash
       ) do
    {derived_key, child_chain} = Hmac.compute(parent_private_key, index, hardened?)

    hash
    |> Map.put(:hmac_derived_key, derived_key)
    |> Map.put(:child_chain_code, child_chain)
  end

  defp compute_parent_fingerprint(%{parent_private_key: private_key} = hash) do
    hash
    |> Map.put(:parent_fingerprint, Fingerprint.compute(private_key))
  end

  defp compute_child_private_key(
         %{
           parent_private_key: parent_private_key,
           index: index,
           hmac_derived_key: hmac_derived_key,
           child_chain_code: child_chain_code,
           parent_fingerprint: parent_fingerprint
         } = hash
       ) do
    child_private_key =
      (Binary.to_integer(hmac_derived_key) + Binary.to_integer(parent_private_key.key))
      |> rem(@order_of_the_curve)
      |> Binary.from_integer()

    hash
    |> Map.put(:child_private_key, %PrivateKey{
      key: child_private_key,
      chain_code: child_chain_code,
      depth: parent_private_key.depth + 1,
      index: index,
      parent_fingerprint: parent_fingerprint
    })
  end
end