From 21267167d450090d5b4a14871ae0ce31942ec160 Mon Sep 17 00:00:00 2001 From: Sachin Meier Date: Thu, 24 Jun 2021 00:25:14 -0700 Subject: [PATCH 1/6] rebase --- test/script_test.exs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/test/script_test.exs b/test/script_test.exs index dbbbcdc..ef0364d 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" @@ -1125,6 +1125,7 @@ defmodule Bitcoinex.ScriptTest do end end end +<<<<<<< HEAD test "test bip350 test vectors" do for t <- @bip350_test_vectors do @@ -1132,6 +1133,8 @@ defmodule Bitcoinex.ScriptTest do assert Script.to_address(s, t.net) == {:ok, t.b32} end end +======= +>>>>>>> 7206ceb (add multisig capability & more testing) end describe "test from_address" do @@ -1264,6 +1267,26 @@ defmodule Bitcoinex.ScriptTest do end end + describe "test extract multisig policy" do + test "extract policy from multisig script" do + for multi <- @raw_multisigs_with_data do + {:ok, ms} = Script.parse_script(multi.script_hex) + {:ok, m, pks} = Script.extract_multi_policy(ms) + {:ok, ms2} = Script.create_multi(m, pks) + + assert ms == ms2 + end + + for m <- @raw_multisig_scripts do + {:ok, ms} = Script.parse_script(m) + {:ok, m, pks} = Script.extract_multi_policy(ms) + {:ok, ms2} = Script.create_multi(m, pks) + + assert ms == ms2 + end + end + end + describe "full tests" do test "test parse, serialize and create addresses for multisig" do # from tx 0a6140bbf75e73f11b90c4dabf71f83394d493d635c2bbf19d207fb821de74f5 From 3f74ff0fb49694978c1e2987cd0e74a82221d4a6 Mon Sep 17 00:00:00 2001 From: Sachin Meier Date: Fri, 30 Apr 2021 16:12:10 -0700 Subject: [PATCH 2/6] init stash rename files temp temp more descriptor testing --- lib/descriptor.ex | 459 ++++++++++++++++++++++++++++++++++++ lib/extendedkey.ex | 24 +- lib/secp256k1/privatekey.ex | 7 +- test/descriptor_test.exs | 179 ++++++++++++++ test/extendedkey_test.exs | 10 +- 5 files changed, 659 insertions(+), 20 deletions(-) create mode 100644 lib/descriptor.ex create mode 100644 test/descriptor_test.exs diff --git a/lib/descriptor.ex b/lib/descriptor.ex new file mode 100644 index 0000000..b2faa6d --- /dev/null +++ b/lib/descriptor.ex @@ -0,0 +1,459 @@ +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() + + @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(_), 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 + :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(_), 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" -> + {:ok, pk} = Point.parse_public_key(hex_data) + {:ok, pk, DerivationPath.new()} + + # 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 dkey_type :: DKey.t() | Point.t() | PrivateKey.t() + + @type t :: %__MODULE__{ + script_type: descriptor_type, + data: t() | DKey.t() | Script.t() | {non_neg_integer(), list(DKeys.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 + + def 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 + + def 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()) :: {:ok, String.t()} | {:error, 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 + + 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 + #TODO + # :addr -> + # return exact address script type + # :raw -> + # return exact script type + end + end + + 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) + :multi -> create_multi(dkey) + :sortedmulti -> create_sortedmulti(dkey) + :addr -> create_addr(dkey) + :raw -> create_raw(dkey) + end + end + + # Allow users to easily set an xpub, origin and desc info. this is a weird way + + @spec create_p2pk(dkey_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_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_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_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_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_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 +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/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..77d67d9 --- /dev/null +++ b/test/descriptor_test.exs @@ -0,0 +1,179 @@ +defmodule Bitcoinex.DescriptorTest do + use ExUnit.Case + doctest Bitcoinex.Descriptor + + alias Bitcoinex.{Descriptor, ExtendedKey, Secp256k1.Point, Secp256k1.PrivateKey} + + # 0-15 are from https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md + # >15 are original examples and probably 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 "test 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, _,_} = PrivateKey.parse_wif("KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E") + {:ok, desc} = Descriptor.create_p2pk(priv) + assert Descriptor.serialize_descriptor(desc) == "pk(KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E)" + end + + test "create p2pkh" do + # 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)", + + # 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)" + + end + + + end + +end + +# alias Bitcoinex.{ +# ExtendedKey, +# ExtendedKey.DerivationPath, +# Descriptor.DKey, +# Secp256k1.Point, +# Secp256k1.PrivateKey +# } + +# {:ok, px} = +# "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U" +# |> ExtendedKey.parse_extended_key() + +# {:ok, dp} = DerivationPath.from_string("44'/0'/0'/") +# {:ok, ddp} = DerivationPath.from_string("0/1'/*'") +# {:ok, cx} = ExtendedKey.derive_extended_key(px, dp) +# fp = ExtendedKey.get_fingerprint(cx) +# dk = %DKey{key: cx, ancestor_path: dp, fingerprint: fp, descendant_path: ddp} +# DKey.serialize(dk) + +# {:ok, sk, network, _comp} = +# PrivateKey.parse_wif("KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E") + +# dk2 = %DKey{key: {sk, network}} + +# {:ok, pk} = +# Point.parse_public_key("020003b94aecea4d0a57a6c87cf43c50c8b3736f33ab7fd34f02441b6e94477689") + +# dk3 = %DKey{key: pk} + +# "[d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*" + +# "[d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*'" 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 From 59e51b6457a592ecd116fe67651e901e7de467bd Mon Sep 17 00:00:00 2001 From: Sachin Meier Date: Tue, 22 Feb 2022 22:04:27 -0800 Subject: [PATCH 3/6] finish descriptor testing & format lint --- lib/descriptor.ex | 941 ++++++++++++++++++++------------------- test/descriptor_test.exs | 663 ++++++++++++++++++++------- test/script_test.exs | 23 - 3 files changed, 978 insertions(+), 649 deletions(-) diff --git a/lib/descriptor.ex b/lib/descriptor.ex index b2faa6d..cea07f1 100644 --- a/lib/descriptor.ex +++ b/lib/descriptor.ex @@ -1,459 +1,486 @@ 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() - - @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(_), 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 - :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(_), 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" -> - {:ok, pk} = Point.parse_public_key(hex_data) - {:ok, pk, DerivationPath.new()} - - # 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 dkey_type :: DKey.t() | Point.t() | PrivateKey.t() - - @type t :: %__MODULE__{ - script_type: descriptor_type, - data: t() | DKey.t() | Script.t() | {non_neg_integer(), list(DKeys.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 - - def 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 - - def 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()) :: {:ok, String.t()} | {:error, 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 - - 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 - #TODO - # :addr -> - # return exact address script type - # :raw -> - # return exact script type - end - end - - 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) - :multi -> create_multi(dkey) - :sortedmulti -> create_sortedmulti(dkey) - :addr -> create_addr(dkey) - :raw -> create_raw(dkey) - end - end - - # Allow users to easily set an xpub, origin and desc info. this is a weird way - - @spec create_p2pk(dkey_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_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_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_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_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_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 + @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() + + @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(_), 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 + :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(_), 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(DKeys.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 + + def 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 + + def 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 + + 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 + # TODO + # :addr -> + # return exact address script type + # :raw -> + # return exact script type + end + end + + 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) + :multi -> create_multi(dkey) + :sortedmulti -> create_sortedmulti(dkey) + :addr -> create_addr(dkey) + :raw -> create_raw(dkey) + 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 end diff --git a/test/descriptor_test.exs b/test/descriptor_test.exs index 77d67d9..54b797c 100644 --- a/test/descriptor_test.exs +++ b/test/descriptor_test.exs @@ -1,179 +1,504 @@ defmodule Bitcoinex.DescriptorTest do - use ExUnit.Case - doctest Bitcoinex.Descriptor - - alias Bitcoinex.{Descriptor, ExtendedKey, Secp256k1.Point, Secp256k1.PrivateKey} - - # 0-15 are from https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md - # >15 are original examples and probably 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 + 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 "test 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, _,_} = PrivateKey.parse_wif("KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E") - {:ok, desc} = Descriptor.create_p2pk(priv) - assert Descriptor.serialize_descriptor(desc) == "pk(KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E)" - end - - test "create p2pkh" do - # 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)", - - # 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)" - - end - - - end + # 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) -end - -# alias Bitcoinex.{ -# ExtendedKey, -# ExtendedKey.DerivationPath, -# Descriptor.DKey, -# Secp256k1.Point, -# Secp256k1.PrivateKey -# } + assert Descriptor.serialize_descriptor(desc) == + "pkh(cMahea7zqjxrtgAbB7LSGbcQUr1uX1okdzTtkBA29TFk41r74ddm)" + end -# {:ok, px} = -# "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U" -# |> ExtendedKey.parse_extended_key() + test "create p2sh" do + # 3 describes a P2SH-P2WPKH output with the specified public key. + {:ok, pk} = + Point.parse_public_key( + "03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556" + ) -# {:ok, dp} = DerivationPath.from_string("44'/0'/0'/") -# {:ok, ddp} = DerivationPath.from_string("0/1'/*'") -# {:ok, cx} = ExtendedKey.derive_extended_key(px, dp) -# fp = ExtendedKey.get_fingerprint(cx) -# dk = %DKey{key: cx, ancestor_path: dp, fingerprint: fp, descendant_path: ddp} -# DKey.serialize(dk) + {:ok, wpkh_desc} = Descriptor.create_p2wpkh(pk) + {:ok, sh_desc} = Descriptor.create_p2sh(wpkh_desc) -# {:ok, sk, network, _comp} = -# PrivateKey.parse_wif("KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E") + assert Descriptor.serialize_descriptor(sh_desc) == + "sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))" -# dk2 = %DKey{key: {sk, network}} + # 5 describes an (overly complicated) P2SH-P2WSH-P2PKH output with the specified public key. + {:ok, pk} = + Point.parse_public_key( + "02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13" + ) -# {:ok, pk} = -# Point.parse_public_key("020003b94aecea4d0a57a6c87cf43c50c8b3736f33ab7fd34f02441b6e94477689") + {: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" + ) -# dk3 = %DKey{key: pk} + {: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" + ) -# "[d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*" - -# "[d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*'" + {: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/script_test.exs b/test/script_test.exs index ef0364d..a29f0c8 100644 --- a/test/script_test.exs +++ b/test/script_test.exs @@ -1125,7 +1125,6 @@ defmodule Bitcoinex.ScriptTest do end end end -<<<<<<< HEAD test "test bip350 test vectors" do for t <- @bip350_test_vectors do @@ -1133,8 +1132,6 @@ defmodule Bitcoinex.ScriptTest do assert Script.to_address(s, t.net) == {:ok, t.b32} end end -======= ->>>>>>> 7206ceb (add multisig capability & more testing) end describe "test from_address" do @@ -1267,26 +1264,6 @@ defmodule Bitcoinex.ScriptTest do end end - describe "test extract multisig policy" do - test "extract policy from multisig script" do - for multi <- @raw_multisigs_with_data do - {:ok, ms} = Script.parse_script(multi.script_hex) - {:ok, m, pks} = Script.extract_multi_policy(ms) - {:ok, ms2} = Script.create_multi(m, pks) - - assert ms == ms2 - end - - for m <- @raw_multisig_scripts do - {:ok, ms} = Script.parse_script(m) - {:ok, m, pks} = Script.extract_multi_policy(ms) - {:ok, ms2} = Script.create_multi(m, pks) - - assert ms == ms2 - end - end - end - describe "full tests" do test "test parse, serialize and create addresses for multisig" do # from tx 0a6140bbf75e73f11b90c4dabf71f83394d493d635c2bbf19d207fb821de74f5 From f64b90866a533d404ac0010719d7ed1d25915ad1 Mon Sep 17 00:00:00 2001 From: Sachin Meier Date: Sun, 27 Feb 2022 11:35:21 -0800 Subject: [PATCH 4/6] fix lint --- lib/descriptor.ex | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/descriptor.ex b/lib/descriptor.ex index cea07f1..4ebf688 100644 --- a/lib/descriptor.ex +++ b/lib/descriptor.ex @@ -226,7 +226,7 @@ defmodule Bitcoinex.Descriptor do @type t :: %__MODULE__{ script_type: descriptor_type, - data: t() | DKey.t() | Script.t() | {non_neg_integer(), list(DKeys.t())} | binary + data: t() | DKey.t() | Script.t() | {non_neg_integer(), list(DKey.t())} | binary } @enforce_keys [ :script_type, @@ -342,6 +342,7 @@ defmodule Bitcoinex.Descriptor do to_string(script_type) <> "(#{data})" end + @spec get_script_type(t()) :: descriptor_type() def get_script_type(descriptor) do case descriptor.script_type do :pk -> @@ -383,10 +384,17 @@ defmodule Bitcoinex.Descriptor do :pkh -> create_p2pkh(dkey) :wpkh -> create_p2wpkh(dkey) :combo -> create_combo(dkey) - :multi -> create_multi(dkey) - :sortedmulti -> create_sortedmulti(dkey) :addr -> create_addr(dkey) :raw -> create_raw(dkey) + _ -> {:error, "unknown script type"} + end + end + + 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 From 9f90ecff58e3daf4196e6526302f8c5a5eaf7736 Mon Sep 17 00:00:00 2001 From: Sachin Meier Date: Thu, 7 Jul 2022 18:20:08 -0700 Subject: [PATCH 5/6] fix lint --- lib/descriptor.ex | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/descriptor.ex b/lib/descriptor.ex index 4ebf688..8bf1015 100644 --- a/lib/descriptor.ex +++ b/lib/descriptor.ex @@ -342,7 +342,7 @@ defmodule Bitcoinex.Descriptor do to_string(script_type) <> "(#{data})" end - @spec get_script_type(t()) :: descriptor_type() + @spec get_script_type(t()) :: Script.script_type() def get_script_type(descriptor) do case descriptor.script_type do :pk -> @@ -368,11 +368,18 @@ defmodule Bitcoinex.Descriptor do :sortedmulti -> :multi - # TODO - # :addr -> + + :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 -> - # return exact script type + :raw -> + case Script.parse_script(descriptor.data) do + {:ok, script} -> Script.get_script_type(script) + {:error, _msg} -> :non_standard + end end end From cfbc2ec20aff3b97bc13699d9e0c3d65baaea71b Mon Sep 17 00:00:00 2001 From: Sachin Meier Date: Mon, 29 Aug 2022 11:13:56 -0400 Subject: [PATCH 6/6] temp --- lib/descriptor.ex | 46 +++++++++++++++++-- lib/script.ex | 102 +++++++++++++++++++------------------------ test/script_test.exs | 19 ++++++++ 3 files changed, 107 insertions(+), 60 deletions(-) diff --git a/lib/descriptor.ex b/lib/descriptor.ex index 8bf1015..f9f1726 100644 --- a/lib/descriptor.ex +++ b/lib/descriptor.ex @@ -28,7 +28,7 @@ defmodule Bitcoinex.Descriptor do defmodule DKey do # WIF encoding requires network info @type key_type :: - ExtendedKey.t() | {PrivateKey.t(), Network.network_name()} | Point.t() + ExtendedKey.t() | {PrivateKey.t(), Network.network_name()} | Point.t() | String.t() @type t :: %__MODULE__{ key: key_type, @@ -52,6 +52,7 @@ defmodule Bitcoinex.Descriptor do 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 @@ -60,6 +61,7 @@ defmodule Bitcoinex.Descriptor do :extended_key -> true :public_key -> true :private_key -> true + :address -> true :invalid_key -> false end end @@ -69,6 +71,7 @@ defmodule Bitcoinex.Descriptor do 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) @@ -245,7 +248,7 @@ defmodule Bitcoinex.Descriptor do end end - def parser(desc) do + defp parser(desc) do case split_descriptor(desc) do # sh & wsh can be recursive {:ok, :sh, rest} -> @@ -279,7 +282,7 @@ defmodule Bitcoinex.Descriptor do end end - def split_descriptor(desc) do + defp split_descriptor(desc) do [s_type, rest] = String.split(desc, "(", parts: 2) case String.split_at(rest, -1) do @@ -383,6 +386,7 @@ defmodule Bitcoinex.Descriptor do 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) @@ -397,6 +401,7 @@ defmodule Bitcoinex.Descriptor do 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}) @@ -498,4 +503,39 @@ defmodule Bitcoinex.Descriptor do {: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/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/test/script_test.exs b/test/script_test.exs index a29f0c8..385d613 100644 --- a/test/script_test.exs +++ b/test/script_test.exs @@ -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"