diff --git a/lib/descriptor.ex b/lib/descriptor.ex new file mode 100644 index 0000000..f9f1726 --- /dev/null +++ b/lib/descriptor.ex @@ -0,0 +1,541 @@ +defmodule Bitcoinex.Descriptor do + @moduledoc """ + Module for using Descriptors + + types of descriptors: + + :pkh (%DKEY) + :sh (%DESCRIPTOR) # recursive + :wpkh (%DKEY) + :wsh (%DESCRIPTOR) # recursive + :pk (%DKEY) + :combo (%DKEY) + :multi (m, [%DKEY...]) + :sortedmulti (m, [%DKEY...]) + :addr (str) + :raw (str) + """ + alias Bitcoinex.{ + Script, + ExtendedKey, + Network, + ExtendedKey.DerivationPath, + Secp256k1.PrivateKey, + Secp256k1.Point + } + + # TODO can't create DKey straight from WIF + defmodule DKey do + # WIF encoding requires network info + @type key_type :: + ExtendedKey.t() | {PrivateKey.t(), Network.network_name()} | Point.t() | String.t() + + @type t :: %__MODULE__{ + key: key_type, + parent_fingerprint: binary, + ancestor_path: DerivationPath.t(), + descendant_path: DerivationPath.t() + } + + @enforce_keys [ + :key + ] + + defstruct [ + # default both to empty for easier serialization + :key, + parent_fingerprint: <<>>, + ancestor_path: DerivationPath.new(), + descendant_path: DerivationPath.new() + ] + + def get_type(%__MODULE__{key: %ExtendedKey{}}), do: :extended_key + def get_type(%__MODULE__{key: %Point{}}), do: :public_key + def get_type(%__MODULE__{key: {%PrivateKey{}, _}}), do: :private_key + def get_type(%__MODULE__{key: key}) when is_binary(key), do: :address + def get_type(_), do: :invalid_key + + def is_valid?(dkey) do + case get_type(dkey) do + # TODO: maybe expand later + :extended_key -> true + :public_key -> true + :private_key -> true + :address -> true + :invalid_key -> false + end + end + + # used by descriptor module to make all keys into dkeys. + # if dkey is passed, returns identity + def from_key(dkey = %__MODULE__{}), do: dkey + def from_key(pk = %Point{}), do: %__MODULE__{key: pk} + def from_key(xkey = %ExtendedKey{}), do: %__MODULE__{key: xkey} + def from_key(addr) when is_binary(addr), do: %__MODULE__{key: addr} + def from_key(_), do: {:error, "invalid key"} + + def from_key(sk = %PrivateKey{}, network), do: from_private_key(sk, network) + + def from_key(xkey = %ExtendedKey{}, pathdata) do + defaults = %{ + parent_fingerprint: <<>>, + anc_path: DerivationPath.new(), + desc_path: DerivationPath.new() + } + + data = Map.merge(defaults, pathdata) + from_extended_key(xkey, data.parent_fingerprint, data.anc_path, data.desc_path) + end + + def from_key(_, _), do: {:error, "invalid key"} + + @spec from_private_key(PrivateKey.t(), Network.network_name()) :: t() + def from_private_key(sk = %PrivateKey{}, network) do + # ensure valid network is passed + _ = Network.get_network(network) + %__MODULE__{key: {sk, network}} + end + + def from_extended_key( + xkey = %ExtendedKey{}, + parent_fingerprint, + anc_path = %DerivationPath{}, + desc_path = %DerivationPath{} + ) do + %__MODULE__{ + key: xkey, + parent_fingerprint: parent_fingerprint, + ancestor_path: anc_path, + descendant_path: desc_path + } + end + + def serialize(desc = %__MODULE__{key: key}) do + fp = Base.encode16(desc.parent_fingerprint, case: :lower) + {:ok, anc_path} = DerivationPath.to_string(desc.ancestor_path) + {:ok, desc_path} = DerivationPath.to_string(desc.descendant_path) + + serialized_key = serialize_key(key) + + case fp <> handle_slashes(anc_path) do + "" -> + serialized_key <> handle_slashes(desc_path) + + origin -> + "[" <> origin <> "]" <> serialized_key <> handle_slashes(desc_path) + end + end + + defp serialize_key(key = %ExtendedKey{}), do: ExtendedKey.to_string(key) + defp serialize_key({sk = %PrivateKey{}, network}), do: PrivateKey.wif!(sk, network) + defp serialize_key(key = %Point{}), do: Point.sec(key) |> Base.encode16(case: :lower) + + defp handle_slashes(""), do: "" + + defp handle_slashes(deriv_str) do + case String.split_at(deriv_str, -1) do + {deriv, "/"} -> "/" <> deriv + _ -> "/" <> deriv_str + end + end + + def parse(hex_data) do + try do + {:ok, parser(hex_data)} + rescue + _ -> {:error, "failed to parse descriptor key"} + end + end + + def parser(hex_data) do + if String.first(hex_data) == "[" do + {:ok, fp, anc_path, remaining} = parse_ancestor_data(hex_data) + {:ok, key, desc_path} = parse_key_data(remaining) + + %__MODULE__{ + key: key, + parent_fingerprint: fp, + ancestor_path: anc_path, + descendant_path: desc_path + } + else + {:ok, key, desc_path} = parse_key_data(hex_data) + + %__MODULE__{key: key, descendant_path: desc_path} + end + end + + defp parse_ancestor_data("[" <> hex_data) do + [anc_data, remaining] = String.split(hex_data, "]", parts: 2) + [fp | tail] = String.split(anc_data, "/", parts: 2) + + <> = + fp + |> String.downcase() + |> Base.decode16!(case: :lower) + + # ancestor path is optional + case tail do + [deriv] -> + {:ok, anc_path} = DerivationPath.from_string(deriv) + {:ok, fingerprint, anc_path, remaining} + + [] -> + {:ok, fingerprint, DerivationPath.new(), remaining} + end + end + + defp parse_ancestor_data(_), do: raise(ArgumentError) + + defp parse_key_data(hex_data) do + case String.first(hex_data) do + # extended key + "x" -> + case String.split(hex_data, "/", parts: 2) do + [xkey] -> + {:ok, xkey} = ExtendedKey.parse_extended_key(xkey) + {:ok, xkey, DerivationPath.new()} + + [xkey, desc_str] -> + {:ok, xkey} = ExtendedKey.parse_extended_key(xkey) + {:ok, desc_path} = DerivationPath.from_string(desc_str) + {:ok, xkey, desc_path} + end + + # public key + "0" -> + # since uncompressed pubkeys are not allowed for sh and wpkh, + # we disallow them universally + if String.length(hex_data) != 66 do + {:error, "public key must be compressed (33bytes)"} + else + {:ok, pk} = Point.parse_public_key(hex_data) + {:ok, pk, DerivationPath.new()} + end + + # private key + _ -> + # WARNING: only accepts compressed private keys + {:ok, sk, network, true} = PrivateKey.parse_wif(hex_data) + {:ok, {sk, network}, DerivationPath.new()} + end + end + end + + @type descriptor_type :: + :pk | :pkh | :sh | :wpkh | :wsh | :combo | :multi | :sortedmulti | :addr | :raw + @descriptor_types ~w(pk pkh sh wpkh wsh combo multi sortedmulti addr raw)a + @top_level_only [:sh, :combo, :addr, :raw] + + @type t :: %__MODULE__{ + script_type: descriptor_type, + data: t() | DKey.t() | Script.t() | {non_neg_integer(), list(DKey.t())} | binary + } + @enforce_keys [ + :script_type, + :data + ] + defstruct [ + :script_type, + :data + ] + + def parse_descriptor(desc) do + try do + parser(desc) + rescue + _ -> {:error, "invalid descriptor"} + end + end + + defp parser(desc) do + case split_descriptor(desc) do + # sh & wsh can be recursive + {:ok, :sh, rest} -> + {:ok, inner} = parser(rest) + create_p2sh(inner) + + {:ok, :wsh, rest} -> + {:ok, inner} = parser(rest) + create_p2wsh(inner) + + {:ok, :multi, rest} -> + {:ok, inner} = parse_multi(rest) + create_multi(inner) + + {:ok, :sortedmulti, rest} -> + {:ok, inner} = parse_multi(rest) + create_sortedmulti(inner) + + {:ok, :raw, rest} -> + create_raw(rest) + + {:ok, :addr, rest} -> + create_addr(rest) + + {:ok, script_type, rest} -> + {:ok, dkey} = DKey.parse(rest) + create_descriptor(script_type, dkey) + + {:error, _msg} -> + raise ArgumentError + end + end + + defp split_descriptor(desc) do + [s_type, rest] = String.split(desc, "(", parts: 2) + + case String.split_at(rest, -1) do + {rest, ")"} -> + if String.to_atom(s_type) in @descriptor_types do + {:ok, String.to_atom(s_type), rest} + else + {:error, "invalid descriptor"} + end + + _ -> + {:error, "invalid descriptor"} + end + end + + defp parse_multi(multi_str) do + [m | keys] = String.split(multi_str, ",") + dkeys = parse_dkeys(keys) + {:ok, {String.to_integer(m), dkeys}} + end + + defp parse_dkeys([]), do: [] + + defp parse_dkeys([dstr | rest]) do + case DKey.parse(dstr) do + {:ok, dkey} -> [dkey | parse_dkeys(rest)] + {:error, "invalid key"} -> raise ArgumentError + end + end + + @spec serialize_descriptor(t()) :: String.t() + def serialize_descriptor(%__MODULE__{script_type: st, data: data}) do + cond do + st in [:sh, :wsh] -> serialize_recursive(st, data) + st in [:multi, :sortedmulti] -> serialize_multi(st, data) + st in [:pk, :pkh, :wpkh, :combo] -> serialize_key_descriptor(st, data) + st in [:addr, :raw] -> serialize_simple(st, data) + end + end + + defp serialize_recursive(script_type, data) do + to_string(script_type) <> "(" <> serialize_descriptor(data) <> ")" + end + + defp serialize_key_descriptor(script_type, dkey) do + to_string(script_type) <> "(" <> DKey.serialize(dkey) <> ")" + end + + defp serialize_multi(script_type, {m, dkeys}) do + to_string(script_type) <> "(#{m}," <> serialize_dkeys(dkeys) <> ")" + end + + defp serialize_dkeys(dkeys) do + dkeys + |> Enum.map(&DKey.serialize/1) + |> Enum.join(",") + end + + defp serialize_simple(script_type, data) do + to_string(script_type) <> "(#{data})" + end + + @spec get_script_type(t()) :: Script.script_type() + def get_script_type(descriptor) do + case descriptor.script_type do + :pk -> + :p2pk + + :pkh -> + :p2pkh + + :sh -> + :p2sh + + :wpkh -> + :p2wpkh + + :wsh -> + :p2wsh + + :combo -> + :non_standard + + :multi -> + :multi + + :sortedmulti -> + :multi + + :addr -> + case Script.from_address(descriptor.data) do + {:ok, script, _network} -> Script.get_script_type(script) + {:error, _msg} -> :non_standard + end + # return exact address script type + :raw -> + case Script.parse_script(descriptor.data) do + {:ok, script} -> Script.get_script_type(script) + {:error, _msg} -> :non_standard + end + end + end + + @spec create_descriptor(atom, DKey.t()) :: {:ok, t()} | {:error, String.t()} + def create_descriptor(dtype, dkey) do + case dtype do + :sh -> create_p2sh(dkey) + :wsh -> create_p2wsh(dkey) + :pk -> create_p2pk(dkey) + :pkh -> create_p2pkh(dkey) + :wpkh -> create_p2wpkh(dkey) + :combo -> create_combo(dkey) + :addr -> create_addr(dkey) + :raw -> create_raw(dkey) + _ -> {:error, "unknown script type"} + end + end + + @spec create_descriptor(atom, list(DKey.t())) :: {:ok, t()} | {:error, String.t()} + def create_descriptor(dtype, m, dkeys) do + case dtype do + :multi -> create_multi({m, dkeys}) + :sortedmulti -> create_sortedmulti({m, dkeys}) + _ -> {:error, "unknown script type"} + end + end + + # Allow users to easily set an xpub, origin and desc info. this is a weird way + + @spec create_p2pk(DKey.key_type()) :: {:ok, t()} | {:error, String.t()} + def create_p2pk(key) do + try do + {:ok, %__MODULE__{script_type: :pk, data: DKey.from_key(key)}} + rescue + _ -> {:error, "invalid key"} + end + end + + @spec create_p2pkh(DKey.key_type()) :: {:ok, t()} | {:error, String.t()} + def create_p2pkh(key) do + try do + {:ok, %__MODULE__{script_type: :pkh, data: DKey.from_key(key)}} + rescue + _ -> {:error, "invalid key"} + end + end + + @spec create_p2sh(t()) :: {:ok, t()} | {:error, String.t()} + def create_p2sh(descriptor = %__MODULE__{script_type: st}) when st not in @top_level_only do + {:ok, %__MODULE__{script_type: :sh, data: descriptor}} + end + + def create_p2sh(_), do: {:error, "p2sh descriptors can only contain descriptors."} + + @spec create_p2wsh(t()) :: {:ok, t()} | {:error, String.t()} + def create_p2wsh(descriptor = %__MODULE__{script_type: st}) + when st not in [:wsh | [:wpkh | @top_level_only]] do + try do + {:ok, %__MODULE__{script_type: :wsh, data: descriptor}} + rescue + _ -> {:error, "invalid script"} + end + end + + @spec create_p2wpkh(DKey.key_type()) :: {:ok, t()} | {:error, String.t()} + def create_p2wpkh(key) do + try do + {:ok, %__MODULE__{script_type: :wpkh, data: DKey.from_key(key)}} + rescue + _ -> {:error, "invalid key"} + end + end + + @spec create_combo(DKey.key_type()) :: {:ok, t()} | {:error, String.t()} + def create_combo(key) do + try do + {:ok, %__MODULE__{script_type: :combo, data: DKey.from_key(key)}} + rescue + _ -> {:error, "invalid key"} + end + end + + @spec create_multi({non_neg_integer(), list(DKey.key_type())}) :: + {:ok, t()} | {:error, String.t()} + def create_multi({m, keys}) do + try do + {:ok, %__MODULE__{script_type: :multi, data: {m, Enum.map(keys, &DKey.from_key/1)}}} + rescue + _ -> {:error, "invalid keys present. All keys must be DKey type"} + end + end + + @spec create_sortedmulti({non_neg_integer(), list(DKey.key_type())}) :: + {:ok, t()} | {:error, String.t()} + def create_sortedmulti({m, keys}) do + try do + {:ok, %__MODULE__{script_type: :sortedmulti, data: {m, Enum.map(keys, &DKey.from_key/1)}}} + rescue + _ -> {:error, "invalid keys present. All keys must be DKey type"} + end + end + + @spec create_addr(String.t()) :: {:ok, t()} | {:error, String.t()} + def create_addr(addr_str) do + # TODO switch this to `Address.is_valid?(:testnet) || Address.is_valid?(:mainnet) || Address.is_valid?(:regtest) + case Script.from_address(addr_str) do + # check address is valid agnostic of network + {:ok, _script, _network} -> {:ok, %__MODULE__{script_type: :addr, data: addr_str}} + {:error, _msg} -> {:error, "invalid address"} + end + end + + @spec create_raw(String.t()) :: {:ok, t()} | {:error, String.t()} + def create_raw(hex_str) do + case Script.parse_script(hex_str) do + # check script is well-formed. + {:ok, script} -> {:ok, %__MODULE__{script_type: :raw, data: script}} + {:error, _msg} -> {:error, "invalid script"} + end + end + + # p2pk + @spec derive_script(t(), Bitcoinex.Network.network_name(), list(non_neg_integer())) :: {:ok, Script.t()} | {:error, String.t()} + def derive_script(%__MODULE__{script_type: :pk, data: data}, _network, indexes) do + case derive_key(data, indexes) do + {:ok, key} -> + Script.create_p2pk(key) + {:error, err} -> + {:error, err} + end + end + + # p2pk + @spec derive_address(t(), Bitcoinex.Network.network_name(), list(non_neg_integer())) :: {:ok, String.t()} | {:error, String.t()} + def derive_address(%__MODULE__{script_type: :pk, data: data}, _network, indexes) do + case derive_key(data, indexes) do + {:ok, key} -> + {:ok, Point.serialize_public_key(key)} + {:error, err} -> + {:error, err} + end + end + + # p2pkh, p2wpkh, p2sh, p2wsh + def derive_address(desc = %__MODULE__{}, network, indexes) do + case derive_script(desc, network, indexes) do + {:ok, script} -> + Script.to_address(script, network) + {:error, err} -> + {:error, err} + end + end + + # address + def derive_address(%__MODULE__{script_type: :addr, data: data}, _), do: {:ok, data.key} +end diff --git a/lib/extendedkey.ex b/lib/extendedkey.ex index d3f9824..3f54cb0 100644 --- a/lib/extendedkey.ex +++ b/lib/extendedkey.ex @@ -9,7 +9,7 @@ defmodule Bitcoinex.ExtendedKey do defmodule DerivationPath do @moduledoc """ - Contains a list of integers (or the :any atom) representing a bip32 derivation path. + Contains a list of integers (or the :any atom) representing a bip32 derivation path. The :any atom represents a wildcard in the derivation path. DerivationPath structs can be used by ExtendedKey.derive_extended_key to derive a child key at the given path. """ @@ -202,7 +202,7 @@ defmodule Bitcoinex.ExtendedKey do end @doc """ - network_from_extended_key returns :testnet or :mainnet + network_from_extended_key returns :testnet or :mainnet depending on the network prefix of the key. """ @spec network_from_extended_key(t()) :: atom @@ -242,10 +242,10 @@ defmodule Bitcoinex.ExtendedKey do @spec get_child_num(t()) :: binary def get_child_num(%__MODULE__{child_num: child_num}), do: child_num - # PARSE & SERIALIZE + # PARSE & SERIALIZE @doc """ - parse_extended_key takes binary or string representation + parse_extended_key takes binary or string representation of an extended key and parses it to an extended key object """ @spec parse_extended_key(binary) :: {:ok, t()} | {:error, String.t()} @@ -316,15 +316,15 @@ defmodule Bitcoinex.ExtendedKey do @doc """ display returns the extended key as a string """ - @spec display_extended_key(t()) :: String.t() - def display_extended_key(xkey) do + @spec to_string(t()) :: String.t() + def to_string(xkey) do xkey |> serialize_extended_key() |> Base58.encode_base() end @doc """ - seed_to_master_private_key transforms a bip32 seed + seed_to_master_private_key transforms a bip32 seed into a master extended private key """ @spec seed_to_master_private_key(binary, atom) :: {:ok, t()} | {:error, String.t()} @@ -393,7 +393,7 @@ defmodule Bitcoinex.ExtendedKey do end @doc """ - to_public_key takes an extended key xkey and + to_public_key takes an extended key xkey and returns the public key. """ @spec to_public_key(t()) :: {:ok, Point.t()} | {:error, String.t()} @@ -410,7 +410,7 @@ defmodule Bitcoinex.ExtendedKey do @doc """ derive_child uses a public or private key xkey to - derive the public or private key at index idx. + derive the public or private key at index idx. public key -> public child private key -> private child """ @@ -482,8 +482,8 @@ defmodule Bitcoinex.ExtendedKey do end @doc """ - derive_private_child uses a private key xkey to - derive the private key at index idx + derive_private_child uses a private key xkey to + derive the private key at index idx """ @spec derive_private_child(t(), non_neg_integer()) :: {:ok, t()} | {:error, String.t()} def derive_private_child(_, idx) when idx >>> 32 != 0, do: {:error, "idx must be in 0..2**32-1"} @@ -573,7 +573,7 @@ defmodule Bitcoinex.ExtendedKey do end @doc """ - derive_extended_key uses an extended xkey and a derivation + derive_extended_key uses an extended xkey and a derivation path to derive the extended key at that path """ @spec derive_extended_key(t() | binary, DerivationPath.t()) :: {:ok, t()} | {:error, String.t()} diff --git a/lib/script.ex b/lib/script.ex index b58c58e..e5d79f9 100644 --- a/lib/script.ex +++ b/lib/script.ex @@ -68,7 +68,7 @@ defmodule Bitcoinex.Script do end @doc """ - hash160 is a helper function which returns the hash160 + hash160 is a helper function which returns the hash160 digest of the serialized script, as used in P2SH scripts. """ @spec hash160(t()) :: binary @@ -102,7 +102,7 @@ defmodule Bitcoinex.Script do def get_op_atom(i), do: if(i > 0 and i < 0x4C, do: i, else: Map.fetch(opcode_nums(), i)) @doc """ - pop returns the first element of the script and the remaining script. + pop returns the first element of the script and the remaining script. Returns nil if script is empty """ @spec pop(t()) :: nil | {:ok, non_neg_integer() | binary, t()} @@ -132,7 +132,7 @@ defmodule Bitcoinex.Script do end @doc """ - push_data returns a script with the binary data and any + push_data returns a script with the binary data and any accompanying pushdata or pushbytes opcodes added to the front of the script. """ @spec push_data(t(), binary) :: {:ok, t()} | {:error, String.t()} @@ -158,7 +158,7 @@ defmodule Bitcoinex.Script do end end - # SERIALIZE & PARSE + # SERIALIZE & PARSE defp serializer(%__MODULE__{items: []}, acc), do: acc defp serializer(%__MODULE__{items: [item | script]}, acc) when is_integer(item) do @@ -197,18 +197,18 @@ defmodule Bitcoinex.Script do end @doc """ - serialize_script serializes the script into binary + serialize_script serializes the script into binary according to Bitcoin's standard. """ @spec serialize_script(t()) :: binary def serialize_script(script = %__MODULE__{}) do - # serialize_script(%Script{items: [0x51]}) will still display "Q" but + # serialize_script(%Script{items: [0x51]}) will still display "Q" but # it functions as binary 0x51. Use to_hex for displaying scripts. serializer(script, <<>>) end @doc """ - to_hex returns the hex of a serialized script. + to_hex returns the hex of a serialized script. """ @spec to_hex(t()) :: String.t() def to_hex(script) do @@ -412,7 +412,7 @@ defmodule Bitcoinex.Script do defp test_multi(_, _, _), do: false @doc """ - extract_multi_policy takes in a raw multisig script and returns the m, the + extract_multi_policy takes in a raw multisig script and returns the m, the number of signatures required and the n authorized public keys. """ @spec extract_multi_policy(t()) :: @@ -468,8 +468,9 @@ defmodule Bitcoinex.Script do @doc """ create_p2pkh creates a p2pkh script using the passed 20-byte public key hash + or a public key """ - @spec create_p2pkh(binary) :: {:ok, t()} | {:error, String.t()} + @spec create_p2pkh(binary | Point.t()) :: {:ok, t()} | {:error, String.t()} def create_p2pkh(<>) do {:ok, s} = push_op(new(), 0xAC) {:ok, s} = push_op(s, 0x88) @@ -478,6 +479,12 @@ defmodule Bitcoinex.Script do push_op(s, 0x76) end + def create_p2pkh(p = %Point{}) do + p + |> public_key_hash() + |> create_p2pkh() + end + def create_p2pkh(_), do: {:error, "pubkey hash must be a #{@h160_length}-byte hash"} @doc """ @@ -520,7 +527,7 @@ defmodule Bitcoinex.Script do defp fill_multi_keys(_, _), do: raise(ArgumentError) @doc """ - create_p2sh_multi returns both a P2SH-wrapped multisig script + create_p2sh_multi returns both a P2SH-wrapped multisig script and the underlying raw multisig script using m and the list of public keys. """ @spec create_p2sh_multi(non_neg_integer(), list(Point.t())) :: @@ -538,7 +545,7 @@ defmodule Bitcoinex.Script do end @doc """ - create_p2wsh_multi returns both a P2WSH-wrapped multisig script + create_p2wsh_multi returns both a P2WSH-wrapped multisig script and the underlying raw multisig script using m and the list of public keys. """ @spec create_p2wsh_multi(non_neg_integer(), list(Point.t())) :: @@ -557,7 +564,7 @@ defmodule Bitcoinex.Script do @doc """ create_witness_scriptpubkey creates any witness script from a witness version - and witness program. It performs no validity checks. + and witness program. It performs no validity checks. """ @spec create_witness_scriptpubkey(non_neg_integer(), binary) :: {:ok, t()} def create_witness_scriptpubkey(version, witness_program) do @@ -573,7 +580,13 @@ defmodule Bitcoinex.Script do def create_p2wpkh(<>), do: create_witness_scriptpubkey(0, pkh) - def create_p2wpkh(_), do: {:error, "pubkey hash must be a #{@h160_length}-byte hash"} + def create_p2wpkh(p = %Point{}) do + p + |> public_key_hash() + |> create_p2wpkh() + end + + def create_p2wpkh(_) , do: {:error, "pubkey hash must be a #{@h160_length}-byte hash"} @doc """ create_p2wsh creates a p2wsh script using the passed 32-byte script hash @@ -606,12 +619,16 @@ defmodule Bitcoinex.Script do {:ok, p2sh, p2wpkh} end - def create_p2sh_p2wpkh(_), do: {:error, "public key hash must be #{@h160_length}-bytes"} + def create_p2sh_p2wpkh(p = %Point{}) do + p + |> public_key_hash() + |> create_p2sh_p2wpkh() + end - # CREATE SCRIPTS FROM PUBKEYS + def create_p2sh_p2wpkh(_), do: {:error, "public key hash must be #{@h160_length}-bytes"} @doc """ - public_key_hash takes the hash160 of the public key's compressed sec encoding. + public_key_hash takes the hash160 of the public key's compressed sec encoding. Can be used to create a pkh script. """ @spec public_key_hash(Point.t()) :: binary @@ -621,45 +638,6 @@ defmodule Bitcoinex.Script do |> Utils.hash160() end - @doc """ - public_key_to_p2pkh creates a p2pkh script from a public key. - All public keys are compressed. - """ - @spec public_key_to_p2pkh(Point.t()) :: {:ok, t()} - def public_key_to_p2pkh(p = %Point{}) do - p - |> public_key_hash() - |> create_p2pkh() - end - - def public_key_to_p2pkh(_), do: {:error, "invalid public key"} - - @doc """ - public_key_to_p2wpkh creates a p2wpkh script from a public key. - All public keys are compressed. - """ - @spec public_key_to_p2wpkh(Point.t()) :: {:ok, t()} - def public_key_to_p2wpkh(p = %Point{}) do - p - |> public_key_hash() - |> create_p2wpkh() - end - - def public_key_to_p2wpkh(_), do: {:error, "invalid public key"} - - @doc """ - public_key_to_p2sh_p2wpkh creates a p2sh-p2wpkh script from a public key. - All public keys are compressed. - """ - @spec public_key_to_p2sh_p2wpkh(Point.t()) :: {:ok, t(), t()} - def public_key_to_p2sh_p2wpkh(p = %Point{}) do - p - |> public_key_hash() - |> create_p2sh_p2wpkh() - end - - def public_key_to_p2sh_p2wpkh(_), do: {:error, "invalid public key"} - # ADDRESS CREATION & DECODING @doc """ @@ -669,7 +647,7 @@ defmodule Bitcoinex.Script do {:error, String.t()} | {:ok, t(), Bitcoinex.Network.network_name()} def from_address(addr) do case String.slice(addr, 0, 2) do - # segwit addresses + # segwit addresses p when p in ["bc", "tb"] -> case Segwit.decode_address(addr) do {:ok, {network, version, program}} -> @@ -742,13 +720,23 @@ defmodule Bitcoinex.Script do {:ok, <>, _script} = pop(script) Segwit.encode_address(network, 1, :binary.bin_to_list(res)) + # p2pk + 0x41 -> + {:ok, pubkey_length, script} = pop(script) + if pubkey_length not in @pubkey_lengths do + {:error, "invalid pubkey length"} + else + {:ok, pubkey, _script} = pop(script) + {:ok, Base.encod16(pubkey, case: :lower)} + end + # p2sh 0xA9 -> {:ok, @h160_length, script} = pop(script) {:ok, <>, _script} = pop(script) {:ok, Address.encode(res, network, :p2sh)} - # p2pkh + # p2pkh 0x76 -> {:ok, 0xA9, script} = pop(script) {:ok, @h160_length, script} = pop(script) diff --git a/lib/secp256k1/privatekey.ex b/lib/secp256k1/privatekey.ex index bd8e719..527cf03 100644 --- a/lib/secp256k1/privatekey.ex +++ b/lib/secp256k1/privatekey.ex @@ -92,8 +92,9 @@ defmodule Bitcoinex.Secp256k1.PrivateKey do @doc """ returns the base58check encoded private key as a string assumes all keys are compressed + TODO: can this handle uncompressed WIFs? """ - @spec parse_wif(String.t()) :: {:ok, t(), atom, boolean} + @spec parse_wif(String.t()) :: {:ok, t(), atom, boolean} | {:error, String.t()} def parse_wif(wif_str) do {state, bin} = Base58.decode(wif_str) @@ -195,8 +196,8 @@ defmodule Bitcoinex.Secp256k1.PrivateKey do @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. + where privkey is a PrivateKey object and z is an integer. + The nonce is derived using RFC6979. """ @spec sign(t(), integer) :: Signature.t() def sign(privkey, z) do diff --git a/test/descriptor_test.exs b/test/descriptor_test.exs new file mode 100644 index 0000000..54b797c --- /dev/null +++ b/test/descriptor_test.exs @@ -0,0 +1,504 @@ +defmodule Bitcoinex.DescriptorTest do + use ExUnit.Case + doctest Bitcoinex.Descriptor + + alias Bitcoinex.{ + Descriptor, + ExtendedKey.DerivationPath, + ExtendedKey, + Secp256k1.Point, + Secp256k1.PrivateKey + } + + # 0-15 are from https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md + # >15 are original examples and edge cases + + @pk_descriptors [ + # 0 describes a P2PK output with the specified public key. + "pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)", + # 11 describes a P2PK output with the public key of the specified xpub. + "pk(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)", + # 16 describes a P2PK to a mainnet private key + "pk(KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E)", + # 17 describes a P2PK to a testnet private key + "pk(cMahea7zqjxrtgAbB7LSGbcQUr1uX1okdzTtkBA29TFk41r74ddm)", + # 18 describes a P2PK to a mainnet private key with HD origin info + "pk([d34db33f]KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E)", + # 19 describes a P2PK to a mainnet private key with HD origin and ancestor path info + "pk([d34db33f/44'/0'/0']KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E)" + ] + + @pkh_descriptors [ + # 1 describes a P2PKH output with the specified public key. + "pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)", + # 12 describes a P2PKH output with child key 1/2 of the specified xpub. + "pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1/2)", + # 13 describes a set of P2PKH outputs, but additionally specifies that the specified xpub is a child of a master with fingerprint d34db33f, and derived using path 44'/0'/0'. + "pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)", + # 20 describes a P2PKH to a mainnet private key + "pkh(KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E)", + # 21 describes a P2PKH to a testnet private key + "pkh(cMahea7zqjxrtgAbB7LSGbcQUr1uX1okdzTtkBA29TFk41r74ddm)" + ] + + @wpkh_descriptors [ + # 2 describes a P2WPKH output with the specified public key. + "wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)", + # 21 describes a P2WPKH to a mainnet private key + "wpkh(KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E)", + # 22 describes a P2WPKH to a testnet private key + "wpkh(cMahea7zqjxrtgAbB7LSGbcQUr1uX1okdzTtkBA29TFk41r74ddm)" + ] + + @sh_descriptors [ + # 3 describes a P2SH-P2WPKH output with the specified public key. + "sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))", + # 5 describes an (overly complicated) P2SH-P2WSH-P2PKH output with the specified public key. + "sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))", + # 7 describes a P2SH 2-of-2 multisig output with keys in the specified order. + "sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))", + # 8 describes a P2SH 2-of-2 multisig output with keys sorted lexicographically in the resulting redeemScript. + "sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))", + # 10 describes a P2SH-P2WSH 1-of-3 multisig output with keys in the specified order. + "sh(wsh(multi(1,03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8,03499fdf9e895e719cfd64e67f07d38e3226aa7b63678949e6e49b241a60e823e4,02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)))" + ] + + @wsh_descriptors [ + # 9 describes a P2WSH 2-of-3 multisig output with keys in the specified order. + "wsh(multi(2,03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a))", + # 14 describes a set of 1-of-2 P2WSH multisig outputs where the first multisig key is the 1/0/i child of the first specified xpub and the second multisig key is the 0/0/i child of the second specified xpub, and i is any number in a configurable range (0-1000 by default). + "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))", + # 15 describes a set of 1-of-2 P2WSH multisig outputs where one multisig key is the 1/0/i child of the first specified xpub and the other multisig key is the 0/0/i child of the second specified xpub, and i is any number in a configurable range (0-1000 by default). The order of public keys in the resulting witnessScripts is determined by the lexicographic order of the public keys at that index. + "wsh(sortedmulti(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))" + ] + + @combo_descriptors [ + # 4 describes any P2PK, P2PKH, P2WPKH, or P2SH-P2WPKH output with the specified public key. + "combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)" + ] + + @multi_descriptors [ + # 6 describes a bare 1-of-2 multisig output with keys in the specified order. + "multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)", + # taken from 9, describes a 2-of-3 multisig output with keys in the specified order. + "multi(2,03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a)", + # taken from 14, describes a set of 1-of-2 P2WSH multisig outputs where the first multisig key is the 1/0/i child of the first specified xpub and the second multisig key is the 0/0/i child of the second specified xpub, and i is any number in a configurable range (0-1000 by default). + "multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*)" + ] + + @sorted_multi_descriptors [ + # taken from 15, describes a set of 1-of-2 P2WSH multisig outputs where one multisig key is the 1/0/i child of the first specified xpub and the other multisig key is the 0/0/i child of the second specified xpub, and i is any number in a configurable range (0-1000 by default). The order of public keys in the resulting witnessScripts is determined by the lexicographic order of the public keys at that index. + "sortedmulti(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*)" + ] + + @all_descriptors @pk_descriptors ++ + @pkh_descriptors ++ + @wpkh_descriptors ++ + @sh_descriptors ++ + @wsh_descriptors ++ + @combo_descriptors ++ + @multi_descriptors ++ + @sorted_multi_descriptors + + describe "parse descriptor" do + end + + describe "test parse/serialize pair" do + test "parse and serialize descriptor" do + for d <- @all_descriptors do + {:ok, desc} = Descriptor.parse_descriptor(d) + assert Descriptor.serialize_descriptor(desc) == d + end + end + end + + describe "create descriptor" do + test "create p2pk" do + # ex0 from @pk_descriptors + {:ok, pk} = + Point.parse_public_key( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ) + + {:ok, desc} = Descriptor.create_p2pk(pk) + + assert Descriptor.serialize_descriptor(desc) == + "pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)" + + # ex11 from @pk_descriptors + {:ok, xpub} = + ExtendedKey.parse_extended_key( + "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8" + ) + + {:ok, desc} = Descriptor.create_p2pk(xpub) + + assert Descriptor.serialize_descriptor(desc) == + "pk(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)" + + # ex16 from @pk_descriptors + {:ok, priv, :mainnet, _} = + PrivateKey.parse_wif("KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E") + + dkey = Descriptor.DKey.from_key(priv, :mainnet) + {:ok, desc} = Descriptor.create_p2pk(dkey) + + assert Descriptor.serialize_descriptor(desc) == + "pk(KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E)" + end + + test "create p2pkh" do + # ex1 from @pkh_descriptors + {:ok, pk} = + Point.parse_public_key( + "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + ) + + {:ok, desc} = Descriptor.create_p2pkh(pk) + + assert Descriptor.serialize_descriptor(desc) == + "pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)" + + # ex12 from @pkh_descriptors + {:ok, xpub} = + ExtendedKey.parse_extended_key( + "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw" + ) + + {:ok, deriv} = ExtendedKey.DerivationPath.from_string("1/2") + # TODO make it easier to create Descriptors with path data + dkey = Descriptor.DKey.from_key(xpub, %{desc_path: deriv}) + {:ok, desc} = Descriptor.create_p2pkh(dkey) + + assert Descriptor.serialize_descriptor(desc) == + "pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1/2)" + + # ex13 from @pkh_descriptors + {:ok, mfp} = Base.decode16("d34db33f", case: :lower) + + {:ok, xpub} = + ExtendedKey.parse_extended_key( + "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL" + ) + + {:ok, anc_path} = DerivationPath.from_string("44'/0'/0'") + {:ok, desc_path} = DerivationPath.from_string("1/*") + + dkey = + Descriptor.DKey.from_key(xpub, %{ + parent_fingerprint: mfp, + anc_path: anc_path, + desc_path: desc_path + }) + + {:ok, desc} = Descriptor.create_p2pkh(dkey) + + assert Descriptor.serialize_descriptor(desc) == + "pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)" + + # ex20 from @pkh_descriptors + {:ok, priv, :mainnet, _compressed} = + PrivateKey.parse_wif("KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E") + + priv1 = PrivateKey.wif!(priv, :mainnet) + assert priv1 == "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E" + + dkey = Descriptor.DKey.from_key(priv, :mainnet) + {:ok, desc} = Descriptor.create_p2pkh(dkey) + + assert Descriptor.serialize_descriptor(desc) == + "pkh(KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E)" + + # ex21 from @pkh_descriptors + {:ok, priv, :testnet, _compressed} = + PrivateKey.parse_wif("cMahea7zqjxrtgAbB7LSGbcQUr1uX1okdzTtkBA29TFk41r74ddm") + + dkey = Descriptor.DKey.from_key(priv, :testnet) + {:ok, desc} = Descriptor.create_p2pkh(dkey) + + assert Descriptor.serialize_descriptor(desc) == + "pkh(cMahea7zqjxrtgAbB7LSGbcQUr1uX1okdzTtkBA29TFk41r74ddm)" + end + + test "create p2sh" do + # 3 describes a P2SH-P2WPKH output with the specified public key. + {:ok, pk} = + Point.parse_public_key( + "03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556" + ) + + {:ok, wpkh_desc} = Descriptor.create_p2wpkh(pk) + {:ok, sh_desc} = Descriptor.create_p2sh(wpkh_desc) + + assert Descriptor.serialize_descriptor(sh_desc) == + "sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))" + + # 5 describes an (overly complicated) P2SH-P2WSH-P2PKH output with the specified public key. + {:ok, pk} = + Point.parse_public_key( + "02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13" + ) + + {:ok, pkh_desc} = Descriptor.create_p2pkh(pk) + {:ok, wsh_desc} = Descriptor.create_p2wsh(pkh_desc) + {:ok, sh_desc} = Descriptor.create_p2sh(wsh_desc) + + assert Descriptor.serialize_descriptor(sh_desc) == + "sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))" + + # 7 describes a P2SH 2-of-2 multisig output with keys in the specified order. + {:ok, pk1} = + Point.parse_public_key( + "022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01" + ) + + {:ok, pk2} = + Point.parse_public_key( + "03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe" + ) + + {:ok, multi_desc} = Descriptor.create_multi({2, [pk1, pk2]}) + {:ok, sh_desc} = Descriptor.create_p2sh(multi_desc) + + assert Descriptor.serialize_descriptor(sh_desc) == + "sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))" + + # 8 describes a P2SH 2-of-2 multisig output with keys sorted lexicographically in the resulting redeemScript. + {:ok, pk1} = + Point.parse_public_key( + "03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe" + ) + + {:ok, pk2} = + Point.parse_public_key( + "022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01" + ) + + {:ok, multi_desc} = Descriptor.create_sortedmulti({2, [pk1, pk2]}) + {:ok, sh_desc} = Descriptor.create_p2sh(multi_desc) + + assert Descriptor.serialize_descriptor(sh_desc) == + "sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))" + + # 10 describes a P2SH-P2WSH 1-of-3 multisig output with keys in the specified order. + {:ok, pk1} = + Point.parse_public_key( + "03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8" + ) + + {:ok, pk2} = + Point.parse_public_key( + "03499fdf9e895e719cfd64e67f07d38e3226aa7b63678949e6e49b241a60e823e4" + ) + + {:ok, pk3} = + Point.parse_public_key( + "02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e" + ) + + {:ok, multi_desc} = Descriptor.create_multi({1, [pk1, pk2, pk3]}) + {:ok, wsh_desc} = Descriptor.create_p2wsh(multi_desc) + {:ok, sh_desc} = Descriptor.create_p2sh(wsh_desc) + + assert Descriptor.serialize_descriptor(sh_desc) == + "sh(wsh(multi(1,03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8,03499fdf9e895e719cfd64e67f07d38e3226aa7b63678949e6e49b241a60e823e4,02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)))" + end + + test "create p2wpkh" do + # ex2 describes a P2WPKH output with the specified public key. + {:ok, pk} = + Point.parse_public_key( + "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9" + ) + + {:ok, desc} = Descriptor.create_p2wpkh(pk) + + assert Descriptor.serialize_descriptor(desc) == + "wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)" + + # ex21 describes a P2WPKH to a mainnet private key + {:ok, priv, :mainnet, _compressed} = + PrivateKey.parse_wif("KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E") + + dkey = Descriptor.DKey.from_key(priv, :mainnet) + {:ok, desc} = Descriptor.create_p2wpkh(dkey) + + assert Descriptor.serialize_descriptor(desc) == + "wpkh(KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E)" + + # ex22 describes a P2WPKH to a testnet private key + {:ok, priv, :testnet, _compressed} = + PrivateKey.parse_wif("cMahea7zqjxrtgAbB7LSGbcQUr1uX1okdzTtkBA29TFk41r74ddm") + + dkey = Descriptor.DKey.from_key(priv, :testnet) + {:ok, desc} = Descriptor.create_p2wpkh(dkey) + + assert Descriptor.serialize_descriptor(desc) == + "wpkh(cMahea7zqjxrtgAbB7LSGbcQUr1uX1okdzTtkBA29TFk41r74ddm)" + end + + test "create p2wsh" do + # 9 describes a P2WSH 2-of-3 multisig output with keys in the specified order. + {:ok, pk1} = + Point.parse_public_key( + "03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7" + ) + + {:ok, pk2} = + Point.parse_public_key( + "03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb" + ) + + {:ok, pk3} = + Point.parse_public_key( + "03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a" + ) + + {:ok, multi_desc} = Descriptor.create_multi({2, [pk1, pk2, pk3]}) + {:ok, wsh_desc} = Descriptor.create_p2wsh(multi_desc) + + assert Descriptor.serialize_descriptor(wsh_desc) == + "wsh(multi(2,03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a))" + + # 14 describes a set of 1-of-2 P2WSH multisig outputs where the first multisig key is the 1/0/i child of the first specified xpub and the second multisig key is the 0/0/i child of the second specified xpub, and i is any number in a configurable range (0-1000 by default). + {:ok, xpub1} = + ExtendedKey.parse_extended_key( + "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB" + ) + + {:ok, deriv1} = ExtendedKey.DerivationPath.from_string("1/0/*") + + {:ok, xpub2} = + ExtendedKey.parse_extended_key( + "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH" + ) + + {:ok, deriv2} = ExtendedKey.DerivationPath.from_string("0/0/*") + dkey1 = Descriptor.DKey.from_key(xpub1, %{desc_path: deriv1}) + dkey2 = Descriptor.DKey.from_key(xpub2, %{desc_path: deriv2}) + {:ok, multi_desc} = Descriptor.create_multi({1, [dkey1, dkey2]}) + {:ok, wsh_desc} = Descriptor.create_p2wsh(multi_desc) + + assert Descriptor.serialize_descriptor(wsh_desc) == + "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))" + + # 15 describes a set of 1-of-2 P2WSH multisig outputs where one multisig key is the 1/0/i child of the first specified xpub and the other multisig key is the 0/0/i child of the second specified xpub, and i is any number in a configurable range (0-1000 by default). + {:ok, xpub1} = + ExtendedKey.parse_extended_key( + "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB" + ) + + {:ok, deriv1} = ExtendedKey.DerivationPath.from_string("1/0/*") + + {:ok, xpub2} = + ExtendedKey.parse_extended_key( + "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH" + ) + + {:ok, deriv2} = ExtendedKey.DerivationPath.from_string("0/0/*") + dkey1 = Descriptor.DKey.from_key(xpub1, %{desc_path: deriv1}) + dkey2 = Descriptor.DKey.from_key(xpub2, %{desc_path: deriv2}) + {:ok, multi_desc} = Descriptor.create_sortedmulti({1, [dkey1, dkey2]}) + {:ok, wsh_desc} = Descriptor.create_p2wsh(multi_desc) + + assert Descriptor.serialize_descriptor(wsh_desc) == + "wsh(sortedmulti(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))" + end + + test "create combo" do + # 4 describes any P2PK, P2PKH, P2WPKH, or P2SH-P2WPKH output with the specified public key. + {:ok, pk} = + Point.parse_public_key( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ) + + {:ok, desc} = Descriptor.create_combo(pk) + + assert Descriptor.serialize_descriptor(desc) == + "combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)" + end + + test "create multi" do + # 6 describes a bare 1-of-2 multisig output with keys in the specified order. + {:ok, pk1} = + Point.parse_public_key( + "022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4" + ) + + {:ok, pk2} = + Point.parse_public_key( + "025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc" + ) + + {:ok, desc} = Descriptor.create_multi({1, [pk1, pk2]}) + + assert Descriptor.serialize_descriptor(desc) == + "multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)" + + # taken from 9, describes a 2-of-3 multisig output with keys in the specified order. + {:ok, pk1} = + Point.parse_public_key( + "03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7" + ) + + {:ok, pk2} = + Point.parse_public_key( + "03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb" + ) + + {:ok, pk3} = + Point.parse_public_key( + "03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a" + ) + + {:ok, desc} = Descriptor.create_multi({2, [pk1, pk2, pk3]}) + + assert Descriptor.serialize_descriptor(desc) == + "multi(2,03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a)" + + # taken from 14, describes a set of 1-of-2 P2WSH multisig outputs where the first multisig key is the 1/0/i child of the first specified xpub and the second multisig key is the 0/0/i child of the second specified xpub, and i is any number in a configurable range (0-1000 by default). + {:ok, xpub1} = + ExtendedKey.parse_extended_key( + "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB" + ) + + {:ok, deriv1} = ExtendedKey.DerivationPath.from_string("1/0/*") + + {:ok, xpub2} = + ExtendedKey.parse_extended_key( + "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH" + ) + + {:ok, deriv2} = ExtendedKey.DerivationPath.from_string("0/0/*") + dkey1 = Descriptor.DKey.from_key(xpub1, %{desc_path: deriv1}) + dkey2 = Descriptor.DKey.from_key(xpub2, %{desc_path: deriv2}) + {:ok, desc} = Descriptor.create_multi({1, [dkey1, dkey2]}) + + assert Descriptor.serialize_descriptor(desc) == + "multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*)" + end + + test "create sortedmulti" do + # taken from 15, describes a set of 1-of-2 P2WSH multisig outputs where one multisig key is the 1/0/i child of the first specified xpub and the other multisig key is the 0/0/i child of the second specified xpub, and i is any number in a configurable range (0-1000 by default). The order of public keys in the resulting witnessScripts is determined by the lexicographic order of the public keys at that index. + {:ok, xpub1} = + ExtendedKey.parse_extended_key( + "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB" + ) + + {:ok, deriv1} = ExtendedKey.DerivationPath.from_string("1/0/*") + + {:ok, xpub2} = + ExtendedKey.parse_extended_key( + "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH" + ) + + {:ok, deriv2} = ExtendedKey.DerivationPath.from_string("0/0/*") + dkey1 = Descriptor.DKey.from_key(xpub1, %{desc_path: deriv1}) + dkey2 = Descriptor.DKey.from_key(xpub2, %{desc_path: deriv2}) + {:ok, desc} = Descriptor.create_sortedmulti({1, [dkey1, dkey2]}) + + assert Descriptor.serialize_descriptor(desc) == + "sortedmulti(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*)" + end + end +end diff --git a/test/extendedkey_test.exs b/test/extendedkey_test.exs index b2826b3..662bf9f 100644 --- a/test/extendedkey_test.exs +++ b/test/extendedkey_test.exs @@ -256,10 +256,10 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do t = @bip32_test_case_1 # priv assert ExtendedKey.parse_extended_key(t.xprv_m) == {:ok, t.xprv_m_obj} - assert ExtendedKey.display_extended_key(t.xprv_m_obj) == t.xprv_m + assert ExtendedKey.to_string(t.xprv_m_obj) == t.xprv_m # pub assert ExtendedKey.parse_extended_key(t.xpub_m) == {:ok, t.xpub_m_obj} - assert ExtendedKey.display_extended_key(t.xpub_m_obj) == t.xpub_m + assert ExtendedKey.to_string(t.xpub_m_obj) == t.xpub_m end end @@ -570,7 +570,7 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) {:ok, xpub_m_0h} = ExtendedKey.derive_public_child(xprv, @min_hardened_child_num) - assert ExtendedKey.display_extended_key(xpub_m_0h) == t.xpub_m_0h + assert ExtendedKey.to_string(xpub_m_0h) == t.xpub_m_0h end test "BIP32 tests 3: derive master prv key from seed" do @@ -663,7 +663,7 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do t.xprv_m_obj |> ExtendedKey.derive_extended_key(deriv) - assert ExtendedKey.display_extended_key(child_key) == t.xprv_m_0h_1_2h_2 + assert ExtendedKey.to_string(child_key) == t.xprv_m_0h_1_2h_2 end test "successfully derive xpub child key with derivation path" do @@ -682,7 +682,7 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do {:ok, xprv_t2} = ExtendedKey.derive_extended_key(xprv_t1, deriv) {:ok, child_key} = ExtendedKey.to_extended_public_key(xprv_t2) - assert ExtendedKey.display_extended_key(child_key) == t.xpub_m_0_2147483647h_1_2147483646h + assert ExtendedKey.to_string(child_key) == t.xpub_m_0_2147483647h_1_2147483646h end test "test use of deriv path bip32 test 2" do diff --git a/test/script_test.exs b/test/script_test.exs index dbbbcdc..385d613 100644 --- a/test/script_test.exs +++ b/test/script_test.exs @@ -94,7 +94,7 @@ defmodule Bitcoinex.ScriptTest do "0020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d" ] - # from + # from # https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki#test-vectors-for-v0-v16-native-segregated-witness-addresses # test vectors that are not valid v0 or v1 scripts have been removed. @bip350_test_vectors [ @@ -682,7 +682,7 @@ defmodule Bitcoinex.ScriptTest do describe "test parsing scripts" do test "test parse pushdata1 script" do - # pushdata1 + # pushdata1 data_hex = "c5802547372094c58025802547372094c5802547802c9ca07652be7e8025472547372094c5802547802c9ca07652be7e802547372094c5802547802c9ca07652be7e47802c9ca07652be7e6419bc9aa1" @@ -720,7 +720,7 @@ defmodule Bitcoinex.ScriptTest do end test "test parse pushdata2 script" do - # pushdata2 + # pushdata2 data_hex = "c5802547372094c58025478025473720941cee958bca07652be7e6419bc91cee958b8025473720941cee958bca07652be7e6419bc91cee958b3720941cee958bca07652be7e6419bc91ceec5802547372094c58025478025473720941cee958bca07652be7e6419bc91cee958b8025473720941cee958bca07652be7e6419bc91cee958b3720941cee958bca07652be7e6419c5802547372094c58025478025473720941cee958bca07652be7e6419bc91cee958b8025473720941cee958bca07652be7e6419bc91cee958b3720941cee958bca07652be7ec5802547372094c58025478025473720941cee958bca07652be7e6419bc91cee958b8025473720941cee958bca07652be7e6419bc91cee958b3720941cee958bca07652be7e6419bc91cee958bc58025473720941cee958bca07652be7e6419bc9ca07652be7e6419bc96419bc91cee958bc58025473720941cee958bca07652be7e6419bc9ca07652be7e6419bc9bc91cee958bc58025473720941cee958bca07652be7e6419bc9ca07652c5802547372094c58025478025473720941cee958bca07652be7e6419bc91cee958b8025473720941cee958bca07652be7e6419bc91cee958b3720941cee958bca07652be9bc9958bc58025473720941cee958bca07652be7e6419bc9ca07652be7e6419bc9" @@ -1050,6 +1050,25 @@ defmodule Bitcoinex.ScriptTest do assert Script.to_address(p2wpkh, :mainnet) == {:ok, p2wpkh_addr} end + test "test p2pk address" do + # 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b + script_hex = "4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac" + pubkey = "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f" + {:ok, script} = Script.parse_script(script_hex) + assert Script.to_address(script, :mainnet) == {:ok, pubkey} + + # f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16 + script_hex = "4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac" + pubkey = "04ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c" + {:ok, script} = Script.parse_script(script_hex) + assert Script.to_address(script, :mainnet) == {:ok, pubkey} + + script_hex = "21035ce3ee697cd5148e12ab7bb45c1ef4dd5ee2bf4867d9d35135e214e073211344ac" + pubkey = "035ce3ee697cd5148e12ab7bb45c1ef4dd5ee2bf4867d9d35135e214e073211344" + {:ok, script} = Script.parse_script(script_hex) + assert Script.to_address(script, :mainnet) == {:ok, pubkey} + end + test "test p2pkh address" do # from tx 1af0fbe9141371e29ab870121a3d9ae361d6664d789e367e6341e8a4b3311ea0 addr = "12wRAwmwVBXrnquwwc8uH5xHT7ExaP6gU3"