diff --git a/lib/extendedkey.ex b/lib/extendedkey.ex index a7275c4..a0db3bd 100644 --- a/lib/extendedkey.ex +++ b/lib/extendedkey.ex @@ -42,18 +42,21 @@ defmodule Bitcoinex.ExtendedKey do def new(), do: %__MODULE__{child_nums: []} + @spec serialize(t()) :: {:ok, binary} | {:error, String.t()} + def serialize(dp = %__MODULE__{}), do: to_bin(dp) + @spec to_string(t()) :: {:ok, String.t()} | {:error, String.t()} - def to_string(%__MODULE__{child_nums: path}), do: tto_string(path, "") + def to_string(%__MODULE__{child_nums: path}), do: path_to_string(path, "") - defp tto_string([], path_acc), do: {:ok, path_acc} + defp path_to_string([], path_acc), do: {:ok, path_acc} - defp tto_string([l | rest], path_acc) do + defp path_to_string([l | rest], path_acc) do cond do l == :any -> - tto_string(rest, path_acc <> "*/") + path_to_string(rest, path_acc <> "*/") l == :anyh -> - tto_string(rest, path_acc <> "*'/") + path_to_string(rest, path_acc <> "*'/") l > @max_hardened_child_num -> {:error, "index cannot be greater than #{@max_hardened_child_num}"} @@ -63,7 +66,7 @@ defmodule Bitcoinex.ExtendedKey do # hardened l >= @min_hardened_child_num -> - tto_string( + path_to_string( rest, path_acc <> (l @@ -74,31 +77,108 @@ defmodule Bitcoinex.ExtendedKey do # unhardened true -> - tto_string(rest, path_acc <> Integer.to_string(l) <> "/") + path_to_string(rest, path_acc <> Integer.to_string(l) <> "/") + end + end + + @spec to_bin(t()) :: {:ok, binary} | {:error, String.t()} + def to_bin(%__MODULE__{child_nums: child_nums}) do + try do + {:ok, to_bin(child_nums, <<>>)} + rescue + e in ArgumentError -> {:error, e.message} + end + end + + defp to_bin([], path_acc), do: path_acc + + defp to_bin([lvl | rest], path_acc) do + cond do + lvl == :any or lvl == :anyh -> + raise(ArgumentError, + message: "Derivation Path with wildcard cannot be encoded to binary." + ) + + lvl > @max_hardened_child_num -> + raise(ArgumentError, message: "index cannot be greater than #{@max_hardened_child_num}") + + lvl < @min_non_hardened_child_num -> + raise(ArgumentError, message: "index cannot be less than #{@min_non_hardened_child_num}") + + true -> + lvlbin = + lvl + |> :binary.encode_unsigned(:little) + |> Bitcoinex.Utils.pad(4, :trailing) + + to_bin(rest, path_acc <> lvlbin) end end + @spec parse(binary) :: {:ok, t()} | {:error, String.t()} + def parse(dp), do: from_bin(dp) + @spec from_string(String.t()) :: {:ok, t()} | {:error, String.t()} def from_string(pathstr) do try do - {:ok, %__MODULE__{child_nums: tfrom_string(String.split(pathstr, "/"))}} + {:ok, + %__MODULE__{ + child_nums: + pathstr + |> String.split("/") + |> path_from_string([]) + |> Enum.reverse() + }} rescue e in ArgumentError -> {:error, e.message} end end - defp tfrom_string(path_list) do + defp path_from_string(path_list, child_nums) do case path_list do - [] -> [] - [""] -> [] - ["m" | rest] -> tfrom_string(rest) - ["*" | rest] -> [:any | tfrom_string(rest)] - ["*'" | rest] -> [:anyh | tfrom_string(rest)] - ["*h" | rest] -> [:anyh | tfrom_string(rest)] - [i | rest] -> [str_to_level(i) | tfrom_string(rest)] + [] -> + child_nums + + [""] -> + child_nums + + ["m" | rest] -> + if child_nums != [] do + raise(ArgumentError, + message: "m can only be present at the begining of a derivation path." + ) + else + path_from_string(rest, child_nums) + end + + ["*" | rest] -> + path_from_string(rest, [:any | child_nums]) + + ["*'" | rest] -> + path_from_string(rest, [:anyh | child_nums]) + + ["*h" | rest] -> + path_from_string(rest, [:anyh | child_nums]) + + [i | rest] -> + path_from_string(rest, [str_to_level(i) | child_nums]) end end + @spec from_bin(binary) :: {:ok, t()} | {:error, String.t()} + def from_bin(bin) do + try do + {:ok, %__MODULE__{child_nums: Enum.reverse(from_bin(bin, []))}} + rescue + _e in ArgumentError -> {:error, "invalid binary encoding of derivation path"} + end + end + + defp from_bin(<<>>, child_nums), do: child_nums + + defp from_bin(<>, child_nums), + do: from_bin(bin, [level | child_nums]) + defp str_to_level(level) do {num, is_hardened} = case String.split(level, ["'", "h"]) do @@ -111,6 +191,7 @@ defmodule Bitcoinex.ExtendedKey do nnum = String.to_integer(num) + # TODO benchmark and make this two comparisons if nnum in @min_non_hardened_child_num..@max_non_hardened_child_num do if is_hardened do nnum + @min_hardened_child_num @@ -124,6 +205,8 @@ defmodule Bitcoinex.ExtendedKey do def add(%__MODULE__{child_nums: path1}, %__MODULE__{child_nums: path2}), do: %__MODULE__{child_nums: path1 ++ path2} + + def depth(%__MODULE__{child_nums: child_nums}), do: length(child_nums) end @type t :: %__MODULE__{ @@ -245,11 +328,25 @@ defmodule Bitcoinex.ExtendedKey do # PARSE & SERIALIZE @doc """ - parse_extended_key takes binary or string representation + parse! calls parse, which takes binary or string representation + of an extended key and parses it to an extended key object. + parse! raises ArgumentError on failure. + """ + @spec parse!(binary) :: t() + def parse!(xpub) do + case parse(xpub) do + {:ok, res} -> res + {:error, msg} -> raise(ArgumentError, message: msg) + end + end + + @doc """ + parse takes binary or string representation of an extended key and parses it to an extended key object + returns {:error, msg} on failure """ - @spec parse_extended_key(binary) :: {:ok, t()} | {:error, String.t()} - def parse_extended_key( + @spec parse(binary) :: {:ok, t()} | {:error, String.t()} + def parse( xkey = <> + ) do + xkey + |> Base58.append_checksum() + |> parse() + end + # parse from string - def parse_extended_key(xkey) do + def parse(xkey) do case Base58.decode(xkey) do {:error, _} -> {:error, "error parsing key"} @@ -292,7 +400,7 @@ defmodule Bitcoinex.ExtendedKey do {:ok, xkey} -> xkey |> Base58.append_checksum() - |> parse_extended_key() + |> parse() end end @@ -303,23 +411,33 @@ defmodule Bitcoinex.ExtendedKey do end @doc """ - serialize_extended_key takes an extended key + serialize takes an extended key and returns the binary """ - @spec serialize_extended_key(t()) :: binary - def serialize_extended_key(xkey) do - (xkey.prefix <> - xkey.depth <> xkey.parent_fingerprint <> xkey.child_num <> xkey.chaincode <> xkey.key) - |> Base58.append_checksum() + @spec serialize(t(), list({:with_checksum?, boolean})) :: binary + def serialize(xkey, opts \\ []) do + with_checksum? = Keyword.get(opts, :with_checksum?, true) + + extended_key_without_checksum_bin = + xkey.prefix <> + xkey.depth <> xkey.parent_fingerprint <> xkey.child_num <> xkey.chaincode <> xkey.key + + case with_checksum? do + true -> + Base58.append_checksum(extended_key_without_checksum_bin) + + false -> + extended_key_without_checksum_bin + end end @doc """ display returns the extended key as a string """ - @spec display_extended_key(t()) :: String.t() - def display_extended_key(xkey) do + @spec display(t()) :: String.t() + def display(xkey) do xkey - |> serialize_extended_key() + |> serialize() |> Base58.encode_base() end @@ -339,7 +457,7 @@ defmodule Bitcoinex.ExtendedKey do (prefix <> depth_fingerprint_childnum <> chaincode <> <<0>> <> key) |> Base58.append_checksum() - |> parse_extended_key() + |> parse() else {:error, "invalid extended private key prefix"} end @@ -368,7 +486,7 @@ defmodule Bitcoinex.ExtendedKey do |> Kernel.<>(xprv.chaincode) |> Kernel.<>(pubkey) |> Base58.append_checksum() - |> parse_extended_key() + |> parse() rescue _ in MatchError -> {:error, "invalid private key"} end @@ -475,7 +593,7 @@ defmodule Bitcoinex.ExtendedKey do (xkey.prefix <> child_depth <> fingerprint <> i <> child_chaincode <> Point.sec(pubkey)) |> Base58.append_checksum() - |> parse_extended_key() + |> parse() end end end @@ -526,7 +644,7 @@ defmodule Bitcoinex.ExtendedKey do (xkey.prefix <> child_depth <> fingerprint <> i <> child_chaincode <> <<0>> <> child_key) |> Base58.append_checksum() - |> parse_extended_key() + |> parse() rescue _ in MatchError -> {:error, "invalid private key in extended private key"} end diff --git a/lib/lightning_network/invoice.ex b/lib/lightning_network/invoice.ex index e216318..9819006 100644 --- a/lib/lightning_network/invoice.ex +++ b/lib/lightning_network/invoice.ex @@ -150,7 +150,7 @@ defmodule Bitcoinex.LightningNetwork.Invoice do # TODO if destination exist from tagged field, we dun need to recover but to verify it with signature # but that require convert lg sig before using secp256k1 to verify it - # TODO refactor too nested + # TODO refactor to nested case Bitcoinex.Secp256k1.Ecdsa.ecdsa_recover_compact(hash, signature, recoveryId) do {:ok, pubkey} -> if is_nil(destination) or destination == pubkey do diff --git a/lib/opcode.ex b/lib/opcode.ex index 31e7c93..1fe6c0b 100644 --- a/lib/opcode.ex +++ b/lib/opcode.ex @@ -227,7 +227,9 @@ defmodule Bitcoinex.Opcode do op_codeseparator: 0xAB, op_checksig: 0xAC, op_checksigverify: 0xAD, + # disabled in tapscript op_checkmultisig: 0xAE, + # disabled in tapscript op_checkmultisigverify: 0xAF, op_nop1: 0xB0, op_nop2: 0xB1, @@ -240,6 +242,7 @@ defmodule Bitcoinex.Opcode do op_nop8: 0xB7, op_nop9: 0xB8, op_nop10: 0xB9, + op_checksigadd: 0xBA, op_smallinteger: 0xFA, op_pubkeys: 0xFB, op_pubkeyhash: 0xFD, diff --git a/lib/psbt.ex b/lib/psbt.ex index e0553f4..61af54e 100644 --- a/lib/psbt.ex +++ b/lib/psbt.ex @@ -11,6 +11,8 @@ defmodule Bitcoinex.PSBT do alias Bitcoinex.PSBT.Global alias Bitcoinex.PSBT.In alias Bitcoinex.PSBT.Out + alias Bitcoinex.PSBT.Utils + alias Bitcoinex.Transaction alias Bitcoinex.Transaction.Utils, as: TxUtils @type t() :: %__MODULE__{} @@ -24,6 +26,7 @@ defmodule Bitcoinex.PSBT do @magic 0x70736274 @separator 0xFF + @spec separator :: 255 def separator, do: @separator @doc """ @@ -77,12 +80,21 @@ defmodule Bitcoinex.PSBT do |> Base.encode64() end - defp parse(<<@magic::big-size(32), @separator::big-size(8), psbt::binary>>) do + def parse(<<@magic::big-size(32), @separator::big-size(8), psbt::binary>>) do # key-value pairs for all global data {global, psbt} = Global.parse_global(psbt) - in_counter = length(global.unsigned_tx.inputs) + + {in_counter, out_counter} = + cond do + # either unsigned_tx must be present for v0 or in/out count must be present for v2 PSBT + global.unsigned_tx != nil -> + {length(global.unsigned_tx.inputs), length(global.unsigned_tx.outputs)} + + global.input_count != nil && global.output_count != nil -> + {global.input_count, global.output_count} + end + {inputs, psbt} = In.parse_inputs(psbt, in_counter) - out_counter = length(global.unsigned_tx.outputs) {outputs, _} = Out.parse_outputs(psbt, out_counter) {:ok, @@ -92,6 +104,47 @@ defmodule Bitcoinex.PSBT do outputs: outputs }} end + + @spec from_tx(Transaction.t()) :: {:ok, PSBT.t()} + def from_tx(tx) do + inputs = In.from_tx_inputs(tx.inputs, tx.witnesses) + outputs = Out.from_tx_outputs(tx.outputs) + + {:ok, + %PSBT{ + global: Global.from_tx(tx), + inputs: inputs, + outputs: outputs + }} + end + + def to_tx(psbt) do + tx = psbt.global.unsigned_tx + + inputs = In.populate_script_sigs(tx.inputs, psbt.inputs) + + witnesses = In.populate_witnesses(psbt.inputs) + + %Bitcoinex.Transaction{tx | witnesses: witnesses, inputs: inputs} + end + + @spec add_global_field(PSBT.t(), atom, any) :: PSBT.t() + def add_global_field(psbt, field, value) do + global = Global.add_field(psbt.global, field, value) + %PSBT{psbt | global: global} + end + + @spec add_input_field(PSBT.t(), integer, atom, any) :: PSBT.t() + def add_input_field(psbt, input_idx, field, value) do + inputs = Utils.set_item_field(psbt.inputs, input_idx, &In.add_field/3, field, value) + %PSBT{psbt | inputs: inputs} + end + + @spec set_output_field(PSBT.t(), non_neg_integer, atom, any) :: PSBT.t() + def set_output_field(psbt, output_idx, field, value) do + outputs = Utils.set_item_field(psbt.outputs, output_idx, &Out.add_field/3, field, value) + %PSBT{psbt | outputs: outputs} + end end defmodule Bitcoinex.PSBT.Utils do @@ -99,6 +152,8 @@ defmodule Bitcoinex.PSBT.Utils do Contains utility functions used throughout PSBT serialization. """ alias Bitcoinex.Transaction.Utils, as: TxUtils + alias Bitcoinex.Utils + alias Bitcoinex.ExtendedKey.DerivationPath def parse_compact_size_value(key_value) do {len, key_value} = TxUtils.get_counter(key_value) @@ -123,10 +178,64 @@ defmodule Bitcoinex.PSBT.Utils do end def serialize_kv(key, val) do - key_len = TxUtils.serialize_compact_size_unsigned_int(byte_size(key)) - val_len = TxUtils.serialize_compact_size_unsigned_int(byte_size(val)) + key_len = Utils.serialize_compact_size_unsigned_int(byte_size(key)) + val_len = Utils.serialize_compact_size_unsigned_int(byte_size(val)) key_len <> key <> val_len <> val end + + @spec serialize_repeatable_fields(atom, list(any), any) :: binary + def serialize_repeatable_fields(_, nil, _), do: <<>> + + def serialize_repeatable_fields(field, values, serialize_func) do + for(kv <- values, do: serialize_func.(field, kv)) + |> :erlang.list_to_binary() + end + + @spec parse_fingerprint_path(<<_::32, _::_*8>>) :: {<<_::32>>, DerivationPath.t()} + def parse_fingerprint_path(data) do + <> = data + {:ok, path} = DerivationPath.parse(path_bin) + {pfp, path} + end + + # reuse this elsewhere + @spec serialize_fingerprint_path(binary, DerivationPath.t()) :: binary + def serialize_fingerprint_path(pfp, path) do + {:ok, path_bin} = DerivationPath.serialize(path) + pfp <> path_bin + end + + def parse_leaf_hashes(value, leaf_hash_ct) do + leaf_hashes_byte_size = 32 * leaf_hash_ct + <> = value + + leaf_hashes = + leaf_hashes + |> :erlang.binary_to_list() + |> Enum.chunk_every(32) + |> Enum.map(&:erlang.list_to_binary/1) + + {leaf_hashes, value} + end + + @spec serialize_leaf_hashes(list(binary)) :: binary + def serialize_leaf_hashes(leaf_hashes) do + leaf_hashes_bin = Enum.reduce(leaf_hashes, <<>>, fn leaf_hash, acc -> acc <> leaf_hash end) + Utils.serialize_compact_size_unsigned_int(length(leaf_hashes)) <> leaf_hashes_bin + end + + @spec append(nil | list, any) :: [any] + def append(nil, item), do: [item] + def append(items, item), do: items ++ [item] + + def set_item_field(items, idx, add_field_func, field, value) do + item = + items + |> Enum.at(idx) + |> add_field_func.(field, value) + + List.replace_at(items, idx, item) + end end defmodule Bitcoinex.PSBT.Global do @@ -135,35 +244,114 @@ defmodule Bitcoinex.PSBT.Global do """ alias Bitcoinex.PSBT.Global alias Bitcoinex.Transaction + alias Bitcoinex.Utils alias Bitcoinex.Transaction.Utils, as: TxUtils alias Bitcoinex.PSBT.Utils, as: PsbtUtils - alias Bitcoinex.Base58 + alias Bitcoinex.ExtendedKey + alias Bitcoinex.ExtendedKey.DerivationPath, as: DerivationPath defstruct [ :unsigned_tx, :xpub, + :tx_version, + :fallback_locktime, + :input_count, + :output_count, + :tx_modifiable, :version, - :proprietary + :proprietary, + :unknown ] @psbt_global_unsigned_tx 0x00 @psbt_global_xpub 0x01 + @psbt_global_tx_version 0x02 + @psbt_global_fallback_locktime 0x03 + @psbt_global_input_count 0x04 + @psbt_global_output_count 0x05 + @psbt_global_tx_modifiable 0x06 @psbt_global_version 0xFB @psbt_global_proprietary 0xFC + def add_field(global, :unsigned_tx, unsigned_tx = %Transaction{}) + when global.unsigned_tx == nil do + %Global{global | unsigned_tx: unsigned_tx} + end + + def add_field( + global, + :xpub, + global_xpub = %{ + xpub: %ExtendedKey{}, + pfp: <<_::binary-size(4)>>, + derivation: %DerivationPath{} + } + ) do + global_xpubs = PsbtUtils.append(global.xpub, global_xpub) + %Global{global | xpub: global_xpubs} + end + + def add_field(global, :tx_version, value) when global.tx_version == nil and value > 0 do + %Global{global | tx_version: value} + end + + def add_field(global, :fallback_locktime, value) when value >= 0 do + %Global{global | fallback_locktime: value} + end + + def add_field(global, :input_count, input_count) when input_count > 0 do + %Global{global | input_count: input_count} + end + + def add_field(global, :output_count, output_count) when output_count > 0 do + %Global{global | output_count: output_count} + end + + def add_field(global, :tx_modifiable, value) do + %Global{global | tx_modifiable: value} + end + + def add_field(global, :version, value) do + %Global{global | version: value} + end + + # TODO: fix + def add_field(global, :proprietary, value = %{key: k, value: v}) + when is_binary(k) and is_binary(v) do + proprietaries = PsbtUtils.append(global.proprietary, value) + %Global{global | proprietary: proprietaries} + end + + def add_field(global, :unknown, value = %{key: k, value: v}) + when is_binary(k) and is_binary(v) do + unknown = PsbtUtils.append(global.unknown, value) + %Global{global | unknown: unknown} + end + + @spec parse_global(nonempty_binary) :: {Global, binary} def parse_global(psbt) do PsbtUtils.parse_key_value(psbt, %Global{}, &parse/3) end + def from_tx(tx) do + %Global{ + unsigned_tx: tx, + tx_version: tx.version, + input_count: length(tx.inputs), + output_count: length(tx.outputs) + } + end + # unsigned transaction defp parse(<<@psbt_global_unsigned_tx::big-size(8)>>, psbt, global) do {txn_len, psbt} = TxUtils.get_counter(psbt) <> = psbt - # todo, different decode function for txn, directly in bytes - case Transaction.decode(Base.encode16(txn_bytes, case: :lower)) do + + case Transaction.decode(txn_bytes) do {:ok, txn} -> - {%Global{global | unsigned_tx: txn}, psbt} + global = add_field(global, :unsigned_tx, txn) + {global, psbt} {:error, error_msg} -> {:error, error_msg} @@ -173,46 +361,72 @@ defmodule Bitcoinex.PSBT.Global do defp parse(<<@psbt_global_xpub::big-size(8), xpub::binary-size(78)>>, psbt, global) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - <> = value - - indexes = for <>, do: chunk - - global_xpub = - case global.xpub do - nil -> - [ - %{ - xpub: Base58.encode(xpub), - master_pfp: master, - derivation: indexes - } - ] - - _ -> - global.xpub ++ - [ - %{ - xpub: Base58.encode(xpub), - master_pfp: master, - derivation: indexes - } - ] - end + {master, path} = PsbtUtils.parse_fingerprint_path(value) + {:ok, xpub} = ExtendedKey.parse(xpub) - global = %Global{global | xpub: global_xpub} + if :binary.decode_unsigned(xpub.depth) != DerivationPath.depth(path), + do: + raise(ArgumentError, + message: "invalid xpub in PSBT: depth does not match number of indexes provided" + ) + global_xpub = %{ + xpub: xpub, + pfp: master, + derivation: path + } + + global = add_field(global, :xpub, global_xpub) {global, psbt} end - defp parse(<<@psbt_global_version::big-size(8)>>, psbt, global) do + defp parse(<<@psbt_global_tx_version::big-size(8)>>, psbt, global) do + {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) + global = add_field(global, :tx_version, value) + {global, psbt} + end + + defp parse(<<@psbt_global_fallback_locktime::big-size(8)>>, psbt, global) do + {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) + global = add_field(global, :fallback_locktime, value) + {global, psbt} + end + + defp parse(<<@psbt_global_input_count::big-size(8)>>, psbt, global) do + {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + {input_count, _} = TxUtils.get_counter(value) + global = add_field(global, :input_count, input_count) + {global, psbt} + end + + defp parse(<<@psbt_global_output_count::big-size(8)>>, psbt, global) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + {output_count, _} = TxUtils.get_counter(value) + global = add_field(global, :output_count, output_count) + {global, psbt} + end + + defp parse(<<@psbt_global_tx_modifiable::big-size(8)>>, psbt, global) do + {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) + global = add_field(global, :tx_modifiable, value) + {global, psbt} + end + + defp parse(<<@psbt_global_version::big-size(8)>>, psbt, global) do + {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) global = %Global{global | version: value} {global, psbt} end - defp parse(<<@psbt_global_proprietary::big-size(8)>>, psbt, global) do + defp parse(<<@psbt_global_proprietary::big-size(8), key::binary>>, psbt, global) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - global = %Global{global | proprietary: value} + global = add_field(global, :proprietary, %{key: key, value: value}) + {global, psbt} + end + + defp parse(key, psbt, global) do + {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + global = add_field(global, :unknown, %{key: key, value: value}) {global, psbt} end @@ -222,29 +436,92 @@ defmodule Bitcoinex.PSBT.Global do defp serialize_kv(:xpub, value) when value != nil do key = <<@psbt_global_xpub::big-size(8)>> - {:ok, key_data} = Base58.decode(value.xpub) + key_data = ExtendedKey.serialize(value.xpub, with_checksum?: false) - val = - <> <> - (for(chunk <- value.derivation, do: <>) - |> :erlang.list_to_binary()) + val = PsbtUtils.serialize_fingerprint_path(value.pfp, value.derivation) PsbtUtils.serialize_kv(key <> key_data, val) end + defp serialize_kv(:tx_version, value) when value != nil do + PsbtUtils.serialize_kv(<<@psbt_global_tx_version::big-size(8)>>, <>) + end + + defp serialize_kv(:fallback_locktime, value) when value != nil do + PsbtUtils.serialize_kv( + <<@psbt_global_fallback_locktime::big-size(8)>>, + <> + ) + end + + defp serialize_kv(:input_count, value) when value != nil do + PsbtUtils.serialize_kv( + <<@psbt_global_input_count::big-size(8)>>, + Utils.serialize_compact_size_unsigned_int(value) + ) + end + + defp serialize_kv(:output_count, value) when value != nil do + PsbtUtils.serialize_kv( + <<@psbt_global_output_count::big-size(8)>>, + Utils.serialize_compact_size_unsigned_int(value) + ) + end + + defp serialize_kv(:tx_modifiable, value) when value != nil do + PsbtUtils.serialize_kv(<<@psbt_global_tx_modifiable::big-size(8)>>, <>) + end + + defp serialize_kv(:version, value) when value != nil do + PsbtUtils.serialize_kv( + <<@psbt_global_version::big-size(8)>>, + <> + ) + end + + defp serialize_kv(:proprietary, %{key: k, value: v}) when is_binary(k) and is_binary(v) do + PsbtUtils.serialize_kv(<<@psbt_global_proprietary::big-size(8), k::binary>>, v) + end + + defp serialize_kv(:unknown, %{key: k, value: v}) do + PsbtUtils.serialize_kv(k, v) + end + + @spec serialize_global(Global) :: nonempty_binary def serialize_global(global) do - # TODO: serialize all other fields in global. - serialized_global = serialize_kv(:unsigned_tx, global.unsigned_tx) - - bip32 = - if global.xpub != nil do - for(bip32 <- global.xpub, do: serialize_kv(:xpub, bip32)) - |> :erlang.list_to_binary() - else - <<>> - end + serialized_global = + Enum.reduce( + [ + :unsigned_tx, + :xpub, + :tx_version, + :fallback_locktime, + :input_count, + :output_count, + :tx_modifiable, + :version, + :proprietary, + :unknown + ], + <<>>, + fn k, acc -> + case Map.get(global, k) do + nil -> + acc + + [] -> + acc + + v = [_ | _] -> + acc <> PsbtUtils.serialize_repeatable_fields(k, v, &serialize_kv/2) + + v -> + acc <> serialize_kv(k, v) + end + end + ) - serialized_global <> bip32 <> <<0x00::big-size(8)>> + serialized_global <> <<0x00::big-size(8)>> end end @@ -258,6 +535,9 @@ defmodule Bitcoinex.PSBT.In do alias Bitcoinex.PSBT.In alias Bitcoinex.PSBT.Utils, as: PsbtUtils alias Bitcoinex.Transaction.Utils, as: TxUtils + alias Bitcoinex.Utils + alias Bitcoinex.Script + alias Bitcoinex.Secp256k1.Point defstruct [ :non_witness_utxo, @@ -270,7 +550,23 @@ defmodule Bitcoinex.PSBT.In do :final_scriptsig, :final_scriptwitness, :por_commitment, - :proprietary + :ripemd160, + :sha256, + :hash160, + :hash256, + :previous_txid, + :output_index, + :sequence, + :required_time_locktime, + :required_height_locktime, + :tap_key_sig, + :tap_script_sig, + :tap_leaf_script, + :tap_bip32_derivation, + :tap_internal_key, + :tap_merkle_root, + :proprietary, + :unknown ] @psbt_in_non_witness_utxo 0x00 @@ -283,13 +579,223 @@ defmodule Bitcoinex.PSBT.In do @psbt_in_final_scriptsig 0x07 @psbt_in_final_scriptwitness 0x08 @psbt_in_por_commitment 0x09 + @psbt_in_ripemd160 0x0A + @psbt_in_sha256 0x0B + @psbt_in_hash160 0x0C + @psbt_in_hash256 0x0D + @psbt_in_previous_txid 0x0E + @psbt_in_output_index 0x0F + @psbt_in_sequence 0x10 + @psbt_in_required_time_locktime 0x11 + @psbt_in_required_height_locktime 0x12 + @psbt_in_tap_key_sig 0x13 + @psbt_in_tap_script_sig 0x14 + @psbt_in_tap_leaf_script 0x15 + @psbt_in_tap_bip32_derivation 0x16 + @psbt_in_tap_internal_key 0x17 + @psbt_in_tap_merkle_root 0x18 @psbt_in_proprietary 0xFC + @minimum_time_locktime Transaction.minimum_time_locktime() + @valid_sighash_flags Transaction.valid_sighash_flags() + + def add_field(input, :non_witness_utxo, tx = %Transaction{}) + when input.non_witness_utxo == nil do + %In{input | non_witness_utxo: tx} + end + + def add_field(input, :witness_utxo, utxo = %Out{}) do + %In{input | witness_utxo: utxo} + end + + def add_field(input, :partial_sig, sig = %{public_key: _, signature: _}) do + sigs = PsbtUtils.append(input.partial_sig, sig) + %In{input | partial_sig: sigs} + end + + def add_field(input, :sighash_type, sighash_type) when sighash_type in @valid_sighash_flags do + %In{input | sighash_type: sighash_type} + end + + def add_field(input, :redeem_script, redeem_script) when is_binary(redeem_script) do + {:ok, script} = Script.parse_script(redeem_script) + add_field(input, :redeem_script, script) + end + + def add_field(input, :redeem_script, redeem_script = %Script{}) do + %In{input | redeem_script: redeem_script} + end + + def add_field(input, :witness_script, witness_script) do + {:ok, _} = Base.decode16(witness_script, case: :lower) + %In{input | witness_script: witness_script} + end + + def add_field(input, :bip32_derivation, derivation = %{public_key: _, pfp: _, derivation: _}) do + derivations = PsbtUtils.append(input.bip32_derivation, derivation) + %In{input | bip32_derivation: derivations} + end + + def add_field(input, :final_scriptsig, final_scriptsig) when is_binary(final_scriptsig) do + {:ok, script} = Script.parse_script(final_scriptsig) + add_field(input, :final_scriptsig, script) + end + + def add_field(input, :final_scriptsig, final_scriptsig = %Script{}) do + %In{input | final_scriptsig: final_scriptsig} + end + + def add_field(input, :final_scriptwitness, final_scriptwitness = %Transaction.Witness{}) do + %In{input | final_scriptwitness: final_scriptwitness} + end + + def add_field(input, :por_commitment, por_commitment) when is_binary(por_commitment) do + %In{input | por_commitment: por_commitment} + end + + def add_field(input, :ripemd160, ripemd160 = %{hash: h, preimage: p}) + when is_binary(h) and is_binary(p) do + ripemd160s = PsbtUtils.append(input.ripemd160, ripemd160) + %In{input | ripemd160: ripemd160s} + end + + def add_field(input, :sha256, sha256 = %{hash: h, preimage: p}) + when is_binary(h) and is_binary(p) do + sha256s = PsbtUtils.append(input.sha256, sha256) + %In{input | sha256: sha256s} + end + + def add_field(input, :hash160, hash160 = %{hash: h, preimage: p}) + when is_binary(h) and is_binary(p) do + hash160s = PsbtUtils.append(input.hash160, hash160) + %In{input | hash160: hash160s} + end + + def add_field(input, :hash256, hash256 = %{hash: h, preimage: p}) + when is_binary(h) and is_binary(p) do + hash256s = PsbtUtils.append(input.hash256, hash256) + %In{input | hash256: hash256s} + end + + def add_field(input, :previous_txid, <>) do + %In{input | previous_txid: previous_txid} + end + + def add_field(input, :output_index, output_index) + when is_integer(output_index) and output_index >= 0 do + %In{input | output_index: output_index} + end + + def add_field(input, :sequence, sequence) when is_integer(sequence) and sequence >= 0 do + %In{input | sequence: sequence} + end + + def add_field(input, :required_time_locktime, locktime) + when is_integer(locktime) and locktime >= @minimum_time_locktime do + %In{input | required_time_locktime: locktime} + end + + def add_field(input, :required_height_locktime, locktime) + when is_integer(locktime) and locktime < @minimum_time_locktime do + %In{input | required_height_locktime: locktime} + end + + def add_field(input, :tap_key_sig, tap_key_sig) + when is_binary(tap_key_sig) and byte_size(tap_key_sig) in [64, 65] do + %In{input | tap_key_sig: tap_key_sig} + end + + def add_field( + input, + :tap_script_sig, + tap_script_sig = %{public_key: _, leaf_hash: _, signature: _} + ) do + sigs = PsbtUtils.append(input.tap_script_sig, tap_script_sig) + %In{input | tap_script_sig: sigs} + end + + # TODO:taproot make this TapLeaf + def add_field( + input, + :tap_leaf_script, + tap_leaf_script = %{leaf_version: _, script: _, control_block: _} + ) do + scripts = PsbtUtils.append(input.tap_leaf_script, tap_leaf_script) + %In{input | tap_leaf_script: scripts} + end + + def add_field( + input, + :tap_bip32_derivation, + tap_bip32_derivation = %{public_key: _, leaf_hashes: _, pfp: _, derivation: _} + ) do + derivations = PsbtUtils.append(input.tap_bip32_derivation, tap_bip32_derivation) + %In{input | tap_bip32_derivation: derivations} + end + + def add_field(input, :tap_internal_key, <>) do + {:ok, pk} = Point.lift_x(tap_internal_key) + add_field(input, :tap_internal_key, pk) + end + + def add_field(input, :tap_internal_key, tap_internal_key = %Point{}) do + %In{input | tap_internal_key: tap_internal_key} + end + + def add_field(input, :tap_merkle_root, <>) do + %In{input | tap_merkle_root: tap_merkle_root} + end + + def add_field(input, :proprietary, proprietary = %{key: k, value: v}) + when is_binary(k) and is_binary(v) do + proprietaries = PsbtUtils.append(input.proprietary, proprietary) + %In{input | proprietary: proprietaries} + end + + def add_field(input, :unknown, value = %{key: k, value: v}) + when is_binary(k) and is_binary(v) do + unknown = PsbtUtils.append(input.unknown, value) + %In{input | unknown: unknown} + end + def parse_inputs(psbt, num_inputs) do psbt |> parse_input([], num_inputs) end + @spec from_tx_inputs(list(Transaction.In.t()), list(Transaction.Witness.t())) :: list() + def from_tx_inputs(tx_inputs, tx_witnesses) do + inputs_witnesses = Enum.zip(tx_inputs, tx_witnesses) + + Enum.reduce(inputs_witnesses, [], fn {input, witness}, acc -> + [ + %In{ + final_scriptsig: input.script_sig, + final_scriptwitness: witness + } + | acc + ] + end) + |> Enum.reverse() + end + + def populate_script_sigs(tx_inputs, psbt_inputs) do + inputs = Enum.zip(tx_inputs, psbt_inputs) + + Enum.reduce(inputs, [], fn {tx_in, psbt_in}, acc -> + [%Transaction.In{tx_in | script_sig: psbt_in.final_scriptsig} | acc] + end) + |> Enum.reverse() + end + + @spec populate_witnesses(list(In)) :: list(binary) + def populate_witnesses(psbt_inputs) do + Enum.reduce(psbt_inputs, [], fn psbt_in, acc -> + [psbt_in.final_scriptwitness | acc] + end) + |> Enum.reverse() + end + defp serialize_kv(:non_witness_utxo, value) when value != nil do PsbtUtils.serialize_kv(<<@psbt_in_non_witness_utxo::big-size(8)>>, TxUtils.serialize(value)) end @@ -299,33 +805,33 @@ defmodule Bitcoinex.PSBT.In do val = <> <> - TxUtils.serialize_compact_size_unsigned_int(byte_size(script)) <> script + Utils.serialize_compact_size_unsigned_int(byte_size(script)) <> script PsbtUtils.serialize_kv(<<@psbt_in_witness_utxo::big-size(8)>>, val) end defp serialize_kv(:partial_sig, value) when value != nil do - key_data = Base.decode16!(value.public_key, case: :lower) + key_data = Point.sec(value.public_key) val = Base.decode16!(value.signature, case: :lower) PsbtUtils.serialize_kv(<<@psbt_in_partial_sig::big-size(8)>> <> key_data, val) end defp serialize_kv(:sighash_type, value) when value != nil do - PsbtUtils.serialize_kv(<<@psbt_in_sighash_type::big-size(8)>>, value) + PsbtUtils.serialize_kv(<<@psbt_in_sighash_type::big-size(8)>>, <>) end defp serialize_kv(:final_scriptsig, value) when value != nil do PsbtUtils.serialize_kv( <<@psbt_in_final_scriptsig::big-size(8)>>, - Base.decode16!(value, case: :lower) + Script.serialize_script(value) ) end defp serialize_kv(:redeem_script, value) when value != nil do PsbtUtils.serialize_kv( <<@psbt_in_redeem_script::big-size(8)>>, - Base.decode16!(value, case: :lower) + Script.serialize_script(value) ) end @@ -344,16 +850,119 @@ defmodule Bitcoinex.PSBT.In do end defp serialize_kv(:bip32_derivation, value) when value != nil do - key_data = Base.decode16!(value.public_key, case: :lower) + key_data = Point.sec(value.public_key) - val = - <> <> - (for(chunk <- value.derivation, do: <>) - |> :erlang.list_to_binary()) + val = PsbtUtils.serialize_fingerprint_path(value.pfp, value.derivation) PsbtUtils.serialize_kv(<<@psbt_in_bip32_derivation::big-size(8)>> <> key_data, val) end + defp serialize_kv(:por_commitment, value) when value != nil do + PsbtUtils.serialize_kv(<<@psbt_in_por_commitment::big-size(8)>>, value) + end + + defp serialize_kv(:in_ripemd160, %{hash: hash, preimage: preimage}) do + PsbtUtils.serialize_kv( + <<@psbt_in_ripemd160::big-size(8), hash::binary-size(20)>>, + preimage + ) + end + + defp serialize_kv(:in_sha256, %{hash: hash, preimage: preimage}) do + PsbtUtils.serialize_kv( + <<@psbt_in_sha256::big-size(8), hash::binary-size(32)>>, + preimage + ) + end + + defp serialize_kv(:in_hash160, %{hash: hash, preimage: preimage}) do + PsbtUtils.serialize_kv( + <<@psbt_in_hash160::big-size(8), hash::binary-size(20)>>, + preimage + ) + end + + defp serialize_kv(:in_hash256, %{hash: hash, preimage: preimage}) do + PsbtUtils.serialize_kv( + <<@psbt_in_hash256::big-size(8), hash::binary-size(32)>>, + preimage + ) + end + + defp serialize_kv(:previous_txid, <>) do + PsbtUtils.serialize_kv(<<@psbt_in_previous_txid::big-size(8)>>, value) + end + + defp serialize_kv(:output_index, value) when value != nil do + PsbtUtils.serialize_kv(<<@psbt_in_output_index::big-size(8)>>, <>) + end + + defp serialize_kv(:sequence, value) when value != nil do + PsbtUtils.serialize_kv(<<@psbt_in_sequence::big-size(8)>>, <>) + end + + defp serialize_kv(:required_time_locktime, value) when value != nil do + PsbtUtils.serialize_kv( + <<@psbt_in_required_time_locktime::big-size(8)>>, + <> + ) + end + + defp serialize_kv(:required_height_locktime, value) when value != nil do + PsbtUtils.serialize_kv( + <<@psbt_in_required_height_locktime::big-size(8)>>, + <> + ) + end + + defp serialize_kv(:tap_key_sig, value) when value != nil do + PsbtUtils.serialize_kv(<<@psbt_in_tap_key_sig::big-size(8)>>, value) + end + + defp serialize_kv(:tap_script_sig, value) when value != nil do + PsbtUtils.serialize_kv( + <<@psbt_in_tap_script_sig::big-size(8), Point.x_bytes(value.public_key)::binary, + value.leaf_hash::binary>>, + value.signature + ) + end + + defp serialize_kv(:tap_leaf_script, value) when value != nil do + # TODO:taproot make this use TapLeaf + script_bytes = Script.serialize_script(value.script) + + PsbtUtils.serialize_kv( + <<@psbt_in_tap_leaf_script::big-size(8), value.control_block::binary>>, + script_bytes <> <> + ) + end + + defp serialize_kv(:tap_bip32_derivation, value) when value != nil do + leaf_hashes = PsbtUtils.serialize_leaf_hashes(value.leaf_hashes) + fingerprint_path = PsbtUtils.serialize_fingerprint_path(value.pfp, value.derivation) + + PsbtUtils.serialize_kv( + <<@psbt_in_tap_bip32_derivation::big-size(8), Point.x_bytes(value.public_key)::binary>>, + leaf_hashes <> fingerprint_path + ) + end + + defp serialize_kv(:tap_internal_key, value) when value != nil do + PsbtUtils.serialize_kv(<<@psbt_in_tap_internal_key::big-size(8)>>, Point.x_bytes(value)) + end + + defp serialize_kv(:tap_merkle_root, <>) do + PsbtUtils.serialize_kv(<<@psbt_in_tap_merkle_root::big-size(8)>>, value) + end + + defp serialize_kv(:proprietary, %{key: k, value: v}) when is_binary(k) and is_binary(v) do + PsbtUtils.serialize_kv(<<@psbt_in_proprietary::big-size(8), k::binary>>, v) + end + + defp serialize_kv(:unknown, %{key: k, value: v}) do + PsbtUtils.serialize_kv(k, v) + end + defp serialize_kv(_key, _value) do <<>> end @@ -376,11 +985,31 @@ defmodule Bitcoinex.PSBT.In do [ :non_witness_utxo, :witness_utxo, - :sighash_type, :partial_sig, + :sighash_type, :redeem_script, + :witness_script, + :bip32_derivation, :final_scriptsig, - :witness_script + :final_scriptwitness, + :por_commitment, + :ripemd160, + :sha256, + :hash160, + :hash256, + :previous_txid, + :output_index, + :sequence, + :required_time_locktime, + :required_height_locktime, + :tap_key_sig, + :tap_script_sig, + :tap_leaf_script, + :tap_bip32_derivation, + :tap_internal_key, + :tap_merkle_root, + :proprietary, + :unknown ], <<>>, fn k, acc -> @@ -388,24 +1017,19 @@ defmodule Bitcoinex.PSBT.In do nil -> acc + [] -> + acc + + v = [_ | _] -> + acc <> PsbtUtils.serialize_repeatable_fields(k, v, &serialize_kv/2) + v -> acc <> serialize_kv(k, v) end end ) - bip32 = - if input.bip32_derivation != nil do - for(bip32 <- input.bip32_derivation, do: serialize_kv(:bip32_derivation, bip32)) - |> :erlang.list_to_binary() - else - <<>> - end - - serialized_input = - serialized_input <> - bip32 <> - serialize_kv(:final_scriptwitness, input.final_scriptwitness) <> <<0x00::big-size(8)>> + serialized_input = serialized_input <> <<0x00::big-size(8)>> serialize_input(inputs, serialized_inputs <> serialized_input) end @@ -414,123 +1038,272 @@ defmodule Bitcoinex.PSBT.In do defp parse_input(psbt, inputs, num_inputs) do case PsbtUtils.parse_key_value(psbt, %In{}, &parse/3) do + # why are we not adding an empty in here? {nil, psbt} -> parse_input(psbt, inputs, num_inputs - 1) {input, psbt} -> - input = - case input do - %{bip32_derivation: bip32_derivation} when is_list(bip32_derivation) -> - %{input | bip32_derivation: Enum.reverse(bip32_derivation)} - - _ -> - input - end - parse_input(psbt, [input | inputs], num_inputs - 1) end end defp parse(<<@psbt_in_non_witness_utxo::big-size(8)>>, psbt, input) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - {:ok, txn} = Transaction.decode(Base.encode16(value, case: :lower)) - input = %In{input | non_witness_utxo: txn} + {:ok, txn} = Transaction.decode(value) + input = add_field(input, :non_witness_utxo, txn) {input, psbt} end defp parse(<<@psbt_in_witness_utxo::big-size(8)>>, psbt, input) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) out = Out.output(value) - input = %In{input | witness_utxo: out} + input = add_field(input, :witness_utxo, out) {input, psbt} end defp parse(<<@psbt_in_partial_sig::big-size(8), public_key::binary-size(33)>>, psbt, input) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{ - input - | partial_sig: %{ - public_key: Base.encode16(public_key, case: :lower), - signature: Base.encode16(value, case: :lower) - } + {:ok, pk} = Point.parse_public_key(public_key) + + partial_sig = %{ + public_key: pk, + signature: Base.encode16(value, case: :lower) } + input = add_field(input, :partial_sig, partial_sig) + {input, psbt} end defp parse(<<@psbt_in_sighash_type::big-size(8)>>, psbt, input) do - {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{input | sighash_type: value} + {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) + input = add_field(input, :sighash_type, value) {input, psbt} end defp parse(<<@psbt_in_redeem_script::big-size(8)>>, psbt, input) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{input | redeem_script: Base.encode16(value, case: :lower)} + input = add_field(input, :redeem_script, value) {input, psbt} end defp parse(<<@psbt_in_witness_script::big-size(8)>>, psbt, input) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{input | witness_script: Base.encode16(value, case: :lower)} + input = add_field(input, :witness_script, Base.encode16(value, case: :lower)) {input, psbt} end defp parse(<<@psbt_in_bip32_derivation::big-size(8), public_key::binary-size(33)>>, psbt, input) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - <> = value - indexes = for <>, do: chunk - - bip32_derivation = - case input.bip32_derivation do - nil -> - [ - %{ - public_key: Base.encode16(public_key, case: :lower), - pfp: pfp, - derivation: indexes - } - ] - - _ -> - [ - %{ - public_key: Base.encode16(public_key, case: :lower), - pfp: pfp, - derivation: indexes - } - | input.bip32_derivation - ] - end + {pfp, path} = PsbtUtils.parse_fingerprint_path(value) + + {:ok, pk} = Point.parse_public_key(public_key) - input = %In{input | bip32_derivation: bip32_derivation} + derivation = %{ + public_key: pk, + pfp: pfp, + derivation: path + } + + input = add_field(input, :bip32_derivation, derivation) {input, psbt} end defp parse(<<@psbt_in_final_scriptsig::big-size(8)>>, psbt, input) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{input | final_scriptsig: Base.encode16(value, case: :lower)} + input = add_field(input, :final_scriptsig, value) {input, psbt} end defp parse(<<@psbt_in_por_commitment::big-size(8)>>, psbt, input) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{input | por_commitment: value} + input = add_field(input, :por_commitment, value) + {input, psbt} + end + + defp parse(<<@psbt_in_ripemd160::big-size(8), hash::binary-size(20)>>, psbt, input) do + # TODO:validation check hash + {preimage, psbt} = PsbtUtils.parse_compact_size_value(psbt) + + data = %{ + hash: hash, + preimage: preimage + } + + input = add_field(input, :ripemd160, data) + {input, psbt} + end + + defp parse(<<@psbt_in_sha256::big-size(8), hash::binary-size(32)>>, psbt, input) do + # TODO:validation check hash + {preimage, psbt} = PsbtUtils.parse_compact_size_value(psbt) + + data = %{ + hash: hash, + preimage: preimage + } + + input = add_field(input, :sha256, data) + {input, psbt} + end + + defp parse(<<@psbt_in_hash160::big-size(8), hash::binary-size(20)>>, psbt, input) do + # TODO:validation check hash + {preimage, psbt} = PsbtUtils.parse_compact_size_value(psbt) + + data = %{ + hash: hash, + preimage: preimage + } + + input = add_field(input, :hash160, data) + {input, psbt} + end + + defp parse(<<@psbt_in_hash256::big-size(8), hash::binary-size(32)>>, psbt, input) do + # TODO:validation check hash + {preimage, psbt} = PsbtUtils.parse_compact_size_value(psbt) + + data = %{ + hash: hash, + preimage: preimage + } + + input = add_field(input, :hash256, data) + {input, psbt} + end + + defp parse(<<@psbt_in_previous_txid::big-size(8)>>, psbt, input) do + {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) + input = add_field(input, :previous_txid, value) + {input, psbt} + end + + defp parse(<<@psbt_in_output_index::big-size(8)>>, psbt, input) do + {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) + input = add_field(input, :output_index, value) + {input, psbt} + end + + defp parse(<<@psbt_in_sequence::big-size(8)>>, psbt, input) do + {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) + input = add_field(input, :sequence, value) + {input, psbt} + end + + defp parse(<<@psbt_in_required_time_locktime::big-size(8)>>, psbt, input) do + {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) + input = add_field(input, :required_time_locktime, value) + {input, psbt} + end + + defp parse(<<@psbt_in_required_height_locktime::big-size(8)>>, psbt, input) do + {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) + input = add_field(input, :required_height_locktime, value) + {input, psbt} + end + + defp parse(<<@psbt_in_tap_key_sig::big-size(8)>>, psbt, input) do + {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + input = add_field(input, :tap_key_sig, value) {input, psbt} end - defp parse(<<@psbt_in_proprietary::big-size(8)>>, psbt, input) do + defp parse( + <<@psbt_in_tap_script_sig::big-size(8), pubkey::binary-size(32), + leaf_hash::binary-size(32)>>, + psbt, + input + ) do + # TODO:validation validate sig len (64|65) {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{input | proprietary: value} + + {:ok, pk} = Point.lift_x(pubkey) + + data = %{ + public_key: pk, + leaf_hash: leaf_hash, + signature: value + } + + input = add_field(input, :tap_script_sig, data) + {input, psbt} + end + + defp parse(<<@psbt_in_tap_leaf_script::big-size(8), control_block::binary>>, psbt, input) do + {tapleaf, psbt} = PsbtUtils.parse_compact_size_value(psbt) + + {leaf_version, script_bytes} = + tapleaf + |> :erlang.binary_to_list() + |> List.pop_at(-1) + + script_bytes = :erlang.list_to_binary(script_bytes) + + {:ok, script} = Script.parse_script(script_bytes) + + data = %{ + # TODO:taproot make this a TapLeaf object + leaf_version: leaf_version, + script: script, + control_block: control_block + } + + input = add_field(input, :tap_leaf_script, data) + {input, psbt} + end + + defp parse(<<@psbt_in_tap_bip32_derivation::big-size(8), pubkey::binary-size(32)>>, psbt, input) do + {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + + {:ok, pk} = Point.lift_x(pubkey) + {leaf_hash_ct, value} = TxUtils.get_counter(value) + {leaf_hashes, value} = PsbtUtils.parse_leaf_hashes(value, leaf_hash_ct) + {pfp, path} = PsbtUtils.parse_fingerprint_path(value) + + derivation = %{ + public_key: pk, + leaf_hashes: leaf_hashes, + pfp: pfp, + derivation: path + } + + input = add_field(input, :tap_bip32_derivation, derivation) + {input, psbt} + end + + defp parse(<<@psbt_in_tap_internal_key::big-size(8)>>, psbt, input) do + {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + input = add_field(input, :tap_internal_key, value) + {input, psbt} + end + + defp parse(<<@psbt_in_tap_merkle_root::big-size(8)>>, psbt, input) do + {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + input = add_field(input, :tap_merkle_root, value) + {input, psbt} + end + + defp parse(<<@psbt_in_proprietary::big-size(8), key::binary>>, psbt, input) do + {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + input = add_field(input, :proprietary, %{key: key, value: value}) {input, psbt} end defp parse(<<@psbt_in_final_scriptwitness::big-size(8)>>, psbt, input) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) value = Witness.witness(value) - input = %In{input | final_scriptwitness: value} + input = add_field(input, :final_scriptwitness, value) + {input, psbt} + end + + defp parse(key, psbt, input) do + {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + kv = %{key: key, value: value} + + input = add_field(input, :unknown, kv) {input, psbt} end end @@ -540,18 +1313,94 @@ defmodule Bitcoinex.PSBT.Out do Output properties of a partially signed bitcoin transaction. """ alias Bitcoinex.PSBT.Out + alias Bitcoinex.Utils alias Bitcoinex.PSBT.Utils, as: PsbtUtils + alias Bitcoinex.Transaction.Utils, as: TxUtils + alias Bitcoinex.Transaction.Out, as: TxOut + alias Bitcoinex.Script + alias Bitcoinex.Secp256k1.Point defstruct [ :redeem_script, :witness_script, :bip32_derivation, - :proprietary + :amount, + :script, + :tap_internal_key, + :tap_tree, + :tap_bip32_derivation, + :proprietary, + :unknown ] @psbt_out_redeem_script 0x00 @psbt_out_scriptwitness 0x01 @psbt_out_bip32_derivation 0x02 + @psbt_out_amount 0x03 + @psbt_out_script 0x04 + @psbt_out_tap_internal_key 0x05 + @psbt_out_tap_tree 0x06 + @psbt_out_tap_bip32_derivation 0x07 + @psbt_out_proprietary 0xFC + + def add_field(output, :redeem_script, script_bytes) + when is_binary(script_bytes) and output.script_bytes == nil do + {:ok, redeem_script} = Script.parse_script(script_bytes) + add_field(output, :redeem_script, redeem_script) + end + + def add_field(output, :redeem_script, script = %Script{}) do + %Out{output | redeem_script: script} + end + + def add_field(output, :witness_script, witness_script) + when is_binary(witness_script) and output.witness_script == nil do + %Out{output | witness_script: witness_script} + end + + def add_field(output, :bip32_derivation, derivation = %{public_key: _, pfp: _, derivation: _}) do + # ensure no duplicate keys? + derivations = PsbtUtils.append(output.bip32_derivation, derivation) + %Out{output | bip32_derivation: derivations} + end + + def add_field(output, :amount, amount) when is_integer(amount) and amount >= 0 do + %Out{output | amount: amount} + end + + def add_field(output, :script, script_bytes) when is_binary(script_bytes) do + {:ok, script} = Script.parse_script(script_bytes) + add_field(output, :script, script) + end + + def add_field(output, :script, script = %Script{}) do + %Out{output | script: script} + end + + def add_field(output, :tap_internal_key, pk) when is_binary(pk) do + {:ok, pk} = Point.lift_x(pk) + %Out{output | tap_internal_key: pk} + end + + # TODO:taproot find a good format for taptree + def add_field(output, :tap_tree, tree) do + %Out{output | tap_tree: tree} + end + + def add_field( + output, + :tap_bip32_derivation, + derivation = %{public_key: _, leaf_hashes: _, pfp: _, derivation: _} + ) do + derivations = PsbtUtils.append(output.tap_bip32_derivation, derivation) + %Out{output | tap_bip32_derivation: derivations} + end + + # TODO: fix + def add_field(output, :proprietary, kv = %{key: _, value: _}) do + kvs = PsbtUtils.append(output.proprietary, kv) + %Out{output | proprietary: kvs} + end def serialize_outputs(outputs) when is_list(outputs) and length(outputs) > 0 do serialize_output(outputs, <<>>) @@ -561,10 +1410,16 @@ defmodule Bitcoinex.PSBT.Out do <<>> end + @spec from_tx_outputs(list(TxOut)) :: list(Out) + def from_tx_outputs(tx_outputs) do + Enum.reduce(tx_outputs, [], fn _, acc -> [%Out{} | acc] end) + |> Enum.reverse() + end + defp serialize_kv(:redeem_script, value) when value != nil do PsbtUtils.serialize_kv( <<@psbt_out_redeem_script::big-size(8)>>, - Base.decode16!(value, case: :lower) + Script.serialize_script(value) ) end @@ -576,45 +1431,87 @@ defmodule Bitcoinex.PSBT.Out do end defp serialize_kv(:bip32_derivation, value) when value != nil do - key_data = Base.decode16!(value.public_key, case: :lower) - - val = - <> <> - (for(chunk <- value.derivation, do: <>) - |> :erlang.list_to_binary()) + key_data = Point.sec(value.public_key) + val = PsbtUtils.serialize_fingerprint_path(value.pfp, value.derivation) PsbtUtils.serialize_kv(<<@psbt_out_bip32_derivation::big-size(8)>> <> key_data, val) end + defp serialize_kv(:amount, value) when value != nil do + PsbtUtils.serialize_kv(<<@psbt_out_amount::big-size(8)>>, <>) + end + + defp serialize_kv(:script, value) when value != nil do + PsbtUtils.serialize_kv(<<@psbt_out_script::big-size(8)>>, Script.serialize_script(value)) + end + + defp serialize_kv(:tap_internal_key, value) when value != nil do + PsbtUtils.serialize_kv(<<@psbt_out_tap_internal_key::big-size(8)>>, Point.x_bytes(value)) + end + + defp serialize_kv(:tap_tree, value) when value != nil do + tree = serialize_tap_tree(value.leaves) + PsbtUtils.serialize_kv(<<@psbt_out_tap_tree::big-size(8)>>, tree) + end + + defp serialize_kv(:tap_bip32_derivation, value) when value != nil do + key = <<@psbt_out_tap_bip32_derivation::big-size(8)>> <> Point.x_bytes(value.public_key) + leaf_hashes = PsbtUtils.serialize_leaf_hashes(value.leaf_hashes) + fingerprint_path = PsbtUtils.serialize_fingerprint_path(value.pfp, value.derivation) + + PsbtUtils.serialize_kv(key, leaf_hashes <> fingerprint_path) + end + + defp serialize_kv(:proprietary, %{key: k, value: v}) when is_binary(k) and is_binary(v) do + PsbtUtils.serialize_kv(<<@psbt_out_proprietary::big-size(8), k::binary>>, v) + end + + defp serialize_kv(:unknown, %{key: k, value: v}) when is_binary(k) and is_binary(v) do + PsbtUtils.serialize_kv(k, v) + end + defp serialize_kv(_key, _value) do <<>> end - defp serialize_output([], serialize_outputs), do: serialize_outputs + defp serialize_output([], serialized_outputs), do: serialized_outputs defp serialize_output(outputs, serialized_outputs) do [output | outputs] = outputs serialized_output = - case output do - %Out{bip32_derivation: nil, proprietary: nil, redeem_script: nil, witness_script: nil} -> - <<0x00::big-size(8)>> - - _ -> - serialized_output = - serialize_kv(:redeem_script, output.redeem_script) <> - serialize_kv(:witness_script, output.witness_script) - - bip32 = - if output.bip32_derivation != nil do - for(bip32 <- output.bip32_derivation, do: serialize_kv(:bip32_derivation, bip32)) - |> :erlang.list_to_binary() - else - <<>> - end - - serialized_output <> bip32 <> <<0x00::big-size(8)>> - end + Enum.reduce( + [ + :redeem_script, + :witness_script, + :bip32_derivation, + :amount, + :script, + :tap_internal_key, + :tap_tree, + :tap_bip32_derivation, + :proprietary, + :unknown + ], + <<>>, + fn k, acc -> + case Map.get(output, k) do + nil -> + acc + + [] -> + acc + + v = [_ | _] -> + acc <> PsbtUtils.serialize_repeatable_fields(k, v, &serialize_kv/2) + + v -> + acc <> serialize_kv(k, v) + end + end + ) + + serialized_output = serialized_output <> <<0x00::big-size(8)>> serialize_output(outputs, serialized_outputs <> serialized_output) end @@ -626,38 +1523,19 @@ defmodule Bitcoinex.PSBT.Out do defp parse_output(psbt, outputs, 0), do: {Enum.reverse(outputs), psbt} defp parse_output(psbt, outputs, num_outputs) do - case PsbtUtils.parse_key_value(psbt, %Out{}, &parse/3) do - {output = %Out{ - bip32_derivation: nil, - proprietary: nil, - redeem_script: nil, - witness_script: nil - }, psbt} -> - parse_output(psbt, [output | outputs], num_outputs - 1) - - {output, psbt} -> - output = - case output do - %{bip32_derivation: bip32_derivation} when is_list(bip32_derivation) -> - %{output | bip32_derivation: Enum.reverse(bip32_derivation)} - - _ -> - output - end - - parse_output(psbt, [output | outputs], num_outputs - 1) - end + {output, psbt} = PsbtUtils.parse_key_value(psbt, %Out{}, &parse/3) + parse_output(psbt, [output | outputs], num_outputs - 1) end defp parse(<<@psbt_out_redeem_script::big-size(8)>>, psbt, output) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - output = %Out{output | redeem_script: Base.encode16(value, case: :lower)} + output = add_field(output, :redeem_script, value) {output, psbt} end defp parse(<<@psbt_out_scriptwitness::big-size(8)>>, psbt, output) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - output = %Out{output | witness_script: Base.encode16(value, case: :lower)} + output = add_field(output, :witness_script, Base.encode16(value, case: :lower)) {output, psbt} end @@ -668,36 +1546,105 @@ defmodule Bitcoinex.PSBT.Out do ) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - <> = value - indexes = for <>, do: chunk - - bip32_derivation = - case output.bip32_derivation do - nil -> - [ - %{ - public_key: Base.encode16(public_key, case: :lower), - pfp: pfp, - derivation: indexes - } - ] - - _ -> - [ - %{ - public_key: Base.encode16(public_key, case: :lower), - pfp: pfp, - derivation: indexes - } - | output.bip32_derivation - ] - end + {:ok, pk} = Point.parse_public_key(public_key) + + {pfp, path} = PsbtUtils.parse_fingerprint_path(value) + + derivation = %{ + public_key: pk, + pfp: pfp, + derivation: path + } + + output = add_field(output, :bip32_derivation, derivation) + {output, psbt} + end + + defp parse(<<@psbt_out_amount::big-size(8)>>, psbt, output) do + {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) + output = add_field(output, :amount, amount) + {output, psbt} + end + + defp parse(<<@psbt_out_script::big-size(8)>>, psbt, output) do + {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + output = add_field(output, :script, value) + {output, psbt} + end + + defp parse(<<@psbt_out_tap_internal_key::big-size(8)>>, psbt, output) do + {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + output = add_field(output, :tap_internal_key, value) + {output, psbt} + end + + defp parse(<<@psbt_out_tap_tree::big-size(8)>>, psbt, output) do + {tree, psbt} = PsbtUtils.parse_compact_size_value(psbt) + leaves = parse_tap_tree(tree, []) + # hack to ensure tap_tree is not treated like a repeatable field + output = add_field(output, :tap_tree, %{leaves: leaves}) + {output, psbt} + end + + defp parse( + <<@psbt_out_tap_bip32_derivation::big-size(8), pubkey::binary-size(32)>>, + psbt, + output = %Out{} + ) do + {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + + {leaf_hash_ct, value} = TxUtils.get_counter(value) + {leaf_hashes, value} = PsbtUtils.parse_leaf_hashes(value, leaf_hash_ct) + {pfp, path} = PsbtUtils.parse_fingerprint_path(value) - output = %Out{ - output - | bip32_derivation: bip32_derivation + {:ok, pk} = Point.lift_x(pubkey) + + derivation = %{ + public_key: pk, + leaf_hashes: leaf_hashes, + pfp: pfp, + derivation: path } + output = add_field(output, :tap_bip32_derivation, derivation) {output, psbt} end + + # TODO: fix + defp parse(<<@psbt_out_proprietary::big-size(8)>>, psbt, output) do + {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + output = add_field(output, :proprietary, value) + {output, psbt} + end + + defp parse_tap_tree(<<>>, scripts), do: Enum.reverse(scripts) + + defp parse_tap_tree(tree, scripts) do + <> = tree + {script, tree} = PsbtUtils.parse_compact_size_value(rest) + {:ok, script} = Script.parse_script(script) + + data = %{ + # TODO:taproot make this TapLeaf + depth: depth, + leaf_version: leaf_version, + script: script + } + + # TODO:taproot ideally we can build an actual binary tree not just a list. + # But this is only useful once taproot is merged in + parse_tap_tree(tree, [data | scripts]) + end + + defp serialize_tap_tree(leaves) do + Enum.reduce(leaves, <<>>, fn leaf, acc -> + # TODO:taproot use Script.serialize_with_compact_size + script_bytes = Script.serialize_script(leaf.script) + + acc <> + <> <> + Utils.serialize_compact_size_unsigned_int(byte_size(script_bytes)) <> + script_bytes + end) + end end diff --git a/lib/script.ex b/lib/script.ex index 342a65d..c696299 100644 --- a/lib/script.ex +++ b/lib/script.ex @@ -5,14 +5,17 @@ defmodule Bitcoinex.Script do import Bitcoinex.Opcode - alias Bitcoinex.Secp256k1.Point + alias Bitcoinex.Secp256k1.{Point, Math, PrivateKey} - alias Bitcoinex.{Utils, Address, Segwit, Base58, Network} + alias Bitcoinex.{Utils, Address, Segwit, Base58, Network, Taproot} @wsh_length 32 @tapkey_length 32 @h160_length 20 - @pubkey_lengths [33, 65] + @pubkey_lengths [@tapkey_length, 33, 65] + + # hash of G.x, used to construct unsolvable internal taproot keys + @h 0x50929B74C1A04954B78B4B6035E97A5E078A5A0F28EC96D547BFEE9ACE803AC0 @type script_type :: :p2pk | :p2pkh | :p2sh | :p2wpkh | :p2wsh | :p2tr | :multi | :non_standard @@ -25,7 +28,7 @@ defmodule Bitcoinex.Script do ] defstruct [:items] - defguard is_valid_multi(m, pubkeys) + defguard is_valid_multisig(m, pubkeys) when is_integer(m) and m > 0 and length(pubkeys) > 0 and length(pubkeys) >= m defp invalid_opcode_error(msg), do: {:error, "invalid opcode: #{msg}"} @@ -158,6 +161,18 @@ defmodule Bitcoinex.Script do end end + @spec push_num(t(), non_neg_integer()) :: {:ok, t()} + def push_num(%__MODULE__{items: stack}, num) when num >= 0 and num <= 16 do + op_num = num_to_op_num(num) + {:ok, %__MODULE__{items: [op_num | stack]}} + end + + def num_to_op_num(0), do: 0 + + def num_to_op_num(num) when num > 0 and num <= 16 do + 0x50 + num + end + # SERIALIZE & PARSE defp serializer(%__MODULE__{items: []}, acc), do: acc @@ -204,6 +219,11 @@ defmodule Bitcoinex.Script do serializer(script, <<>>) end + def serialize_with_compact_size(script = %__MODULE__{}) do + s = serialize_script(script) + Utils.serialize_compact_size_unsigned_int(byte_size(s)) <> s + end + @doc """ to_hex returns the hex of a serialized script. """ @@ -331,7 +351,7 @@ defmodule Bitcoinex.Script do true end - def is_p2pk?(%__MODULE__{}), do: false + def is_p2pk?(_), do: false @doc """ is_p2pkh? returns whether a given script is of the p2pkh format: @@ -343,7 +363,7 @@ defmodule Bitcoinex.Script do }), do: true - def is_p2pkh?(%__MODULE__{}), do: false + def is_p2pkh?(_), do: false @doc """ is_p2sh? returns whether a given script is of the p2sh format: @@ -353,7 +373,7 @@ defmodule Bitcoinex.Script do def is_p2sh?(%__MODULE__{items: [0xA9, @h160_length, <<_::binary-size(@h160_length)>>, 0x87]}), do: true - def is_p2sh?(%__MODULE__{}), do: false + def is_p2sh?(_), do: false @doc """ is_p2wpkh? returns whether a given script is of the p2wpkh format: @@ -363,7 +383,7 @@ defmodule Bitcoinex.Script do def is_p2wpkh?(%__MODULE__{items: [0x00, @h160_length, <<_::binary-size(@h160_length)>>]}), do: true - def is_p2wpkh?(%__MODULE__{}), do: false + def is_p2wpkh?(_), do: false @doc """ is_p2wsh? returns whether a given script is of the p2wsh format: @@ -373,7 +393,7 @@ defmodule Bitcoinex.Script do def is_p2wsh?(%__MODULE__{items: [0x00, @wsh_length, <<_::binary-size(@wsh_length)>>]}), do: true - def is_p2wsh?(%__MODULE__{}), do: false + def is_p2wsh?(_), do: false @doc """ is_p2tr? returns whether a given script is of the p2tr format: @@ -383,39 +403,39 @@ defmodule Bitcoinex.Script do def is_p2tr?(%__MODULE__{items: [0x51, @tapkey_length, <<_::binary-size(@tapkey_length)>>]}), do: true - def is_p2tr?(%__MODULE__{}), do: false + def is_p2tr?(_), do: false @doc """ - is_multi? returns whether a given script is of the raw multisig format: + is_multisig? returns whether a given script is of the raw multisig format: OP_(INT) [Public Keys] OP_(INT) OP_CHECKMULTISIG """ - @spec is_multi?(t()) :: boolean - def is_multi?(%__MODULE__{items: [op_m | rest]}) + @spec is_multisig?(t()) :: boolean + def is_multisig?(%__MODULE__{items: [op_m | rest]}) when op_m > 0x50 and op_m <= 0x60 and length(rest) > 3 do - test_multi(rest, 0, op_m) + test_multisig(rest, 0, op_m) end - def is_multi?(_), do: false + def is_multisig?(_), do: false - defp test_multi([op_n, 0xAE], n, m) when op_n == 0x50 + n and m <= op_n, do: true + defp test_multisig([op_n, 0xAE], n, m) when op_n == 0x50 + n and m <= op_n, do: true - defp test_multi([op_push | [pk | rest]], n, m) when op_push in @pubkey_lengths do + defp test_multisig([op_push | [pk | rest]], n, m) when op_push in @pubkey_lengths do case Point.parse_public_key(pk) do - {:ok, _pk} -> test_multi(rest, n + 1, m) + {:ok, _pk} -> test_multisig(rest, n + 1, m) {:error, _msg} -> false end end - defp test_multi(_, _, _), do: false + defp test_multisig(_, _, _), do: false @doc """ - extract_multi_policy takes in a raw multisig script and returns the m, the + extract_multisig_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()) :: + @spec extract_multisig_policy(t()) :: {:ok, non_neg_integer(), list(Point.t())} | {:error, String.t()} - def extract_multi_policy(script = %__MODULE__{items: [op_m | items]}) do - if is_multi?(script) do + def extract_multisig_policy(script = %__MODULE__{items: [op_m | items]}) do + if is_multisig?(script) do {:ok, op_m - 0x50, extractor(items, [])} else {:error, "invalid raw multisig script"} @@ -445,7 +465,7 @@ defmodule Bitcoinex.Script do is_p2wsh?(script) -> :p2wsh is_p2pk?(script) -> :p2pk is_p2tr?(script) -> :p2tr - is_multi?(script) -> :multi + is_multisig?(script) -> :multi true -> :non_standard end end @@ -456,12 +476,13 @@ defmodule Bitcoinex.Script do create_p2pk creates a p2pk script using the passed public key """ @spec create_p2pk(binary) :: {:ok, t()} | {:error, String.t()} - def create_p2pk(pk) when is_binary(pk) and byte_size(pk) in [33, 65] do + def create_p2pk(pk) when is_binary(pk) and byte_size(pk) in @pubkey_lengths do {:ok, s} = push_op(new(), 0xAC) push_data(s, pk) end - def create_p2pk(_), do: {:error, "pubkey must be 33 or 65 bytes compressed or uncompressed SEC"} + def create_p2pk(_), + do: {:error, "pubkey must be 32, 33, 65 bytes compressed or uncompressed SEC"} @doc """ create_p2pkh creates a p2pkh script using the passed 20-byte public key hash @@ -501,44 +522,101 @@ defmodule Bitcoinex.Script do end @doc """ - create_multi creates a raw multisig script using m and the list of public keys. + create_multisig creates a raw multisig script using m and the list of public keys. + this function sorts the pubkeys lexicographically, complying with BIP 67. For a non-compliant + version of this function, use create_unsorted_multisig """ - @spec create_multi(non_neg_integer(), list(Point.t())) :: {:ok, t()} | {:error, String.t()} - def create_multi(m, pubkeys) when is_valid_multi(m, pubkeys) do + @spec create_multisig(non_neg_integer(), list(Point.t()), list({:bip67_sort, boolean})) :: + {:ok, t()} | {:error, String.t()} + def create_multisig(m, pubkeys, opts \\ []) + + def create_multisig(m, pubkeys, opts) when is_valid_multisig(m, pubkeys) do + pubkeys = sort_pubkeys(pubkeys, opts) + try do # checkmultisig {:ok, s} = push_op(new(), 0xAE) {:ok, s} = push_op(s, 0x50 + length(pubkeys)) - s = fill_multi_keys(s, pubkeys) + s = fill_multisig_keys(s, pubkeys) push_op(s, 0x50 + m) rescue _ -> {:error, "invalid public key."} end end - def create_multi(_, _), do: {:error, "invalid multisig: must be of form: (int, list(%Point)"} + def create_multisig(_, _, _), + do: {:error, "invalid multisig: must be of form: (int, list(%Point)"} - defp fill_multi_keys(s, []), do: s + defp fill_multisig_keys(s, []), do: s - defp fill_multi_keys(s, [pk = %Point{} | pubkeys]) do - {:ok, s} = push_data(fill_multi_keys(s, pubkeys), Point.sec(pk)) + defp fill_multisig_keys(s, [pk = %Point{} | pubkeys]) do + {:ok, s} = push_data(fill_multisig_keys(s, pubkeys), Point.sec(pk)) s end - defp fill_multi_keys(_, _), do: raise(ArgumentError) + defp fill_multisig_keys(_, _), do: raise(ArgumentError) + + @spec create_tapscript_multisig( + non_neg_integer(), + list(Point.t()), + list({:bip67_sort, boolean}) + ) :: t() + def create_tapscript_multisig(m, pubkeys, opts \\ []) when is_valid_multisig(m, pubkeys) do + pubkeys = sort_pubkeys(pubkeys, opts) + {:ok, s} = push_op(new(), :op_numequal) + {:ok, s} = push_num(s, m) + fill_tapscript_multisig_keys(s, Enum.reverse(pubkeys)) + end + + # creates a script using the pubkeys *in reverse order*. + defp fill_tapscript_multisig_keys(s, []), do: s + + defp fill_tapscript_multisig_keys(s, [last_key]) do + {:ok, s} = push_op(s, :op_checksig) + {:ok, s} = push_data(s, Point.x_bytes(last_key)) + s + end + + defp fill_tapscript_multisig_keys(s, [key | rest]) do + {:ok, s} = push_op(s, :op_checksigadd) + {:ok, s} = push_data(s, Point.x_bytes(key)) + fill_tapscript_multisig_keys(s, rest) + end + + def sort_pubkeys(pubkeys, opts) do + if Keyword.get(opts, :bip67_sort, true) do + lexicographical_sort_pubkeys(pubkeys) + else + pubkeys + end + end + + # BIP67 + def lexicographical_sort_pubkeys(pubkeys) do + pubkeys + |> Enum.map(fn pubkey -> Point.sec(pubkey) |> :erlang.binary_to_list() end) + |> Enum.sort(&Utils.lexicographical_cmp/2) + |> Enum.map(fn bin_list -> + {:ok, pk} = + :erlang.list_to_binary(bin_list) + |> Point.parse_public_key() + + pk + end) + end @doc """ - create_p2sh_multi returns both a P2SH-wrapped multisig script + create_p2sh_multisig 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())) :: + @spec create_p2sh_multisig(non_neg_integer(), list(Point.t()), list({:bip67_sort, boolean})) :: {:ok, t(), t()} | {:error, String.t()} - def create_p2sh_multi(m, pubkeys) do - case create_multi(m, pubkeys) do - {:ok, multi} -> - h160 = hash160(multi) + def create_p2sh_multisig(m, pubkeys, opts \\ []) do + case create_multisig(m, pubkeys, opts) do + {:ok, multisig} -> + h160 = hash160(multisig) {:ok, p2sh} = create_p2sh(h160) - {:ok, p2sh, multi} + {:ok, p2sh, multisig} {:error, msg} -> {:error, msg} @@ -546,17 +624,17 @@ defmodule Bitcoinex.Script do end @doc """ - create_p2wsh_multi returns both a P2WSH-wrapped multisig script + create_p2wsh_multisig 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())) :: + @spec create_p2wsh_multisig(non_neg_integer(), list(Point.t()), list({:bip67_sort, boolean})) :: {:ok, t(), t()} | {:error, String.t()} - def create_p2wsh_multi(m, pubkeys) do - case create_multi(m, pubkeys) do - {:ok, multi} -> - h256 = sha256(multi) + def create_p2wsh_multisig(m, pubkeys, opts \\ []) do + case create_multisig(m, pubkeys, opts) do + {:ok, multisig} -> + h256 = sha256(multisig) {:ok, p2wsh} = create_p2wsh(h256) - {:ok, p2wsh, multi} + {:ok, p2wsh, multisig} {:error, msg} -> {:error, msg} @@ -603,13 +681,62 @@ defmodule Bitcoinex.Script do @doc """ create_p2tr creates a p2tr script using the passed 32-byte public key - or Point. If a point is passed, it's interpreted as q, the full witness - program or taproot output key per BIP 341 rather than the keyspend pubkey. + or Point. If a point is passed, it's interpreted as p, the internal key. + If only p is passed, the script_tree is assumed to be empty. """ - @spec create_p2tr(binary | Point.t()) :: {:ok, t()} - def create_p2tr(<>), do: create_witness_scriptpubkey(1, pk) - def create_p2tr(q = %Point{}), do: create_witness_scriptpubkey(1, Point.x_bytes(q)) - def create_p2tr(_), do: {:error, "public key must be #{@tapkey_length}-bytes"} + @spec create_p2tr(<<_::256>> | Point.t() | nil, Taproot.script_tree()) :: + {:ok, Bitcoinex.Script.t()} + | {:ok, Bitcoinex.Script.t(), non_neg_integer()} + | {:error, String.t()} + def create_p2tr(p \\ nil, script_tree \\ nil) + def create_p2tr(nil, nil), do: {:error, "script_tree or internal pubkey must be non-nil"} + def create_p2tr(p = %Point{}, script_tree), do: create_p2tr(Point.x_bytes(p), script_tree) + + def create_p2tr(<>, script_tree) do + {_, hash} = Taproot.merkelize_script_tree(script_tree) + {:ok, p} = Point.lift_x(px) + q = Taproot.tweak_pubkey(p, hash) + create_witness_scriptpubkey(1, Point.x_bytes(q)) + end + + def create_p2tr(nil, script_tree) do + r = + 32 + |> :crypto.strong_rand_bytes() + |> :binary.decode_unsigned() + + create_p2tr_script_only(script_tree, r) + end + + @spec create_p2tr_script_only(Taproot.script_tree(), non_neg_integer()) :: + {:ok, Bitcoinex.Script.t(), non_neg_integer()} + def create_p2tr_script_only(script_tree, r) do + p = calculate_unsolvable_internal_key(r) + {:ok, script} = create_p2tr(p, script_tree) + {:ok, script, r} + end + + @spec calculate_unsolvable_internal_key(non_neg_integer) :: + {:error, String.t()} | Bitcoinex.Secp256k1.Point.t() + def calculate_unsolvable_internal_key(r) do + case PrivateKey.new(r) do + {:error, msg} -> + {:error, msg} + + {:ok, sk} -> + {:ok, hk} = Point.lift_x(@h) + + sk + |> PrivateKey.to_point() + |> Math.add(hk) + end + end + + @spec validate_unsolvable_internal_key(t(), Taproot.script_tree(), non_neg_integer) :: boolean + def validate_unsolvable_internal_key(p2tr_script, script_tree, r) do + {:ok, script, _} = create_p2tr_script_only(script_tree, r) + script == p2tr_script + end @doc """ create_p2sh_p2wpkh creates a p2wsh script using the passed 20-byte public key hash diff --git a/lib/secp256k1/point.ex b/lib/secp256k1/point.ex index ff980c1..50259bd 100644 --- a/lib/secp256k1/point.ex +++ b/lib/secp256k1/point.ex @@ -105,6 +105,16 @@ defmodule Bitcoinex.Secp256k1.Point do end end + @doc """ + negate returns the pubkey with the same x but the other y. + It does this by passing y % 2 == 0 as y_is_odd to Secp256k1.get_y. + """ + @spec negate(t()) :: t() + def negate(%__MODULE__{x: x, y: y}) do + {:ok, y} = Secp256k1.get_y(x, (y &&& 1) == 0) + %__MODULE__{x: x, y: y} + end + @doc """ sec serializes a compressed public key to binary """ @@ -124,7 +134,7 @@ defmodule Bitcoinex.Secp256k1.Point do """ @spec x_bytes(t()) :: binary def x_bytes(%__MODULE__{x: x}) do - Bitcoinex.Utils.pad(:binary.encode_unsigned(x), 32, :leading) + Utils.int_to_big(x, 32) end @doc """ diff --git a/lib/secp256k1/privatekey.ex b/lib/secp256k1/privatekey.ex index 734feb7..1f01bf4 100644 --- a/lib/secp256k1/privatekey.ex +++ b/lib/secp256k1/privatekey.ex @@ -60,6 +60,11 @@ defmodule Bitcoinex.Secp256k1.PrivateKey do end end + @spec negate(t()) :: t() + def negate(%__MODULE__{d: d}) do + %__MODULE__{d: @n - d} + end + @doc """ serialize_private_key serializes a private key into hex """ diff --git a/lib/secp256k1/schnorr.ex b/lib/secp256k1/schnorr.ex index 728e3ec..d53b43a 100644 --- a/lib/secp256k1/schnorr.ex +++ b/lib/secp256k1/schnorr.ex @@ -17,6 +17,47 @@ defmodule Bitcoinex.Secp256k1.Schnorr do @spec sign(PrivateKey.t(), non_neg_integer(), non_neg_integer()) :: {:ok, Signature.t()} | {:error, String.t()} def sign(privkey, z, aux) do + case calculate_signature_nonce(privkey, z, aux) do + {:error, msg} -> + {:error, msg} + + {:ok, k, d} -> + sig = sign_with_nonce(d, k, z) + {:ok, sig} + end + end + + @doc """ + sign_with_nonce creates a signature (R,s) from a given + private key sk, nonce k, and sighash z + DANGER: signing different messages with the same sk and k + will leak sk. + """ + def sign_with_nonce(sk, k, z) do + d = Secp256k1.force_even_y(sk) + k = Secp256k1.force_even_y(k) + + case {d, k} do + {{:error, _}, _} -> + {:error, "failed to force signing key even"} + + {_, {:error, _}} -> + {:error, "failed to force nonce secret even"} + + {d = %PrivateKey{}, k = %PrivateKey{}} -> + r_point = PrivateKey.to_point(k) + d_point = PrivateKey.to_point(d) + z_bytes = Utils.int_to_big(z, 32) + e = calculate_e(Point.x_bytes(r_point), Point.x_bytes(d_point), z_bytes) + sig_s = calculate_s(k, sk, e) + %Signature{r: r_point.x, s: sig_s} + end + end + + @spec calculate_signature_nonce(PrivateKey.t(), non_neg_integer(), non_neg_integer()) :: + {:error, String.t()} + | {:ok, PrivateKey.t(), PrivateKey.t()} + def calculate_signature_nonce(privkey, z, aux) do case PrivateKey.validate(privkey) do {:error, msg} -> {:error, msg} @@ -35,35 +76,15 @@ defmodule Bitcoinex.Secp256k1.Schnorr do tagged_aux_hash = tagged_hash_aux(aux_bytes) t = Utils.xor_bytes(d_bytes, tagged_aux_hash) - {:ok, k0} = - tagged_hash_nonce(t <> Point.x_bytes(d_point) <> z_bytes) - |> :binary.decode_unsigned() - |> Math.modulo(@n) - |> PrivateKey.new() - - if k0.d == 0 do - {:error, "invalid aux randomness"} - else - r_point = PrivateKey.to_point(k0) - - case Secp256k1.force_even_y(k0) do - {:error, msg} -> - {:error, msg} - - k -> - e = - tagged_hash_challenge( - Point.x_bytes(r_point) <> Point.x_bytes(d_point) <> z_bytes - ) - |> :binary.decode_unsigned() - |> Math.modulo(@n) - - sig_s = - (k.d + d.d * e) - |> Math.modulo(@n) - - {:ok, %Signature{r: r_point.x, s: sig_s}} - end + case calculate_k(t, d_point, z_bytes) do + {:ok, k0} -> + case Secp256k1.force_even_y(k0) do + {:error, msg} -> + {:error, msg} + + k -> + {:ok, k, d} + end end end end @@ -73,8 +94,72 @@ defmodule Bitcoinex.Secp256k1.Schnorr do defp tagged_hash_nonce(nonce), do: Utils.tagged_hash("BIP0340/nonce", nonce) defp tagged_hash_challenge(chal), do: Utils.tagged_hash("BIP0340/challenge", chal) + defp calculate_r(pubkey, s, e) do + @generator_point + |> Math.multiply(s) + |> Math.add(Math.multiply(pubkey, Params.curve().n - e)) + end + + defp calculate_s(k, d, e) do + (k.d + d.d * e) + |> Math.modulo(@n) + end + + defp calculate_k(t, d_point, z_bytes) do + {:ok, k0} = + tagged_hash_nonce(t <> Point.x_bytes(d_point) <> z_bytes) + |> :binary.decode_unsigned() + |> Math.modulo(@n) + |> PrivateKey.new() + + if k0.d == 0 do + {:error, "invalid aux randomness"} + else + {:ok, Secp256k1.force_even_y(k0)} + end + end + + defp calculate_e(nonce_bytes, pubkey_bytes, msg_bytes) do + tagged_hash_challenge(nonce_bytes <> pubkey_bytes <> msg_bytes) + |> :binary.decode_unsigned() + |> Math.modulo(@n) + end + + # this is just like validate_r but without the R.y evenness check + defp partial_validate_r(r_point, rx) do + cond do + Point.is_inf(r_point) -> + {:error, "R point is infinite"} + + r_point.x != rx -> + {:error, "x's do not match #{r_point.x} vs #{rx}"} + + true -> + true + end + end + + defp validate_r(r_point, rx) do + cond do + Point.is_inf(r_point) -> + # {:error, "R point is infinite"} + false + + !Point.has_even_y(r_point) -> + # {:error, "R point is not even"} + false + + r_point.x != rx -> + # {:error, "x's do not match #{r_point.x} vs #{rx}"} + false + + true -> + true + end + end + @doc """ - verify whether the schnorr signature is valid for the given message hash and public key + verify_signature verifies whether the Schnorr signature is valid for the given message hash and public key """ @spec verify_signature(Point.t(), non_neg_integer, Signature.t()) :: boolean | {:error, String.t()} @@ -85,17 +170,143 @@ defmodule Bitcoinex.Secp256k1.Schnorr do def verify_signature(pubkey, z, %Signature{r: r, s: s}) do r_bytes = Utils.int_to_big(r, 32) z_bytes = Utils.int_to_big(z, 32) + e = calculate_e(r_bytes, Point.x_bytes(pubkey), z_bytes) + r_point = calculate_r(pubkey, s, e) + + validate_r(r_point, r) + end + + # negate a secret + defp conditional_negate(d, true), do: %PrivateKey{d: d} |> PrivateKey.negate() + defp conditional_negate(d, false), do: %PrivateKey{d: d} + + # negate a point (switches parity of P.y) + defp conditional_negate_point(point, true), do: Point.negate(point) + defp conditional_negate_point(point, false), do: point + + # Adaptor/Encrypted Signatures + + @doc """ + encrypted_sign signs a message hash z with Private Key sk but encrypts the signature using the tweak_point + as the encryption key. The signer need not know the decryption key / tweak itself, which can later be used + to decrypt the signature into a valid Schnorr signature. This produces an Adaptor Signature. + """ + @spec encrypted_sign(PrivateKey.t(), non_neg_integer(), non_neg_integer(), Point.t()) :: + {:ok, Signature.t(), boolean} + def encrypted_sign(sk = %PrivateKey{}, z, aux, tweak_point = %Point{}) do + z_bytes = Utils.int_to_big(z, 32) + aux_bytes = Utils.int_to_big(aux, 32) + d_point = PrivateKey.to_point(sk) + + case Secp256k1.force_even_y(sk) do + {:error, msg} -> + {:error, msg} + + d -> + d_bytes = Utils.int_to_big(d.d, 32) + tagged_aux_hash = tagged_hash_aux(aux_bytes) + t = Utils.xor_bytes(d_bytes, tagged_aux_hash) + # TODO always add tweak_point to the nonce to commit to it as well + case calculate_k(t, d_point, z_bytes) do + {:ok, k0} -> + r_point = PrivateKey.to_point(k0) + # ensure that tweak_point has even Y + tweaked_r_point = Math.add(r_point, tweak_point) + # ensure (R+T).y is even, if not, negate it, negate k, and set was_negated = true + {tweaked_r_point, was_negated} = make_point_even(tweaked_r_point) + k = conditional_negate(k0.d, was_negated) + + e = calculate_e(Point.x_bytes(tweaked_r_point), Point.x_bytes(d_point), z_bytes) + s = calculate_s(k, d, e) + # we return Signature{R+T,s}, not a valid signature since s is untweaked. + {:ok, %Signature{r: tweaked_r_point.x, s: s}, was_negated} + end + end + end + + @doc """ + verify_encrypted_signature verifies that an encrypted signature commits to a tweak_point / encryption key. + This is different from a regular Schnorr signature verification, as encrypted signatures are not valid Schnorr Signatures. + """ + @spec verify_encrypted_signature( + Signature.t(), + Point.t(), + non_neg_integer(), + Point.t(), + boolean + ) :: boolean + def verify_encrypted_signature( + %Signature{r: tweaked_r, s: s}, + pk = %Point{}, + z, + tweak_point = %Point{}, + was_negated + ) do + z_bytes = Utils.int_to_big(z, 32) + + {:ok, tweaked_r_point} = Point.lift_x(tweaked_r) + # This is subtracting the tweak_point (T) from the tweaked_point (R + T) to get the original R + tweak_point = conditional_negate_point(tweak_point, !was_negated) + r_point = Math.add(tweaked_r_point, tweak_point) + + e = calculate_e(Point.x_bytes(tweaked_r_point), Point.x_bytes(pk), z_bytes) + r_point2 = calculate_r(pk, s, e) + partial_validate_r(r_point, r_point2.x) + end + + defp make_point_even(point) do + if Point.has_even_y(point) do + {point, false} + else + {Point.negate(point), true} + end + end + + @doc """ + decrypt_signature uses the tweak/decryption key to transform an + adaptor/encrypted signature into a final, valid Schnorr signature. + """ + @spec decrypt_signature(Signature.t(), PrivateKey.t(), boolean) :: Signature.t() + def decrypt_signature(%Signature{r: r, s: s}, tweak, was_negated) do + # force even on tweak is a backup. the passed tweak should already be properly negated + tweak = conditional_negate(tweak.d, was_negated) + final_s = Math.modulo(tweak.d + s, @n) + %Signature{r: r, s: final_s} + end + + @doc """ + recover_decryption_key recovers the tweak or decryption key by + subtracting final_sig.s - encrypted_sig.s (mod n). The tweak is + negated if the original R+T point was negated during signing. + """ + @spec recover_decryption_key(Signature.t(), Signature.t(), boolean) :: + PrivateKey.t() | {:error, String.t()} + def recover_decryption_key(%Signature{r: enc_r}, %Signature{r: r}, _) when enc_r != r, + do: {:error, "invalid signature pair"} + + def recover_decryption_key( + _encrypted_sig = %Signature{s: enc_s}, + _sig = %Signature{s: s}, + was_negated + ) do + t = Math.modulo(s - enc_s, @n) + conditional_negate(t, was_negated) + end + + # @spec calculate_signature_point(Point.t(), Point.(), <<_::256>>) :: Point.t() | {:error, String.t()} + def calculate_signature_point(r_point, pk, z_bytes) do e = - tagged_hash_challenge(r_bytes <> Point.x_bytes(pubkey) <> z_bytes) - |> :binary.decode_unsigned() - |> Math.modulo(@n) + calculate_e(Point.x_bytes(r_point), Point.x_bytes(pk), z_bytes) + |> PrivateKey.new() - r_point = - @generator_point - |> Math.multiply(s) - |> Math.add(Math.multiply(pubkey, @n - e)) + case e do + {:error, msg} -> + {:error, msg} - !Point.is_inf(r_point) && Point.has_even_y(r_point) && r_point.x == r + {:ok, e} -> + Math.multiply(pk, e.d) + |> Math.add(r_point) + end end end diff --git a/lib/secp256k1/secp256k1.ex b/lib/secp256k1/secp256k1.ex index 2f5efa2..59fe059 100644 --- a/lib/secp256k1/secp256k1.ex +++ b/lib/secp256k1/secp256k1.ex @@ -113,7 +113,14 @@ defmodule Bitcoinex.Secp256k1 do @spec serialize_signature(t()) :: binary def serialize_signature(%__MODULE__{r: r, s: s}) do - :binary.encode_unsigned(r) <> :binary.encode_unsigned(s) + Utils.int_to_big(r, 32) <> Utils.int_to_big(s, 32) + end + + @spec to_hex(t()) :: binary + def to_hex(sig) do + sig + |> serialize_signature() + |> Base.encode16(case: :lower) end @doc """ diff --git a/lib/segwit.ex b/lib/segwit.ex index 1ebeb33..d9c28f0 100644 --- a/lib/segwit.ex +++ b/lib/segwit.ex @@ -85,8 +85,8 @@ defmodule Bitcoinex.Segwit do @doc """ Simpler Interface to check if address is valid """ - @spec is_valid_segswit_address?(String.t()) :: boolean - def is_valid_segswit_address?(address) when is_binary(address) do + @spec is_valid_segwit_address?(String.t()) :: boolean + def is_valid_segwit_address?(address) when is_binary(address) do case decode_address(address) do {:ok, _} -> true diff --git a/lib/taproot.ex b/lib/taproot.ex new file mode 100644 index 0000000..5cb6f8e --- /dev/null +++ b/lib/taproot.ex @@ -0,0 +1,254 @@ +defmodule Bitcoinex.Taproot do + alias Bitcoinex.Utils + + alias Bitcoinex.Taproot + alias Bitcoinex.{Secp256k1, Script} + alias Bitcoinex.Secp256k1.{Math, Params, Point, PrivateKey} + + @n Params.curve().n + + @bip342_leaf_version 0xC0 + + @spec bip342_leaf_version :: 192 + def bip342_leaf_version(), do: @bip342_leaf_version + + @spec tweak_privkey(PrivateKey.t(), binary) :: PrivateKey.t() | {:error, String.t()} + def tweak_privkey(sk0 = %PrivateKey{}, h) do + sk = Secp256k1.force_even_y(sk0) + + case PrivateKey.to_point(sk) do + {:error, msg} -> + {:error, msg} + + pk -> + t = calculate_taptweak(pk, h) + + if t > @n do + {:error, "invalid tweaked key"} + else + %PrivateKey{d: Math.modulo(sk.d + t, @n)} + end + end + end + + @spec tweak_pubkey(Point.t(), binary) :: Point.t() | {:error, String.t()} + def tweak_pubkey(pk = %Point{}, h) do + t = calculate_taptweak(pk, h) + + if t > @n do + {:error, "invalid tweaked key"} + else + t_point = PrivateKey.to_point(t) + Math.add(pk, t_point) + end + end + + @spec calculate_taptweak(Point.t(), binary) :: non_neg_integer + def calculate_taptweak(pk = %Point{}, h) do + pk + |> Point.x_bytes() + |> Kernel.<>(h) + |> tagged_hash_taptweak() + |> :binary.decode_unsigned() + end + + @spec tagged_hash_tapbranch(binary) :: <<_::256>> + def tagged_hash_tapbranch(br), do: Utils.tagged_hash("TapBranch", br) + + @spec tagged_hash_taptweak(binary) :: <<_::256>> + def tagged_hash_taptweak(root), do: Utils.tagged_hash("TapTweak", root) + + @spec tagged_hash_tapleaf(binary) :: <<_::256>> + def tagged_hash_tapleaf(leaf), do: Utils.tagged_hash("TapLeaf", leaf) + + @spec tagged_hash_tapsighash(binary) :: <<_::256>> + def tagged_hash_tapsighash(sigmsg), do: Utils.tagged_hash("TapSighash", sigmsg) + + defmodule TapLeaf do + @moduledoc """ + TapLeaf represents a leaf of a Taproot Merkle tree. A leaf + contains a version and a Script. + """ + alias Bitcoinex.Script + alias Bitcoinex.Taproot + + @type t :: %__MODULE__{ + version: non_neg_integer(), + script: Script.t() + } + @enforce_keys [ + :version, + :script + ] + defstruct [ + :version, + :script + ] + + @doc """ + new constructs a TapLeaf from a leaf_version and Script. + The script is stored as binary with the compact size prepended to it. + """ + @spec new(non_neg_integer(), Script.t()) :: t() + def new(leaf_version, script = %Script{}) do + %__MODULE__{version: leaf_version, script: script} + end + + @spec from_string(non_neg_integer(), String.t()) :: t() + def from_string(leaf_version, script_hex) do + {:ok, script} = Script.parse_script(script_hex) + new(leaf_version, script) + end + + @doc """ + serialize returns a binary concatenation of the leaf_version and Script. TapLeaf structs + store the Script in binary and alredy prepended with the compact size, so that is not added here. + """ + @spec serialize(t()) :: binary + def serialize(%__MODULE__{version: v, script: s}), + do: :binary.encode_unsigned(v) <> Script.serialize_with_compact_size(s) + + @doc """ + hash returns the Hash_TapLeaf of the serialized TapLeaf + """ + @spec hash(t()) :: <<_::256>> + def hash(tapleaf = %__MODULE__{}), do: serialize(tapleaf) |> Taproot.tagged_hash_tapleaf() + end + + @typedoc """ + script_tree represents a Taproot Script Merkle Tree. Leaves are represented by TapLeaf structs + while branches are {script_tree, script_tree}. Since we sort based on hash at each level, + left vs right branches are irrelevant. An empty tree is represented by nil. + """ + @type script_tree :: Taproot.TapLeaf.t() | {script_tree(), script_tree()} | nil + + @doc """ + merkelize_script_tree takes a script_tree (either nil, a TapLeaf, or a tuple of two script_trees) + and constructs the root node. It returns {root_node, hash}. The hash is nil if the script_tree is empty. + defined in BIP341 https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs + """ + @spec merkelize_script_tree(script_tree()) :: {list({TapLeaf.t(), binary}), binary} + def merkelize_script_tree(nil), do: {nil, <<>>} + + def merkelize_script_tree(leaf = %TapLeaf{}) do + hash = TapLeaf.hash(leaf) + + {[{leaf, <<>>}], hash} + end + + def merkelize_script_tree({left, right}) do + {{l_branches, l_hash}, {r_branches, r_hash}} = + {merkelize_script_tree(left), merkelize_script_tree(right)} + + # cross-mix the right hash with left branch and left hash with right branch + new_left = merkelize_branches(l_branches, r_hash) + new_right = merkelize_branches(r_branches, l_hash) + + node = new_left ++ new_right + + # combine the branches to form root node. + {l_hash, r_hash} = Utils.lexicographical_sort(l_hash, r_hash) + hash = tagged_hash_tapbranch(l_hash <> r_hash) + {node, hash} + end + + defp merkelize_branches([], _), do: [] + + defp merkelize_branches([{leaf, c} | tail], hash) do + [{leaf, c <> hash} | merkelize_branches(tail, hash)] + end + + @spec build_control_block(Point.t(), script_tree(), non_neg_integer()) :: binary + def build_control_block(p = %Point{}, script_tree, script_index) do + {tree, hash} = merkelize_script_tree(script_tree) + {tapleaf, merkle_path} = Enum.at(tree, script_index) + q = tweak_pubkey(p, hash) + q_parity = if Point.has_even_y(q), do: 0, else: 1 + + <> <> Point.x_bytes(p) <> merkle_path + end + + # Should this take a Script or binary script + @spec merkelize_control_block(<<_::256>>, binary) :: any + def merkelize_control_block(<>, path) do + # Consume each 32-byte chunk of the rest of the control path, which are hashes of the merkle tree + path + |> :binary.bin_to_list() + |> Enum.chunk_every(32) + |> Enum.reduce(k0, fn e, k -> merkelize_path(e, k) end) + end + + defp merkelize_path(<>, <>) do + {l, r} = Utils.lexicographical_sort(e, k) + {:cont, tagged_hash_tapbranch(l <> r)} + end + + @doc """ + validate_taproot_scriptpath_spend DOES NOT validate the actual script according to BIP342. + It only validates the BIP341 rules around how a scriptPath spend works. + See: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#script-validation-rules + """ + @spec validate_taproot_scriptpath_spend(Point.t(), binary, binary) :: + bool | {:error, String.t()} + def validate_taproot_scriptpath_spend( + q_point = %Point{}, + script, + <> <> <> <> path + ) do + leaf_version = extract_leaf_version(c) + + k0 = + tagged_hash_tapleaf( + leaf_version <> Utils.serialize_compact_size_unsigned_int(byte_size(script)) <> script + ) + + k = merkelize_control_block(k0, path) + # t is tweak + t = tagged_hash_taptweak(p <> k) |> :binary.decode_unsigned() + + case {PrivateKey.to_point(t), Point.lift_x(p)} do + {{:error, _}, _} -> + {:error, "control block yielded invalid tweak"} + + {_, {:error, _}} -> + {:error, "failed to parse point Q"} + + {tk, {:ok, pk}} -> + validate_q(q_point, Math.add(pk, tk), c) + # TODO evaluate actual script? + end + end + + defp validate_q(given_q = %Point{}, calculated_q = %Point{}, <>) do + q_parity = extract_q_parity(c) + + cond do + q_parity != Point.has_even_y(given_q) -> + {:error, "incorrect Q parity"} + + given_q.x != calculated_q.x -> + {:error, "Q points do not match"} + + true -> + true + end + end + + @spec extract_leaf_version(<<_::8>>) :: binary + defp extract_leaf_version(<>) do + c + |> :binary.decode_unsigned() + |> Bitwise.band(0xFE) + |> :binary.encode_unsigned() + end + + @spec extract_q_parity(<<_::8>>) :: bool + defp extract_q_parity(<>) do + q_mod2 = + c + |> :binary.decode_unsigned() + |> Bitwise.band(1) + + q_mod2 == 0 + end +end diff --git a/lib/transaction.ex b/lib/transaction.ex index 1956d03..3ce9e5b 100644 --- a/lib/transaction.ex +++ b/lib/transaction.ex @@ -9,6 +9,7 @@ defmodule Bitcoinex.Transaction do alias Bitcoinex.Transaction.Witness alias Bitcoinex.Utils alias Bitcoinex.Transaction.Utils, as: TxUtils + alias Bitcoinex.Taproot @type t() :: %__MODULE__{ version: non_neg_integer(), @@ -18,6 +19,7 @@ defmodule Bitcoinex.Transaction do lock_time: non_neg_integer() } + # TODO refactor witnesses into input fields defstruct [ :version, :inputs, @@ -26,6 +28,31 @@ defmodule Bitcoinex.Transaction do :lock_time ] + @minimum_time_locktime 500_000_000 + + def minimum_time_locktime(), do: @minimum_time_locktime + + @sighash_default 0x00 + @sighash_all 0x01 + @sighash_none 0x02 + @sighash_single 0x03 + @sighash_anyonecanpay 0x80 + @sighash_anyonecanpay_all 0x81 + @sighash_anyonecanpay_none 0x82 + @sighash_anyonecanpay_single 0x83 + + @valid_sighash_flags [ + @sighash_default, + @sighash_all, + @sighash_none, + @sighash_single, + @sighash_anyonecanpay_all, + @sighash_anyonecanpay_none, + @sighash_anyonecanpay_single + ] + + def valid_sighash_flags(), do: @valid_sighash_flags + @doc """ Returns the TxID of the given tranasction. @@ -43,22 +70,300 @@ defmodule Bitcoinex.Transaction do ) end + @spec bip341_sighash( + t(), + non_neg_integer(), + non_neg_integer(), + non_neg_integer(), + list(non_neg_integer()), + list(<<_::280>>), + list({:tapleaf, Taproot.TapLeaf.t()}) + ) :: <<_::256>> + def bip341_sighash( + tx = %__MODULE__{}, + hash_type, + ext_flag, + input_idx, + prev_amounts, + prev_scriptpubkeys, + opts \\ [] + ) do + sigmsg = + bip341_sigmsg(tx, hash_type, ext_flag, input_idx, prev_amounts, prev_scriptpubkeys, opts) + + Taproot.tagged_hash_tapsighash(sigmsg) + end + + @spec bip341_sigmsg( + t(), + non_neg_integer(), + non_neg_integer(), + non_neg_integer(), + list(non_neg_integer()), + list(<<_::280>>), + # TODO do good caching + list({:tapleaf, Taproot.TapLeaf.t()}) + ) :: binary + def bip341_sigmsg( + tx, + hash_type, + ext_flag, + input_idx, + prev_amounts, + prev_scriptpubkeys, + opts \\ [] + ) + + def bip341_sigmsg(_, _, ext_flag, _, _, _, _) when ext_flag < 0 or ext_flag > 127, + do: {:error, "ext_flag out of range 0-127"} + + def bip341_sigmsg(_, hash_type, _, _, _, _, _) when hash_type not in @valid_sighash_flags, + do: {:error, "invalid sighash flag"} + + def bip341_sigmsg( + tx = %__MODULE__{}, + hash_type, + ext_flag, + input_idx, + prev_amounts, + prev_scriptpubkeys, + opts + ) do + tx_data = bip341_tx_data(tx, hash_type, prev_amounts, prev_scriptpubkeys) + + bip341_sigmsg_with_cache( + tx, + hash_type, + ext_flag, + input_idx, + prev_amounts, + prev_scriptpubkeys, + tx_data, + opts + ) + end + + @spec bip341_sigmsg_with_cache( + t(), + non_neg_integer(), + non_neg_integer(), + non_neg_integer(), + list(non_neg_integer()), + list(binary), + binary, + list({:tapleaf, Taproot.TapLeaf.t()}) + ) :: binary + def bip341_sigmsg_with_cache( + tx = %__MODULE__{}, + hash_type, + ext_flag, + input_idx, + prev_amounts, + prev_scriptpubkeys, + cached_tx_data, + opts \\ [] + ) do + hash_byte = :binary.encode_unsigned(hash_type) + + input_data = + bip341_input_data( + tx, + hash_type, + ext_flag, + input_idx, + Enum.at(prev_amounts, input_idx), + Enum.at(prev_scriptpubkeys, input_idx) + ) + + output_data = bip341_output_data(tx, input_idx, hash_type) + + tapleaf = Keyword.get(opts, :tapleaf, nil) + + ext = + case tapleaf do + tl = %Taproot.TapLeaf{} -> + # TODO last_executed_codesep_pos not implemented + sigmsg_extension(ext_flag, tl) + + nil -> + sigmsg_extension(ext_flag) + end + + <<0>> <> + hash_byte <> + cached_tx_data <> input_data <> output_data <> ext + end + + # TODO good caching + # The results of this function can be reused across input signings. + @spec bip341_tx_data(t(), non_neg_integer(), list(non_neg_integer()), list(<<_::280>>)) :: + binary + def bip341_tx_data(tx, hash_type, prev_amounts, prev_scriptpubkeys) do + version = <> + lock_time = <> + acc = version <> lock_time + + acc = + if !hash_type_is_anyonecanpay(hash_type) do + sha_prevouts = bip341_sha_prevouts(tx.inputs) + sha_amounts = bip341_sha_amounts(prev_amounts) + sha_scriptpubkeys = bip341_sha_scriptpubkeys(prev_scriptpubkeys) + sha_sequences = bip341_sha_sequences(tx.inputs) + acc <> sha_prevouts <> sha_amounts <> sha_scriptpubkeys <> sha_sequences + else + acc + end + + if !hash_type_is_none_or_single(hash_type) do + sha_outputs = bip341_sha_outputs(tx.outputs) + acc <> sha_outputs + else + acc + end + end + + defp bip341_input_data( + tx, + hash_type, + ext_flag, + input_idx, + prev_amount, + <> + ) do + annex = get_annex(tx, input_idx) + spend_type = ext_flag * 2 + if annex == nil, do: 0, else: 1 + + input_commit = + if hash_type_is_anyonecanpay(hash_type) do + input = Enum.at(tx.inputs, input_idx) + prev_outpoint = Transaction.In.serialize_prevout(input) + + prev_outpoint <> + <> <> + prev_scriptpubkey <> <> + else + <> + end + + <> <> input_commit <> bip341_sha_annex(annex) + end + + defp bip341_output_data(tx, input_idx, hash_type) do + if hash_type_is_single(hash_type) do + tx.outputs + |> Enum.at(input_idx) + |> Out.serialize_output() + |> :erlang.list_to_binary() + |> Utils.sha256() + else + <<>> + end + end + + @spec hash_type_is_anyonecanpay(non_neg_integer()) :: boolean + def hash_type_is_anyonecanpay(hash_type), + do: Bitwise.band(hash_type, @sighash_anyonecanpay) == @sighash_anyonecanpay + + defp hash_type_is_none_or_single(hash_type) do + b = Bitwise.band(hash_type, 3) + b == @sighash_none || b == @sighash_single + end + + defp hash_type_is_single(hash_type) do + Bitwise.band(hash_type, 3) == @sighash_single + end + + @spec get_annex(t(), non_neg_integer()) :: nil | binary | {:error} + def get_annex(%__MODULE__{witnesses: nil}, _), do: nil + def get_annex(%__MODULE__{witnesses: []}, _), do: nil + + def get_annex(%__MODULE__{witnesses: witnesses, inputs: inputs}, input_idx) + when input_idx >= 0 and input_idx < length(inputs) do + witnesses + |> Enum.at(input_idx) + |> Witness.get_annex() + end + + def get_annex(_, _), do: {:error, "input index is out of range"} + + @spec bip341_sha_prevouts(list(In.t())) :: <<_::256>> + def bip341_sha_prevouts(inputs) do + inputs + |> Transaction.In.serialize_prevouts() + |> Utils.sha256() + end + + @spec bip341_sha_amounts(list(non_neg_integer())) :: <<_::256>> + def bip341_sha_amounts(prev_amounts) do + prev_amounts + |> Enum.reduce(<<>>, fn amount, acc -> acc <> <> end) + |> Utils.sha256() + end + + @spec bip341_sha_scriptpubkeys(list(<<_::280>>)) :: <<_::256>> + def bip341_sha_scriptpubkeys(prev_scriptpubkeys) do + prev_scriptpubkeys + |> Enum.reduce(<<>>, fn script, acc -> acc <> script end) + |> Utils.sha256() + end + + @spec bip341_sha_sequences(list(Transaction.In.t())) :: <<_::256>> + def bip341_sha_sequences(inputs) do + inputs + |> Transaction.In.serialize_sequences() + |> Utils.sha256() + end + + @spec bip341_sha_outputs(list(Transaction.Out.t())) :: <<_::256>> + def bip341_sha_outputs(outputs) do + outputs + |> Transaction.Out.serialize_outputs() + |> Utils.sha256() + end + + @spec bip341_sha_annex(nil | binary) :: <<_::256>> + def bip341_sha_annex(nil), do: <<>> + + def bip341_sha_annex(annex) do + annex + |> byte_size() + |> Utils.serialize_compact_size_unsigned_int() + |> Kernel.<>(annex) + |> Utils.sha256() + end + + def sigmsg_extension(0), do: <<>> + + def sigmsg_extension(1, tapleaf, last_executed_codesep_pos \\ 0xFFFFFFFF), + do: bip342_sigmsg_ext(tapleaf, last_executed_codesep_pos) + + def bip342_sigmsg_ext(tapleaf = %Taproot.TapLeaf{}, last_executed_codesep_pos \\ 0xFFFFFFFF) do + key_version = 0x00 + + Taproot.TapLeaf.hash(tapleaf) <> + <> <> <> + end + @doc """ Decodes a transaction in a hex encoded string into binary. """ - def decode(tx_hex) when is_binary(tx_hex) do - case Base.decode16(tx_hex, case: :lower) do - {:ok, tx_bytes} -> - case parse(tx_bytes) do - {:ok, txn} -> - {:ok, txn} + def decode(serialized_tx) when is_binary(serialized_tx) do + tx_bytes = + case Base.decode16(serialized_tx, case: :lower) do + {:ok, tx_bytes} -> + tx_bytes + + # if decoding fails, attempt to parse as if serialized_tx is already binary. + :error -> + serialized_tx + end - :error -> - {:error, :parse_error} - end + case parse(tx_bytes) do + {:ok, txn} -> + {:ok, txn} :error -> - {:error, :decode_error} + {:error, :parse_error} end end @@ -114,6 +419,7 @@ defmodule Bitcoinex.Transaction.Utils do alias Bitcoinex.Transaction.In alias Bitcoinex.Transaction.Out alias Bitcoinex.Transaction.Witness + alias Bitcoinex.Utils @doc """ Returns the Variable Length Integer used in serialization. @@ -149,9 +455,9 @@ defmodule Bitcoinex.Transaction.Utils do version = <> marker = <<0x00::big-size(8)>> flag = <<0x01::big-size(8)>> - tx_in_count = serialize_compact_size_unsigned_int(length(txn.inputs)) + tx_in_count = Utils.serialize_compact_size_unsigned_int(length(txn.inputs)) inputs = In.serialize_inputs(txn.inputs) |> :erlang.list_to_binary() - tx_out_count = serialize_compact_size_unsigned_int(length(txn.outputs)) + tx_out_count = Utils.serialize_compact_size_unsigned_int(length(txn.outputs)) outputs = Out.serialize_outputs(txn.outputs) |> :erlang.list_to_binary() witness = Witness.serialize_witness(txn.witnesses) lock_time = <> @@ -162,33 +468,14 @@ defmodule Bitcoinex.Transaction.Utils do def serialize(txn) do version = <> - tx_in_count = serialize_compact_size_unsigned_int(length(txn.inputs)) + tx_in_count = Utils.serialize_compact_size_unsigned_int(length(txn.inputs)) inputs = In.serialize_inputs(txn.inputs) |> :erlang.list_to_binary() - tx_out_count = serialize_compact_size_unsigned_int(length(txn.outputs)) + tx_out_count = Utils.serialize_compact_size_unsigned_int(length(txn.outputs)) outputs = Out.serialize_outputs(txn.outputs) |> :erlang.list_to_binary() lock_time = <> version <> tx_in_count <> inputs <> tx_out_count <> outputs <> lock_time end - - @doc """ - Returns the serialized variable length integer. - """ - def serialize_compact_size_unsigned_int(compact_size) do - cond do - compact_size >= 0 and compact_size <= 0xFC -> - <> - - compact_size <= 0xFFFF -> - <<0xFD>> <> <> - - compact_size <= 0xFFFFFFFF -> - <<0xFE>> <> <> - - compact_size <= 0xFF -> - <<0xFF>> <> <> - end - end end defmodule Bitcoinex.Transaction.Witness do @@ -197,6 +484,7 @@ defmodule Bitcoinex.Transaction.Witness do """ alias Bitcoinex.Transaction.Witness alias Bitcoinex.Transaction.Utils, as: TxUtils + alias Bitcoinex.Utils @type t :: %__MODULE__{ txinwitness: list(binary()) @@ -206,7 +494,7 @@ defmodule Bitcoinex.Transaction.Witness do ] @doc """ - Wtiness accepts a binary and deserializes it. + Witness accepts a binary and deserializes it. """ @spec witness(binary) :: t() def witness(witness_bytes) do @@ -234,15 +522,15 @@ defmodule Bitcoinex.Transaction.Witness do [witness | witnesses] = witnesses serialized_witness = - if Enum.empty?(witness.txinwitness) do + if witness == nil || Enum.empty?(witness.txinwitness) do <<0x0::big-size(8)>> else - stack_len = TxUtils.serialize_compact_size_unsigned_int(length(witness.txinwitness)) + stack_len = Utils.serialize_compact_size_unsigned_int(length(witness.txinwitness)) field = Enum.reduce(witness.txinwitness, <<>>, fn v, acc -> {:ok, item} = Base.decode16(v, case: :lower) - item_len = TxUtils.serialize_compact_size_unsigned_int(byte_size(item)) + item_len = Utils.serialize_compact_size_unsigned_int(byte_size(item)) acc <> item_len <> item end) @@ -287,6 +575,22 @@ defmodule Bitcoinex.Transaction.Witness do stack_size - 1 ) end + + @spec get_annex(t()) :: nil | binary + def get_annex(%__MODULE__{txinwitness: witnesses}) when length(witnesses) < 2, do: nil + + def get_annex(%__MODULE__{txinwitness: witnesses}) do + last = + witnesses + |> Enum.reverse() + |> Enum.at(0) + + case last do + # TODO switch to binary or int once witnesses are no longer stored as strings + "50" <> _ -> last + _ -> nil + end + end end defmodule Bitcoinex.Transaction.In do @@ -295,6 +599,7 @@ defmodule Bitcoinex.Transaction.In do """ alias Bitcoinex.Transaction.In alias Bitcoinex.Transaction.Utils, as: TxUtils + alias Bitcoinex.Utils @type t :: %__MODULE__{ prev_txid: binary(), @@ -320,17 +625,11 @@ defmodule Bitcoinex.Transaction.In do defp serialize_input(inputs, serialized_inputs) do [input | inputs] = inputs - {:ok, prev_txid} = Base.decode16(input.prev_txid, case: :lower) - - prev_txid = - prev_txid - |> :binary.decode_unsigned(:big) - |> :binary.encode_unsigned(:little) - |> Bitcoinex.Utils.pad(32, :trailing) + prev_txid = prev_txid_little_endian(input.prev_txid) {:ok, script_sig} = Base.decode16(input.script_sig, case: :lower) - script_len = TxUtils.serialize_compact_size_unsigned_int(byte_size(script_sig)) + script_len = Utils.serialize_compact_size_unsigned_int(byte_size(script_sig)) serialized_input = [ prev_txid, @@ -343,6 +642,26 @@ defmodule Bitcoinex.Transaction.In do serialize_input(inputs, [serialized_inputs, serialized_input]) end + def serialize_prevouts(inputs) do + Enum.reduce(inputs, <<>>, fn input, acc -> acc <> serialize_prevout(input) end) + end + + def serialize_prevout(input) do + prev_txid = prev_txid_little_endian(input.prev_txid) + prev_txid <> <> + end + + def serialize_sequences(inputs) do + Enum.reduce(inputs, <<>>, fn input, acc -> acc <> <> end) + end + + def prev_txid_little_endian(prev_txid_hex) do + prev_txid_hex + |> Base.decode16!(case: :lower) + |> Utils.flip_endianness() + |> Utils.pad(32, :trailing) + end + def parse_inputs(counter, inputs) do parse(inputs, [], counter) end @@ -360,6 +679,7 @@ defmodule Bitcoinex.Transaction.In do remaining input = %In{ + # TODO fix this prev_txid: Base.encode16(<<:binary.decode_unsigned(prev_txid, :big)::little-size(256)>>, case: :lower), prev_vout: prev_vout, @@ -369,6 +689,27 @@ defmodule Bitcoinex.Transaction.In do parse(remaining, [input | inputs], count - 1) end + + def lexicographical_sort_inputs(inputs) do + Enum.sort(inputs, &lexicographical_cmp_inputs/2) + end + + def lexicographical_cmp_inputs(input1, input2) do + # compare txids then vouts + input1little_txid = Base.decode16!(input1.prev_txid, case: :lower) + + input1bin = + (input1little_txid <> <>) + |> :erlang.binary_to_list() + + input2little_txid = Base.decode16!(input2.prev_txid, case: :lower) + + input2bin = + (input2little_txid <> <>) + |> :erlang.binary_to_list() + + Utils.lexicographical_cmp(input1bin, input2bin) + end end defmodule Bitcoinex.Transaction.Out do @@ -377,6 +718,7 @@ defmodule Bitcoinex.Transaction.Out do """ alias Bitcoinex.Transaction.Out alias Bitcoinex.Transaction.Utils, as: TxUtils + alias Bitcoinex.Utils @type t :: %__MODULE__{ value: non_neg_integer(), @@ -390,20 +732,22 @@ defmodule Bitcoinex.Transaction.Out do @spec serialize_outputs(list(Out.t())) :: iolist() def serialize_outputs(outputs) do - serialize_output(outputs, []) + serialize_outputs(outputs, []) end - defp serialize_output([], serialized_outputs), do: serialized_outputs + def serialize_outputs([], serialized_outputs), do: serialized_outputs - defp serialize_output(outputs, serialized_outputs) do - [output | outputs] = outputs + def serialize_outputs([output | outputs], serialized_outputs) do + serialized_output = serialize_output(output) + serialize_outputs(outputs, [serialized_outputs, serialized_output]) + end + def serialize_output(output) do {:ok, script_pub_key} = Base.decode16(output.script_pub_key, case: :lower) - script_len = TxUtils.serialize_compact_size_unsigned_int(byte_size(script_pub_key)) + script_len = Utils.serialize_compact_size_unsigned_int(byte_size(script_pub_key)) - serialized_output = [<>, script_len, script_pub_key] - serialize_output(outputs, [serialized_outputs, serialized_output]) + [<>, script_len, script_pub_key] end def output(out_bytes) do @@ -431,4 +775,24 @@ defmodule Bitcoinex.Transaction.Out do parse(remaining, [output | outputs], count - 1) end + + def lexicographical_sort_outputs(outputs) do + Enum.sort(outputs, &lexicographical_cmp_output/2) + end + + def lexicographical_cmp_output(o1, o2) do + # first compare amounts, then scriptpubkeys + cond do + o1.value < o2.value -> + true + + o1.value > o2.value -> + false + + o1.value == o2.value -> + o1spk = Base.decode16!(o1.script_pub_key, case: :lower) |> :erlang.binary_to_list() + o2spk = Base.decode16!(o2.script_pub_key, case: :lower) |> :erlang.binary_to_list() + Utils.lexicographical_cmp(o1spk, o2spk) + end + end end diff --git a/lib/utils.ex b/lib/utils.ex index 54d5dbf..e5bf9c4 100644 --- a/lib/utils.ex +++ b/lib/utils.ex @@ -3,11 +3,12 @@ defmodule Bitcoinex.Utils do Contains useful utility functions used in Bitcoinex. """ - @spec sha256(iodata()) :: binary + @spec sha256(iodata()) :: <<_::256>> def sha256(str) do :crypto.hash(:sha256, str) end + @spec tagged_hash(binary, iodata()) :: <<_::256>> def tagged_hash(tag, str) do tag_hash = sha256(tag) sha256(tag_hash <> tag_hash <> str) @@ -22,7 +23,7 @@ defmodule Bitcoinex.Utils do for _ <- 1..num, do: x end - @spec double_sha256(iodata()) :: binary + @spec double_sha256(iodata()) :: <<_::256>> def double_sha256(preimage) do :crypto.hash( :sha256, @@ -30,7 +31,7 @@ defmodule Bitcoinex.Utils do ) end - @spec hash160(iodata()) :: binary + @spec hash160(iodata()) :: <<_::160>> def hash160(preimage) do :crypto.hash( :ripemd160, @@ -61,6 +62,13 @@ defmodule Bitcoinex.Utils do bin <> <<0::size(pad_len)>> end + @spec flip_endianness(binary) :: binary + def flip_endianness(bin) do + bin + |> :binary.decode_unsigned(:big) + |> :binary.encode_unsigned(:little) + end + @spec int_to_big(non_neg_integer(), non_neg_integer()) :: binary def int_to_big(i, p) do i @@ -68,14 +76,17 @@ defmodule Bitcoinex.Utils do |> pad(p, :leading) end + @spec int_to_little(non_neg_integer(), integer) :: binary def int_to_little(i, p) do i |> :binary.encode_unsigned(:little) |> pad(p, :trailing) end + @spec little_to_int(binary) :: non_neg_integer def little_to_int(i), do: :binary.decode_unsigned(i, :little) + @spec encode_int(non_neg_integer()) :: binary | {:error, <<_::160>>} def encode_int(i) when i > 0 do cond do i < 0xFD -> :binary.encode_unsigned(i) @@ -86,6 +97,7 @@ defmodule Bitcoinex.Utils do end end + @spec hex_to_bin(String.t()) :: binary | {:error, String.t()} def hex_to_bin(str) do str |> String.downcase() @@ -107,4 +119,53 @@ defmodule Bitcoinex.Utils do |> Enum.map(fn {b0, b1} -> Bitwise.bxor(b0, b1) end) |> :binary.list_to_bin() end + + # ascending order + @spec lexicographical_sort(binary, binary) :: {binary, binary} + def lexicographical_sort(bin0, bin1) when is_binary(bin0) and is_binary(bin1) do + if lexicographical_cmp(:binary.bin_to_list(bin0), :binary.bin_to_list(bin1)) do + {bin0, bin1} + else + {bin1, bin0} + end + end + + # equality case + @spec lexicographical_cmp(list(byte), list(byte)) :: boolean + def lexicographical_cmp([], []), do: true + + def lexicographical_cmp([b0 | r0], [b1 | r1]) do + cond do + b0 == b1 -> + lexicographical_cmp(r0, r1) + + b1 < b0 -> + # initial order was incorrect, must be swapped + false + + true -> + # bin0, bin1 was the correct order + true + end + end + + @doc """ + Returns the serialized variable length integer. + """ + @spec serialize_compact_size_unsigned_int(non_neg_integer()) :: binary + def serialize_compact_size_unsigned_int(compact_size) do + cond do + compact_size >= 0 and compact_size <= 0xFC -> + <> + + compact_size <= 0xFFFF -> + <<0xFD>> <> <> + + compact_size <= 0xFFFFFFFF -> + <<0xFE>> <> <> + + compact_size <= 0xFF -> + <<0xFF>> <> <> + end + end end diff --git a/scripts/dlc.exs b/scripts/dlc.exs new file mode 100644 index 0000000..697aab0 --- /dev/null +++ b/scripts/dlc.exs @@ -0,0 +1,429 @@ +alias Bitcoinex.{Secp256k1,Transaction, Script, Utils, Taproot} +alias Bitcoinex.Secp256k1.{Point, PrivateKey, Signature, Schnorr} + + +new_rand_int = fn -> + 32 + |> :crypto.strong_rand_bytes() + |> :binary.decode_unsigned() +end + +new_privkey = fn -> + {:ok, sk} = + new_rand_int.() + |> PrivateKey.new() + Secp256k1.force_even_y(sk) +end + +multisig_2_of_2_script = fn a, b -> + # Script will be pseudo-multisig: + # OP_CHECKSIGVERIFY OP_CHECKSIG + # Scripts are stacks, so must be inserted in reverse order. + # This also means Alices Signature must come first in the witness_script + s = Script.new() + {:ok, s} = Script.push_op(s, :op_checksig) + {:ok, s} = Script.push_data(s, Point.x_bytes(a)) + {:ok, s} = Script.push_op(s, :op_checksigverify) + {:ok, s} = Script.push_data(s, Point.x_bytes(b)) + s +end + +# Initial setup for this example: give Alice and bob one coin worth 100,010,000 sats each, in order to fund the DLC. +# these ouotputs will be simple keyspend-only P2TRs +alice_init_sk = new_privkey.() +alice_init_pk = PrivateKey.to_point(alice_init_sk) +alice_init_script_tree = nil +{:ok, alice_init_script} = Script.create_p2tr(alice_init_pk, alice_init_script_tree) +{:ok, alice_init_addr} = Script.to_address(alice_init_script, :regtest) + +bob_init_sk = new_privkey.() +bob_init_pk = PrivateKey.to_point(bob_init_sk) +bob_init_script_tree = nil +{:ok, bob_init_script} = Script.create_p2tr(bob_init_pk, bob_init_script_tree) +{:ok, bob_init_addr} = Script.to_address(bob_init_script, :regtest) + +# In your regtest node, send bitcoin to each of these 2 addresses in the amount 100_010_000. +# if you use a different amount, edit the *_init_amount variables below +# note the outpoints for both sends. +alice_init_txid = "57495e49895e87ac3ba2f2467abc6124df166a251a0e304eb770ccc040063af4" +alice_init_vout = 1 +alice_init_amount = 100_010_000 + +bob_init_txid = "783bf305752377c006310d66756d680fa9e33c0e870d024b1c2269aa88f4654c" +bob_init_vout = 1 +bob_init_amount = 100_010_000 + +### BEGIN DLC EXAMPLE ### + +# First, Alice and Bob will create a 2-of-2 funding address + +alice_fund_sk = new_privkey.() +alice_fund_pk = PrivateKey.to_point(alice_fund_sk) + +bob_fund_sk = new_privkey.() +bob_fund_pk = PrivateKey.to_point(bob_fund_sk) + +fund_script = multisig_2_of_2_script.(alice_fund_pk, bob_fund_pk) +# If you want to examine the script, uncomment next line: +# Script.display_script(fund_script) + +fund_leaf = Taproot.TapLeaf.new(Taproot.bip342_leaf_version(), fund_script) + +# WARNING: this address only has 1 spend option: a 2-of-2 multisig. Without additional timeout +# spendpaths (allowing both parties to recover funds if the other disappears), this is a trusted +# and unsafe contract to enter. TODO: add timeout allowing both parties to reclaim funds. + +# P2TR for funding addr will have no internal key. Only way to spend is to satisfy the 2-of-2 +# TODO: when MuSig is implemented, the KeySpend route can act as 2-of-2 instead, and is cheaper. +{:ok, fund_scriptpubkey, r} = Script.create_p2tr_script_only(fund_leaf, new_rand_int.()) + +# in the above, r is a random number used to create a unique but provably unspendable internal key +# in the case that we don't want a keypath spend to be possible. If either alice or bob generated the funding +# scriptpubkey, the other should verify that the internal key is unspendable and reconstruct the same script +# to ensure they know what contract they are sending their BTC to. +fund_p = Script.calculate_unsolvable_internal_key(r) +is_correct_p2tr = Script.validate_unsolvable_internal_key(fund_scriptpubkey, fund_leaf, r) + +# ALICE NOR BOB should fund this address until both CETs are signed with adaptor sigs +{:ok, fund_addr} = Script.to_address(fund_scriptpubkey, :regtest) + +# We create the funding transaction. +init_amounts = [alice_init_amount, bob_init_amount] +init_scriptpubkeys = [ + Script.serialize_with_compact_size(alice_init_script), + Script.serialize_with_compact_size(bob_init_script) +] + +fund_amount = 200_010_000 +fund_tx = %Transaction{ + version: 1, + inputs: [ + %Transaction.In{ + prev_txid: alice_init_txid, + prev_vout: alice_init_vout, + script_sig: "", + sequence_no: 2147483648 + }, + %Transaction.In{ + prev_txid: bob_init_txid, + prev_vout: bob_init_vout, + script_sig: "", + sequence_no: 2147483648 + } + ], + outputs: [ + %Transaction.Out{ + # fee of 10_000 sats paid for this tx. This means you'll probably have to disable max fee constraints in bitcoind to broadcast txs + # the next 10_000 will go towards spending this output to settle the contract + value: fund_amount, + script_pub_key: Script.to_hex(fund_scriptpubkey) + } + # A more realistic example would have change outputs to each party here. Omitted for simplicity + ], + lock_time: 0 +} + +# even though the fund tx is not signed, we can calculate the txid and use that to create contract execution txs (CETs) +# which will spend the output created in this tx +fund_txid = Transaction.transaction_id(fund_tx) +# d4be73514a8535c6e21a6007b0de049ba3d4d7d93ee953852c4b7ce97369ff49 +fund_vout = 0 +fund_amounts = [fund_amount] +fund_scriptpubkeys = [Script.serialize_with_compact_size(fund_scriptpubkey)] + +# Oracle Identity & Signing Key +oracle_sk = new_privkey.() +oracle_pk = PrivateKey.to_point(oracle_sk) + +# The bet will be a simple EAGLES or CHIEFS bet. + +# the same nonce must be used for both outcomes in order to guarantee that the Oracle +# cannot sign both events without leaking their own private key. In a more trust-minimized +# example, the Oracle should prove ownership of a UTXO with the public key they use for +# the signing, in order to prove that they have something at stake if they should sign both events. + +# Oracle does not use the standard BIP340 method for generating a nonce. +# This is because the nonce must not commit to the message, so that it can +# be reused for either outcome. +oracle_event_nonce = new_privkey.() +event_nonce_point = PrivateKey.to_point(oracle_event_nonce) + +moon_msg = "MOON! the price rose" +crash_msg = "CRASH! the price fell" + +# Oracle Broadcasts its intention to sign one of the 2 events +public = %{ + oracle_pk: oracle_pk, + event_nonce_point: event_nonce_point, + case1: moon_msg, + case2: crash_msg + # note, arbitrary number of outcomes are workable here. +} + +# Alice And Bob can now compute the Signature Point, which they will use as the tweak point for their adaptor signatures +moon_sighash = Utils.double_sha256(public.case1) +crash_sighash = Utils.double_sha256(public.case2) + +moon_sig_point = Schnorr.calculate_signature_point(public.event_nonce_point, oracle_pk, moon_sighash) +crash_sig_point = Schnorr.calculate_signature_point(public.event_nonce_point, oracle_pk, crash_sighash) + +# Alice and Bob create CETs (Settlement Transactions, which will spend the funding tx (not yet signed/broadcasted)) + +# Alice and Bob each need Addresses to settle to +# For simplicity, these dest addresses will be internal key only Taproot addresses +alice_dest_sk = new_privkey.() +alice_dest_pk = PrivateKey.to_point(alice_dest_sk) +{:ok, alice_dest_script} = Script.create_p2tr(alice_dest_pk, nil) +{:ok, alice_dest_addr} = Script.to_address(alice_dest_script, :regtest) + +bob_dest_sk = new_privkey.() +bob_dest_pk = PrivateKey.to_point(bob_dest_sk) +{:ok, bob_dest_script} = Script.create_p2tr(bob_dest_pk, nil) +{:ok, bob_dest_addr} = Script.to_address(bob_dest_script, :regtest) + +# CET hash type will be sighash default for both, for simplicity +cet_hash_type = 0x00 + +# First CET: MOON, alice wins, and gets 75% of the funding tx (excluding fees) +moon_cet = %Transaction{ + version: 1, + inputs: [ + %Transaction.In{ + prev_txid: fund_txid, + prev_vout: fund_vout, + script_sig: "", + sequence_no: 2147483648, + } + ], + outputs: [ + # ALICE WINS! gets 150M sats from the 1M she put in + %Transaction.Out{ + value: 150_000_000, + script_pub_key: Script.to_hex(alice_dest_script) + }, + # BOB LOSES :( gets 50M sats from the 1M he put in + %Transaction.Out{ + value: 50_000_000, + script_pub_key: Script.to_hex(bob_dest_script) + } + ], + lock_time: 0 +} +# calculate the sighash for the MOON CET +moon_cet_sighash = Transaction.bip341_sighash( + moon_cet, + cet_hash_type, # sighash_default (all) + 0x01, # we are using taproot scriptpath spend, so ext_flag must be 1 + 0, # index we're going to sign + fund_amounts, # list of amounts for each input being spent + fund_scriptpubkeys, # list of prev scriptpubkeys for each input being spent + tapleaf: fund_leaf +) |> :binary.decode_unsigned() + +# Second CET: CRASH, bob wins, and gets 75% of the funding tx (excluding fees) +crash_cet = %Transaction{ + version: 1, + inputs: [ + # Notice this input is the same coin spent in the Moon CET tx. So they can't both be valid. + %Transaction.In{ + prev_txid: fund_txid, + prev_vout: fund_vout, + script_sig: "", + sequence_no: 2147483648, + } + ], + outputs: [ + # ALICE LOSES :( gets 50M sats from the 1M she put in + %Transaction.Out{ + value: 50_000_000, + script_pub_key: Script.to_hex(alice_dest_script) + }, + # BOB WINS! gets 150M sats from the 1M he put in + %Transaction.Out{ + value: 150_000_000, + script_pub_key: Script.to_hex(bob_dest_script) + } + ], + lock_time: 0 +} +# calculate the Sighash for the CRASH CET +crash_cet_sighash = Transaction.bip341_sighash( + crash_cet, + cet_hash_type, # sighash_default (all) + 0x01, # we are using taproot scriptpath spend, so ext_flag = 1 + 0, # only one input in this tx + fund_amounts, # list of amounts for each input being spent + fund_scriptpubkeys, # list of prev scriptpubkeys for each input being spent + tapleaf: fund_leaf +) |> :binary.decode_unsigned() + +# Alice and Bob now create adaptor signatures for each of these CETs. They must share both of their +# Adaptor Signatures with one another. These next 4 steps are in no particular order. + +# Alice creates adaptor sig for Crash Case using crash_sig_point (tweak point/encryption key). +aux_rand = new_rand_int.() # generate some entropy for this signature +{:ok, alice_crash_adaptor_sig, alice_crash_was_negated} = Schnorr.encrypted_sign(alice_fund_sk, crash_cet_sighash, aux_rand, crash_sig_point) + +# Bob creates adaptor sig for Moon Case using moon_sig_point (tweak point/encryption key). +aux_rand = new_rand_int.() # generate some entropy for this signature +{:ok, bob_moon_adaptor_sig, bob_moon_was_negated} = Schnorr.encrypted_sign(bob_fund_sk, moon_cet_sighash, aux_rand, moon_sig_point) + +# Alice creates adaptor sig for Moon Case using moon_sig_point. +aux_rand = new_rand_int.() # generate some entropy for this signature +{:ok, alice_moon_adaptor_sig, alice_moon_was_negated} = Schnorr.encrypted_sign(alice_fund_sk, moon_cet_sighash, aux_rand, moon_sig_point) + +# Bob creates adaptor sig for Crash Case using crash_sig_point. +aux_rand = new_rand_int.() # generate some entropy for this signature +{:ok, bob_crash_adaptor_sig, bob_crash_was_negated} = Schnorr.encrypted_sign(bob_fund_sk, crash_cet_sighash, aux_rand, crash_sig_point) + +# Verification Time! Alice and Bob must each verify one another's adaptor signatures to ensure they will be +# valid once the Oracle publishes the resolution signature + +# Bob verifies Alice's Crash signature +is_valid = Schnorr.verify_encrypted_signature(alice_crash_adaptor_sig, alice_fund_pk, crash_cet_sighash, crash_sig_point, alice_crash_was_negated) + +# Alice verifies Bob's Moon signature +is_valid = Schnorr.verify_encrypted_signature(bob_moon_adaptor_sig, bob_fund_pk, moon_cet_sighash, moon_sig_point, bob_moon_was_negated) + +# Bob verifies Alice's Moon signature +is_valid = Schnorr.verify_encrypted_signature(alice_moon_adaptor_sig, alice_fund_pk, moon_cet_sighash, moon_sig_point, alice_moon_was_negated) + +# Alice verifies Bob's Crash signature +is_valid = Schnorr.verify_encrypted_signature(bob_crash_adaptor_sig, bob_fund_pk, crash_cet_sighash, crash_sig_point, bob_crash_was_negated) + +# IFF all four adaptor signatures are valid, we're good to go! + +# Now that each party has the other's adaptor signatures, they can sign and broadcast the +# Funding transaction (that was created way above). They could not previously do so safely, +# because if they had locked BTC in the 2-of-2 multisig before having their counterparty's +# Adaptor signatures, they could have lost those funds if the counterparty disappeared. +# NOTE: Again, the funding tx has no timeouts in place, so if the Oracle AND a counterparty disappear, +# those funds are locked irretreivably. + +fund_hash_type = 0x00 # sighash_default (all) +fund_ext_flag = 0 # both parties are using keyspend +# This would be done independently by both parties, or collaboratively using PSBT, +alice_fund_sighash = Transaction.bip341_sighash( + fund_tx, + fund_hash_type, + fund_ext_flag, + 0, # alice's input comes first + init_amounts, + init_scriptpubkeys +) |> :binary.decode_unsigned() + +bob_fund_sighash = Transaction.bip341_sighash( + fund_tx, + fund_hash_type, + fund_ext_flag, + 1, # bob's input comes second + init_amounts, + init_scriptpubkeys +) |> :binary.decode_unsigned() + +# each party signs the funding transaction, moving their BTC into the 2-of-2 multisig. + +# In order to sign the tx with the internal private key, they must first tweak it +# so that it can sign for the external taproot key +{_, alice_init_merkle_root_hash} = Taproot.merkelize_script_tree(alice_init_script_tree) +alice_q = Taproot.tweak_privkey(alice_init_sk, alice_init_merkle_root_hash) +aux_rand = new_rand_int.() # more entropy +{:ok, alice_fund_sig} = Schnorr.sign(alice_q, alice_fund_sighash, aux_rand) + +{_, bob_init_merkle_root_hash} = Taproot.merkelize_script_tree(bob_init_script_tree) +aux_rand = new_rand_int.() # even more entropy +bob_q = Taproot.tweak_privkey(bob_init_sk, bob_init_merkle_root_hash) +{:ok, bob_fund_sig} = Schnorr.sign(bob_q, bob_fund_sighash, aux_rand) + +fund_hash_byte = + if fund_hash_type == 0x00 do + <<>> + else + <> + end + +alice_sig_hex = Signature.serialize_signature(alice_fund_sig) <> fund_hash_byte |> Base.encode16(case: :lower) +alice_witness = %Transaction.Witness{ + txinwitness: [ + alice_sig_hex + ] +} + +# NEXT: construct fund witness. +bob_sig_hex = Signature.serialize_signature(bob_fund_sig) <> fund_hash_byte |> Base.encode16(case: :lower) +bob_witness = %Transaction.Witness{ + txinwitness: [ + bob_sig_hex + ] +} + +fund_tx = %Transaction{fund_tx | witnesses: [ + alice_witness, bob_witness +]} + +fund_tx_hex = Transaction.Utils.serialize(fund_tx) |> Base.encode16(case: :lower) +# 5ac6d378bdf1c55b08e92a03de343797b662cb953cd8f3bbfb7e0108bbba7841 + +# FUND TX IS READY TO BROADCAST! +# bitcoin-cli sendrawtransaction 0 +# The 0 at the end disables max_fee constraints. Only do this if you're willing to pay a high fee. + +# ...wait for oracle to announce result... + +# RESOLUTION 1: MOON! Skip to RESOLUTION 2 to execute the CRASH scenario instead + +# Oracle Publishes: +moon_sig = Schnorr.sign_with_nonce(oracle_sk, oracle_event_nonce, :binary.decode_unsigned(moon_sighash)) + +# Alice & Bob should make sure this is a valid Schnorr signature +is_valid = Schnorr.verify_signature(oracle_pk, :binary.decode_unsigned(moon_sighash), moon_sig) + +# alice & bob can both now extract the moon_tweak, which is moon_sig.s and complete their moon_adaptor sigs. +# Since alice won, she has more incentive to do so +%Signature{s: settlement_secret} = moon_sig + +{:ok, settlement_secret} = PrivateKey.new(settlement_secret) + +cet_hash_byte = + if cet_hash_type == 0x00 do + <<>> + else + <> + end + +# Then, Alice can decrypt Bob's signature +bob_moon_sig = Schnorr.decrypt_signature(bob_moon_adaptor_sig, settlement_secret, bob_moon_was_negated) +# why not verify? +is_valid = Schnorr.verify_signature(bob_fund_pk, moon_cet_sighash, bob_moon_sig) + + +# Alice can also decrypt her own adaptor signature +alice_moon_sig = Schnorr.decrypt_signature(alice_moon_adaptor_sig, settlement_secret, alice_moon_was_negated) +# Don't trust, verify +is_valid = Schnorr.verify_signature(alice_fund_pk, moon_cet_sighash, alice_moon_sig) + +# fund_p is the internal taproot key. In this case, it is unsolvable, as verified earlier. +# we take fund_leaf, the script_tree from earlier, and select the index of the script we want to spend. +# Here, there is only 1 script in the tree, so idx must be 0 +control_block = Taproot.build_control_block(fund_p, fund_leaf, 0) + +# serialize everything for insertion into the tx +bob_moon_sig_hex = Signature.serialize_signature(bob_moon_sig) <> cet_hash_byte |> Base.encode16(case: :lower) +alice_moon_sig_hex = Signature.serialize_signature(alice_moon_sig) <> cet_hash_byte |> Base.encode16(case: :lower) +fund_script_hex = Script.to_hex(fund_script) +control_block_hex = control_block |> Base.encode16(case: :lower) + + +# She then adds these to the Moon CET and broadcasts it + +tx = %Transaction{moon_cet | witnesses: [ + %Transaction.Witness{ + txinwitness: [alice_moon_sig_hex, bob_moon_sig_hex, fund_script_hex, control_block_hex] + } +] +} + +Transaction.Utils.serialize(tx) |> Base.encode16(case: :lower) + + +# RESOLUTION 2: CRASH! diff --git a/scripts/gen_test_vectors.exs b/scripts/gen_test_vectors.exs new file mode 100644 index 0000000..43ed9ca --- /dev/null +++ b/scripts/gen_test_vectors.exs @@ -0,0 +1,58 @@ +alias Bitcoinex.Secp256k1 +alias Bitcoinex.Secp256k1.{Math, PrivateKey, Point, Schnorr, Signature} +alias Bitcoinex.Utils + +to_hex = fn i -> "0x" <> Integer.to_string(i, 16) end + +write_row = fn file, sk, pk, tw, t_point, z, aux, ut_sig, tw_sig, err, is_tweaked_s_even, is_tweaked_s_ooo -> IO.binwrite(file, +to_hex.(sk.d) <> "," <> Point.x_hex(pk) <> "," <> to_hex.(tw.d) <> "," +<> Point.x_hex(t_point) <> "," <> to_hex.(z) <> "," <> to_hex.(aux) <> "," +<> Signature.to_hex(ut_sig) <> "," <> Signature.to_hex(tw_sig) <> "," +<> err <> "," <> to_string(is_tweaked_s_even) <> "," <> to_string(is_tweaked_s_ooo) <> "\n") +end + +order_n = 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141 + +{:ok, good_file} = File.open("schnorr_adaptor_test_vectors-good.csv", [:write]) +{:ok, bad_file} = File.open("schnorr_adaptor_test_vectors-bad.csv", [:write]) + +IO.binwrite(good_file, "private_key,public_key,tweak_secret,tweak_point,message_hash,aux_rand,untweaked_adaptor_signature,tweaked_signature,is_tweaked_s_even\n") +IO.binwrite(bad_file, "private_key,public_key,tweak_secret,tweak_point,message_hash,aux_rand,untweaked_adaptor_signature,tweaked_signature,is_tweaked_s_even\n") + +for _ <- 1..50 do + ski = :rand.uniform(order_n-1) + {:ok, sk0} = PrivateKey.new(ski) + sk = Secp256k1.force_even_y(sk0) + pk = PrivateKey.to_point(sk) + + # tweak + ti = :rand.uniform(order_n-1) + {:ok, tw} = PrivateKey.new(ti) + tw = Secp256k1.force_even_y(tw) + tw_point = PrivateKey.to_point(tw) + + msg = + :rand.uniform(order_n-1) + |> :binary.encode_unsigned() + z = Utils.double_sha256(msg) |> :binary.decode_unsigned() + + aux = :rand.uniform(order_n-1) + + # create adaptor sig + {:ok, ut_sig, _tw_point} = Schnorr.sign_for_tweak(sk, z, aux, tw_point) + tw_sig = Schnorr.tweak_signature(ut_sig, tw.d) + + # checks + tweaked_s = tw.d+ut_sig.s + is_tweaked_s_ooo = tweaked_s > order_n + {:ok, tweaked_s} = PrivateKey.new(Math.modulo(tweaked_s, order_n)) + tweaked_forced_s = Secp256k1.force_even_y(tweaked_s) + is_tweaked_s_even = tweaked_forced_s == tweaked_s + + case Schnorr.verify_signature(pk, z, tw_sig) do + true -> + write_row.(good_file, sk, pk, tw, tw_point, z, aux, ut_sig, tw_sig, "", is_tweaked_s_even, is_tweaked_s_ooo) + {:error, err} -> + write_row.(bad_file, sk, pk, tw, tw_point, z, aux, ut_sig, tw_sig, err, is_tweaked_s_even, is_tweaked_s_ooo) + end +end diff --git a/scripts/schnorr_adaptor.exs b/scripts/schnorr_adaptor.exs new file mode 100644 index 0000000..932deb5 --- /dev/null +++ b/scripts/schnorr_adaptor.exs @@ -0,0 +1,40 @@ +alias Bitcoinex.Secp256k1 +alias Bitcoinex.Secp256k1.{PrivateKey, Point, Schnorr, Signature} +alias Bitcoinex.Utils +# private key +{:ok, sk} = PrivateKey.new(1234123412341234123412341234) +pk = PrivateKey.to_point(sk) + +# tweak +{:ok, t} = PrivateKey.new(658393766392737484910002828395) +t = Secp256k1.force_even_y(t) +t_point = PrivateKey.to_point(t) + +msg = "tweakin" +z = Utils.double_sha256(msg) |> :binary.decode_unsigned() + +aux = 1203948712823749283 + +# create adaptor sig +{:ok, ut_sig, t_point_} = Schnorr.sign_for_tweak(sk, z, aux, t_point) +t_point_ == t_point + +# adaptor sig is not a valid schnorr sig +!Schnorr.verify_signature(pk, z, ut_sig) + +# verify adaptor signature +Schnorr.verify_untweaked_signature(pk, z, ut_sig, t_point) + +# complete adaptor sig +tw_sig = Schnorr.tweak_signature(ut_sig, t.d) + +# complete sig must be valid schnorr sig +Schnorr.verify_signature(pk, z, tw_sig) + +# extract tweak +{:ok, tweak} = Schnorr.extract_tweak(pk, z, ut_sig, tw_sig) +tweak == t.d + +# extract signature given tweak +{:ok, sig} = Schnorr.extract_tweaked_signature(pk, z, ut_sig, t.d) +sig == tw_sig diff --git a/scripts/taproot_keyspend.exs b/scripts/taproot_keyspend.exs new file mode 100644 index 0000000..e6a99c9 --- /dev/null +++ b/scripts/taproot_keyspend.exs @@ -0,0 +1,100 @@ +alias Bitcoinex.Secp256k1 +alias Bitcoinex.Transaction +alias Bitcoinex.Script +alias Bitcoinex.Secp256k1 +alias Bitcoinex.Secp256k1.{Point, PrivateKey, Signature, Schnorr} +alias Bitcoinex.Taproot + +new_privkey = fn -> + {:ok, sk} = + 32 + |> :crypto.strong_rand_bytes() + |> :binary.decode_unsigned() + |> PrivateKey.new() + Secp256k1.force_even_y(sk) +end + +sk = new_privkey.() +pk = PrivateKey.to_point(sk) + +# EDIT if you want to include a script tree (optional, but spending is not yet enabled) +script_tree = nil + +{:ok, scriptpubkey} = Script.create_p2tr(pk, script_tree) + +{:ok, addr} = Script.to_address(scriptpubkey, :regtest) +# EDIT to the unique addr +# addr = "bcrt1pfh4qvlzrgmf6f8e6urjkf3ax83kz02xqc8zujnpeycxgc3wrqmxs8py692" +# EDIT to the txid of your send to the addr above +txid = "9d539c9fc4a17ddd920e0bd8d5d43f92382a36765f48a7725e92e184a21a50d7" +# EDIT to the vout of the output you created for the addr +vout = 0 +# EDIT to the amount you sent in the UTXO above +amount = 100_000_000 +# EDIT where you would like to spend to +dest_addr = "bcrt1pzeg29d38m506gtnunlg2tjh4hpvv4mtkjg5tku34ad24830pta5qg0kyn6" + +{:ok, dest_script, _network} = Script.from_address(dest_addr) + +tx = %Transaction{ + version: 1, + inputs: [ + %Transaction.In{ + prev_txid: txid, + prev_vout: vout, + script_sig: "", + sequence_no: 2147483648, + } + ], + outputs: [ + %Transaction.Out{ + value: 50_000_000, + script_pub_key: Script.to_hex(dest_script) + } + ], + lock_time: 0 +} + +#sighash_default +hash_type = 0x00 +ext_flag = 0 +input_idx = 0 + +sighash = Transaction.bip341_sighash( + tx, + hash_type, + ext_flag, + input_idx, + [amount], + [Script.serialize_with_compact_size(scriptpubkey)] +) + +{_, merkle_root_hash} = Taproot.merkelize_script_tree(script_tree) + +q_sk = Taproot.tweak_privkey(sk, merkle_root_hash) + +{:ok, sig} = Schnorr.sign(q_sk, :binary.decode_unsigned(sighash), 0) + +hash_byte = + if hash_type == 0x00 do + <<>> + else + <> + end + +witness_script = Signature.serialize_signature(sig) <> hash_byte |> Base.encode16(case: :lower) + +tx = %Transaction{tx | witnesses: [ + %Transaction.Witness{ + txinwitness: [ + witness_script + ] + } +] +} + +Transaction.Utils.serialize(tx) + +# the last zero arg is to disable max_fee_rate +# use bitcoin-cli sendrawtransaction 0 +# txid: 86dcdf6a88480a16524aa353b47d11228d67f96f59c4a645d65d4aac09330065 diff --git a/scripts/taproot_multi_scriptspend.exs b/scripts/taproot_multi_scriptspend.exs new file mode 100644 index 0000000..ed3e0e5 --- /dev/null +++ b/scripts/taproot_multi_scriptspend.exs @@ -0,0 +1,169 @@ +alias Bitcoinex.Secp256k1 +alias Bitcoinex.Transaction +alias Bitcoinex.Script +alias Bitcoinex.Secp256k1 +alias Bitcoinex.Secp256k1.{Point, PrivateKey, Signature, Schnorr} +alias Bitcoinex.Taproot + +new_privkey = fn -> + {:ok, sk} = + 32 + |> :crypto.strong_rand_bytes() + |> :binary.decode_unsigned() + |> PrivateKey.new() + Secp256k1.force_even_y(sk) +end + +# internal_key +internal_sk = new_privkey.() +internal_pk = PrivateKey.to_point(internal_sk) + +# p2pk script key 1 +p2pk0_sk = new_privkey.() +p2pk1_pk = PrivateKey.to_point(p2pk0_sk) + +{:ok, p2pk0_script} = Script.create_p2pk(Point.x_bytes(p2pk1_pk)) + +# p2pk script key 2 +p2pk1_sk = new_privkey.() +p2pk2_pk = PrivateKey.to_point(p2pk1_sk) + +{:ok, p2pk1_script} = Script.create_p2pk(Point.x_bytes(p2pk2_pk)) + + +leaf0 = Taproot.TapLeaf.new(Taproot.bip342_leaf_version(), p2pk0_script) + +leaf1 = Taproot.TapLeaf.new(Taproot.bip342_leaf_version(), p2pk1_script) + +# single leaf +script_tree = {leaf0, leaf1} + +{:ok, scriptpubkey} = Script.create_p2tr(internal_pk, script_tree) + +{:ok, addr} = Script.to_address(scriptpubkey, :regtest) +# addr = bcrt1pwfprzzadfx3meyn5glm0rfmyl6lgmelqgwfqvxwx3hr756tcr77spqf2aq +# broadcast and mine funding tx +# EDIT note txid of funding tx +txid = "f11bfaa48121c582f652f0a1643c7b5a1692fe3582dff7819d2787486e595f68" +# EDIT to the vout of the output you created for the addr +vout = 0 +# EDIT to the amount you sent in the UTXO above +amount = 100_000_000 +# EDIT where you would like to spend to +dest_addr = "bcrt1pzeg29d38m506gtnunlg2tjh4hpvv4mtkjg5tku34ad24830pta5qg0kyn6" + +{:ok, dest_script, _network} = Script.from_address(dest_addr) + +tx = %Transaction{ + version: 1, + inputs: [ + %Transaction.In{ + prev_txid: txid, + prev_vout: vout, + script_sig: "", + sequence_no: 2147483648, + } + ], + outputs: [ + %Transaction.Out{ + value: 50_000_000, + script_pub_key: Script.to_hex(dest_script) + } + ], + lock_time: 0 +} + +#sighash_default +hash_type = 0x00 +ext_flag = 1 +input_idx = 0 + +# here is where you choose which leaf to use. script_idx must match leaf # + +# SCRIPT 0 +script_idx = 0 +leaf = leaf0 +sk = p2pk0_sk +script = p2pk0_script + +sighash = Transaction.bip341_sighash( + tx, + hash_type, + ext_flag, + input_idx, + [amount], + [Script.serialize_with_compact_size(scriptpubkey)], + tapleaf: leaf +) + +aux_rand = 0 + +{:ok, sig} = Schnorr.sign(sk, :binary.decode_unsigned(sighash), aux_rand) + +control_block = Taproot.build_control_block(internal_pk, script_tree, script_idx) + +hash_byte = + if hash_type == 0x00 do + <<>> + else + <> + end + +sig_hex = Signature.serialize_signature(sig) <> hash_byte |> Base.encode16(case: :lower) +script_hex = Script.to_hex(script) +control_block_hex = control_block |> Base.encode16(case: :lower) + +tx = %Transaction{tx | witnesses: [ + %Transaction.Witness{ + txinwitness: [sig_hex, script_hex, control_block_hex] + } +] +} + +Transaction.Utils.serialize(tx) |> Base.encode16(case: :lower) + +# 01000000000101685f596e4887279d81f7df8235fe92165a7b3c64a1f052f682c52181a4fa1bf10000000000000000800180f0fa02000000002251201650a2b627dd1fa42e7c9fd0a5caf5b858caed769228bb7235eb5553c5e15f680340f53ea81564380d49cd2bdf4bee89b5784a846048818cda51c284c6fd8eaf542bd3d25df4d696c5063d016b8fcab85e2eb78503bca277783a721a2c053c7266972220f6c4e1e4276a75cfcbba374608887f64e09a55a9e6166fe4856324d084c4a784ac41c1a280169a8ed09e3c4b34f832c0ae44d78bb081631c00084f12a602218734a27d9d06d23f4e9ac0a6d9eb2923d59c40b113cb3a150062410da36bc7408584faee00000000 + +# ALTERNATE script +script_idx = 1 +leaf = leaf1 +sk = p2pk1_sk +script = p2pk1_script + +sighash = Transaction.bip341_sighash( + tx, + hash_type, + ext_flag, + input_idx, + [amount], + [Script.serialize_with_compact_size(scriptpubkey)], + tapleaf: leaf +) + +aux_rand = 0 + +{:ok, sig} = Schnorr.sign(sk, :binary.decode_unsigned(sighash), aux_rand) + +control_block = Taproot.build_control_block(internal_pk, script_tree, script_idx) + +hash_byte = + if hash_type == 0x00 do + <<>> + else + <> + end + +sig_hex = Signature.serialize_signature(sig) <> hash_byte |> Base.encode16(case: :lower) +script_hex = Script.to_hex(script) +control_block_hex = control_block |> Base.encode16(case: :lower) + +tx = %Transaction{tx | witnesses: [ + %Transaction.Witness{ + txinwitness: [sig_hex, script_hex, control_block_hex] + } +] +} + +Transaction.Utils.serialize(tx) |> Base.encode16(case: :lower) + +# 01000000000101685f596e4887279d81f7df8235fe92165a7b3c64a1f052f682c52181a4fa1bf10000000000000000800180f0fa02000000002251201650a2b627dd1fa42e7c9fd0a5caf5b858caed769228bb7235eb5553c5e15f68034077228590ea058878df865afdfbb950be53d55da15ee1a4a7f03ee5cc0c01003fb3d290c799b428a400d0743ebb3c9caa2b1c27f99d851cee6464ea7642f939582220e2aa46c4e1f43cb01c89195b39ea80dbf91d7054052fa0cb5805828b786cd734ac41c1a280169a8ed09e3c4b34f832c0ae44d78bb081631c00084f12a602218734a27d509753bb6e46ebbdcfb16bbd3c7c07b59b7aede009760ba50a5ae8ae1d0ee49200000000 diff --git a/scripts/taproot_scriptspend.exs b/scripts/taproot_scriptspend.exs new file mode 100644 index 0000000..6c2ab8d --- /dev/null +++ b/scripts/taproot_scriptspend.exs @@ -0,0 +1,109 @@ +alias Bitcoinex.Secp256k1 +alias Bitcoinex.Transaction +alias Bitcoinex.Script +alias Bitcoinex.Secp256k1 +alias Bitcoinex.Secp256k1.{Point, PrivateKey, Signature, Schnorr} +alias Bitcoinex.Taproot + +new_privkey = fn -> + {:ok, sk} = + 32 + |> :crypto.strong_rand_bytes() + |> :binary.decode_unsigned() + |> PrivateKey.new() + Secp256k1.force_even_y(sk) +end + +# internal_key +internal_sk = new_privkey.() +internal_pk = PrivateKey.to_point(internal_sk) + +# p2pk script key +p2pk_sk = new_privkey.() +p2pk_pk = PrivateKey.to_point(p2pk_sk) + +{:ok, p2pk_script} = Script.create_p2pk(Point.x_bytes(p2pk_pk)) + +# single leaf +script_tree = Taproot.TapLeaf.new(Taproot.bip342_leaf_version(), p2pk_script) + +{:ok, scriptpubkey} = Script.create_p2tr(internal_pk, script_tree) + +{:ok, addr} = Script.to_address(scriptpubkey, :regtest) +# addr = bcrt1ptkdqw3d39fuzg9qtw6r6e2rj098tp5vffhhnyu8r7m62j00q3atq8hpx0m +# broadcast and mine funding tx +# EDIT note txid of funding tx +txid = "0492bc1dce2ee85c92533942219e1ca72069a395d2dc77d595bd2f171da4e6ce" +# EDIT to the vout of the output you created for the addr +vout = 0 +# EDIT to the amount you sent in the UTXO above +amount = 100_000_000 +# EDIT where you would like to spend to +dest_addr = "bcrt1pzeg29d38m506gtnunlg2tjh4hpvv4mtkjg5tku34ad24830pta5qg0kyn6" + +{:ok, dest_script, _network} = Script.from_address(dest_addr) + +tx = %Transaction{ + version: 1, + inputs: [ + %Transaction.In{ + prev_txid: txid, + prev_vout: vout, + script_sig: "", + sequence_no: 2147483648, + } + ], + outputs: [ + %Transaction.Out{ + value: 50_000_000, + script_pub_key: Script.to_hex(dest_script) + } + ], + lock_time: 0 +} + +#sighash_default +hash_type = 0x00 +ext_flag = 1 +input_idx = 0 + +sighash = Transaction.bip341_sighash( + tx, + hash_type, + ext_flag, + input_idx, + [amount], + [Script.serialize_with_compact_size(scriptpubkey)], + tapleaf: script_tree +) + + + +aux_rand = 0 +script_idx = 0 + +{:ok, sig} = Schnorr.sign(p2pk_sk, :binary.decode_unsigned(sighash), aux_rand) + +control_block = Taproot.build_control_block(internal_pk, script_tree, script_idx) + +hash_byte = + if hash_type == 0x00 do + <<>> + else + <> + end + +sig_hex = Signature.serialize_signature(sig) <> hash_byte |> Base.encode16(case: :lower) +script_hex = Script.to_hex(p2pk_script) +control_block_hex = control_block |> Base.encode16(case: :lower) + +tx = %Transaction{tx | witnesses: [ + %Transaction.Witness{ + txinwitness: [sig_hex, script_hex, control_block_hex] + } +] +} + +Transaction.Utils.serialize(tx) |> Base.encode16(case: :lower) + +# a97fb556cff86dda196cb2c9fa4892de259dc8b910ec72b60975822b88b05130 diff --git a/scripts/test_broken_adaptor.exs b/scripts/test_broken_adaptor.exs new file mode 100644 index 0000000..c9919b3 --- /dev/null +++ b/scripts/test_broken_adaptor.exs @@ -0,0 +1,30 @@ + +alias Bitcoinex.Secp256k1 +alias Bitcoinex.Secp256k1.{PrivateKey, Schnorr, Signature} + +test = %{ + privkey: 0x279D71D68D3EE997019D005BDF703C271001631A7EE12E4C9DAD10C0754912DC, + pubkey: 0x22c63594ea2c2199e0500cdf6dffecdf878441720789c8dfcfb9af06a96fd1e4, + tweak_secret: 0xF8EBFDF85A3AF0C337ECB165EF47D565DE15CBCEEB597A243C3D54DF49B703D5, + tweak_point: 0x6545e169e4d2e940e63207110a9d44dd5d4ca65aeb58e3e566658f62d41bd23f, + message_hash: 0x5736367EBB12EDC15B0FA75319B46D016F86A0E057B9237240D6185C93596367, + aux_rand: 0x7E4E37835DDFC6A82A011073DCB779D02F1F5B52A2937B6ADD5B9DA2528FC5C6, + untweaked_sig: "e2125e2f6d791ce59b604dfc0578a823008a5c86f2f2efbd0de68a4cb19688d817ebac918f08e0078c66a26c664d9f169d66dc54fdd95972e68a69b79797274a", + tweaked_sig: "320ab814c2e7e2567af8e738ce83e9fdc55ef57933dd52b169bba46fd3516e4c10d7aa89e943d0cac45353d25595747dc0cdcb3d39ea335b62f5600a1117e9de" +} + +z = test.message_hash +aux = test.aux_rand + +{:ok, t} = PrivateKey.new(test.tweak_secret) +t2 = Secp256k1.force_even_y(t) +t_point = PrivateKey.to_point(t) + +{:ok, sk} = PrivateKey.new(test.privkey) +sk2 = Secp256k1.force_even_y(sk) + +# use sk2 +pk = PrivateKey.to_point(sk2) +{:ok, ut_sig, t_point_} = Schnorr.sign_for_tweak(sk2, z, aux, t_point) +tw_sig = Schnorr.tweak_signature(ut_sig, t.d) +Schnorr.verify_signature(pk, z, tw_sig) diff --git a/test/extendedkey_test.exs b/test/extendedkey_test.exs index 269320f..0ca4958 100644 --- a/test/extendedkey_test.exs +++ b/test/extendedkey_test.exs @@ -122,22 +122,25 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L" } - @derivation_paths_to_strings [ + @derivation_paths_to_serialize [ %{ str: "84/0/0/2/1/", - deriv: %ExtendedKey.DerivationPath{child_nums: [84, 0, 0, 2, 1]} + deriv: %ExtendedKey.DerivationPath{child_nums: [84, 0, 0, 2, 1]}, + bin: <<84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0>> }, %{ str: "84'/0'/", deriv: %ExtendedKey.DerivationPath{ child_nums: [84 + @min_hardened_child_num, 0 + @min_hardened_child_num] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128>> }, %{ str: "84'/0'/", deriv: %ExtendedKey.DerivationPath{ child_nums: [84 + @min_hardened_child_num, 0 + @min_hardened_child_num] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128>> }, %{ str: "84'/0'/1/2/2147483647/", @@ -149,14 +152,16 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do 2, 2_147_483_647 ] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128, 1, 0, 0, 0, 2, 0, 0, 0, 255, 255, 255, 127>> } ] - @strings_to_derivation_paths [ + @derivation_paths_to_parse [ %{ str: "84/0/0/2/1/", - deriv: %ExtendedKey.DerivationPath{child_nums: [84, 0, 0, 2, 1]} + deriv: %ExtendedKey.DerivationPath{child_nums: [84, 0, 0, 2, 1]}, + bin: <<84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0>> }, %{ str: "m/84'/0'/0'/2/1", @@ -168,31 +173,36 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do 2, 1 ] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 2, 0, 0, 0, 1, 0, 0, 0>> }, %{ str: "m/84'/0'/", deriv: %ExtendedKey.DerivationPath{ child_nums: [84 + @min_hardened_child_num, 0 + @min_hardened_child_num] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128>> }, %{ str: "m/84'/0'", deriv: %ExtendedKey.DerivationPath{ child_nums: [84 + @min_hardened_child_num, 0 + @min_hardened_child_num] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128>> }, %{ str: "84'/0'", deriv: %ExtendedKey.DerivationPath{ child_nums: [84 + @min_hardened_child_num, 0 + @min_hardened_child_num] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128>> }, %{ str: "84'/0'/", deriv: %ExtendedKey.DerivationPath{ child_nums: [84 + @min_hardened_child_num, 0 + @min_hardened_child_num] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128>> }, %{ str: "84'/0'/1/2/2147483647", @@ -204,7 +214,8 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do 2, 2_147_483_647 ] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128, 1, 0, 0, 0, 2, 0, 0, 0, 255, 255, 255, 127>> }, %{ str: "m/84h/0h/0h/2/1", @@ -216,25 +227,29 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do 2, 1 ] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 2, 0, 0, 0, 1, 0, 0, 0>> }, %{ str: "m/84h/0h/", deriv: %ExtendedKey.DerivationPath{ child_nums: [84 + @min_hardened_child_num, 0 + @min_hardened_child_num] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128>> }, %{ str: "84h/0h", deriv: %ExtendedKey.DerivationPath{ child_nums: [84 + @min_hardened_child_num, 0 + @min_hardened_child_num] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128>> }, %{ str: "84h/0h/", deriv: %ExtendedKey.DerivationPath{ child_nums: [84 + @min_hardened_child_num, 0 + @min_hardened_child_num] - } + }, + bin: <<84, 0, 0, 128, 0, 0, 0, 128>> } ] @@ -252,15 +267,15 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do # Extended Key Testing - describe "parse_extended_key/1" do + describe "parse/1" do test "successfully parse extended xprv" 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.parse(t.xprv_m) == {:ok, t.xprv_m_obj} + assert ExtendedKey.display(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.parse(t.xpub_m) == {:ok, t.xpub_m_obj} + assert ExtendedKey.display(t.xpub_m_obj) == t.xpub_m end end @@ -276,100 +291,100 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do test "BIP32 tests 1: successfully convert xprv to xpub." do t = @bip32_test_case_1 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m_0h) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0h) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m_0h) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0h) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m_0h_1) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0h_1) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m_0h_1) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0h_1) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m_0h_1_2h) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0h_1_2h) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m_0h_1_2h) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0h_1_2h) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m_0h_1_2h_2) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0h_1_2h_2) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m_0h_1_2h_2) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0h_1_2h_2) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m_0h_1_2h_2_1000000000) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0h_1_2h_2_1000000000) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m_0h_1_2h_2_1000000000) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0h_1_2h_2_1000000000) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} end test "BIP32 tests 1: derive prv keys in sequence" do t = @bip32_test_case_1 # derive prv child from prv parent_fingerprint - {:ok, m_xprv} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, m_xprv} = ExtendedKey.parse(t.xprv_m) {:ok, m_0h_xprv} = ExtendedKey.derive_private_child(m_xprv, @min_hardened_child_num) - assert ExtendedKey.parse_extended_key(t.xprv_m_0h) == {:ok, m_0h_xprv} + assert ExtendedKey.parse(t.xprv_m_0h) == {:ok, m_0h_xprv} # derive child m/0'/1 {:ok, m_0h_1_xprv} = ExtendedKey.derive_private_child(m_0h_xprv, 1) - assert ExtendedKey.parse_extended_key(t.xprv_m_0h_1) == {:ok, m_0h_1_xprv} + assert ExtendedKey.parse(t.xprv_m_0h_1) == {:ok, m_0h_1_xprv} end test "BIP32 tests 1: derive pub keys from master prv key" do t = @bip32_test_case_1 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) {:ok, m_0h_xpub} = ExtendedKey.derive_public_child(xprv, @min_hardened_child_num) - assert ExtendedKey.parse_extended_key(t.xpub_m_0h) == {:ok, m_0h_xpub} + assert ExtendedKey.parse(t.xpub_m_0h) == {:ok, m_0h_xpub} - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) {:ok, xprv} = ExtendedKey.derive_private_child(xprv, @min_hardened_child_num) {:ok, xprv} = ExtendedKey.derive_private_child(xprv, 1) {:ok, m_0h_1_2h_xpub} = ExtendedKey.derive_public_child(xprv, @min_hardened_child_num + 2) - assert ExtendedKey.parse_extended_key(t.xpub_m_0h_1_2h) == {:ok, m_0h_1_2h_xpub} + assert ExtendedKey.parse(t.xpub_m_0h_1_2h) == {:ok, m_0h_1_2h_xpub} end test "BIP32 tests 1: derive m/0'/1/2'/2/1000000000 from master key" do t = @bip32_test_case_1 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) {:ok, xprv} = ExtendedKey.derive_private_child(xprv, @min_hardened_child_num) {:ok, xprv} = ExtendedKey.derive_private_child(xprv, 1) {:ok, xprv} = ExtendedKey.derive_private_child(xprv, @min_hardened_child_num + 2) {:ok, xprv} = ExtendedKey.derive_private_child(xprv, 2) {:ok, m_0h_1_2h_2_1000000000_xprv} = ExtendedKey.derive_private_child(xprv, 1_000_000_000) - assert ExtendedKey.parse_extended_key(t.xprv_m_0h_1_2h_2_1000000000) == + assert ExtendedKey.parse(t.xprv_m_0h_1_2h_2_1000000000) == {:ok, m_0h_1_2h_2_1000000000_xprv} end test "BIP32 tests 1: derive pub child from pub parent_fingerprint" do t = @bip32_test_case_1 - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0h) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0h) {:ok, m_0h_1_xpub} = ExtendedKey.derive_public_child(xpub, 1) - assert ExtendedKey.parse_extended_key(t.xpub_m_0h_1) == {:ok, m_0h_1_xpub} + assert ExtendedKey.parse(t.xpub_m_0h_1) == {:ok, m_0h_1_xpub} - {:ok, xpub_m_0h_1_2h} = ExtendedKey.parse_extended_key(t.xpub_m_0h_1_2h) + {:ok, xpub_m_0h_1_2h} = ExtendedKey.parse(t.xpub_m_0h_1_2h) {:ok, m_0h_1_2h_2_xpub} = ExtendedKey.derive_public_child(xpub_m_0h_1_2h, 2) - assert ExtendedKey.parse_extended_key(t.xpub_m_0h_1_2h_2) == {:ok, m_0h_1_2h_2_xpub} + assert ExtendedKey.parse(t.xpub_m_0h_1_2h_2) == {:ok, m_0h_1_2h_2_xpub} {:ok, m_0h_1_2h_2_1000000000_xpub} = m_0h_1_2h_2_xpub |> ExtendedKey.derive_public_child(1_000_000_000) - assert ExtendedKey.parse_extended_key(t.xpub_m_0h_1_2h_2_1000000000) == + assert ExtendedKey.parse(t.xpub_m_0h_1_2h_2_1000000000) == {:ok, m_0h_1_2h_2_1000000000_xpub} end test "BIP32 tests 1: to_public_key works for both xprv and xpubs" do t = @bip32_test_case_1 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m) pub = ExtendedKey.to_public_key(xpub) # test that to_public_key works for xprv and xpub keys @@ -380,7 +395,7 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do t = @bip32_test_case_1 seed = t.seed |> Base.decode16!(case: :lower) - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) assert ExtendedKey.seed_to_master_private_key(seed) == {:ok, xprv} end @@ -389,20 +404,19 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do t = @bip32_test_case_1 seed = t.seed |> Base.decode16!(case: :lower) - {:ok, xprv} = t.xprv_m |> ExtendedKey.parse_extended_key() + {:ok, xprv} = t.xprv_m |> ExtendedKey.parse() deriv = %ExtendedKey.DerivationPath{child_nums: []} assert ExtendedKey.derive_extended_key(seed, deriv) == {:ok, xprv} # derive m/0'/1 - {:ok, xprv} = t.xprv_m_0h_1 |> ExtendedKey.parse_extended_key() + {:ok, xprv} = t.xprv_m_0h_1 |> ExtendedKey.parse() deriv = %ExtendedKey.DerivationPath{child_nums: [@min_hardened_child_num, 1]} assert ExtendedKey.derive_extended_key(seed, deriv) == {:ok, xprv} # derive xprv_m_0h_1_2h_2_1000000000 - {:ok, xprv_m_0h_1_2h_2_1000000000} = - t.xprv_m_0h_1_2h_2_1000000000 |> ExtendedKey.parse_extended_key() + {:ok, xprv_m_0h_1_2h_2_1000000000} = t.xprv_m_0h_1_2h_2_1000000000 |> ExtendedKey.parse() deriv = %ExtendedKey.DerivationPath{ child_nums: [@min_hardened_child_num, 1, @min_hardened_child_num + 2, 2, 1_000_000_000] @@ -416,55 +430,55 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do test "BIP32 tests 2: successfully convert xprv to xpub." do t = @bip32_test_case_2 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m_0) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m_0) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m_0_2147483647h) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0_2147483647h) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m_0_2147483647h) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0_2147483647h) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m_0_2147483647h_1) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0_2147483647h_1) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m_0_2147483647h_1) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0_2147483647h_1) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m_0_2147483647h_1_2147483646h) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0_2147483647h_1_2147483646h) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m_0_2147483647h_1_2147483646h) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0_2147483647h_1_2147483646h) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m_0_2147483647h_1_2147483646h_2) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0_2147483647h_1_2147483646h_2) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m_0_2147483647h_1_2147483646h_2) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0_2147483647h_1_2147483646h_2) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} end test "BIP32 tests 2: derive prv keys in sequence" do t = @bip32_test_case_2 # derive prv child from prv parent_fingerprint - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) {:ok, m_0_xprv} = ExtendedKey.derive_private_child(xprv, 0) - assert ExtendedKey.parse_extended_key(t.xprv_m_0) == {:ok, m_0_xprv} + assert ExtendedKey.parse(t.xprv_m_0) == {:ok, m_0_xprv} # derive child m/0/2147483647h {:ok, m_0_2147483647h_xprv} = ExtendedKey.derive_private_child(m_0_xprv, 2_147_483_647 + @min_hardened_child_num) - assert ExtendedKey.parse_extended_key(t.xprv_m_0_2147483647h) == {:ok, m_0_2147483647h_xprv} + assert ExtendedKey.parse(t.xprv_m_0_2147483647h) == {:ok, m_0_2147483647h_xprv} end test "BIP32 tests 2: derive pub keys from master prv key" do t = @bip32_test_case_2 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) {:ok, m_0_xpub} = ExtendedKey.derive_public_child(xprv, 0) - assert ExtendedKey.parse_extended_key(t.xpub_m_0) == {:ok, m_0_xpub} + assert ExtendedKey.parse(t.xpub_m_0) == {:ok, m_0_xpub} - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) {:ok, xprv_temp} = ExtendedKey.derive_private_child(xprv, 0) {:ok, xprv_temp} = @@ -472,25 +486,25 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do {:ok, m_0_2147483647h_1_xpub} = ExtendedKey.derive_public_child(xprv_temp, 1) - assert ExtendedKey.parse_extended_key(t.xpub_m_0_2147483647h_1) == + assert ExtendedKey.parse(t.xpub_m_0_2147483647h_1) == {:ok, m_0_2147483647h_1_xpub} end test "BIP32 tests 2: derive child pub keys from prv and pubkey" do t = @bip32_test_case_2 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) {:ok, xpub1} = ExtendedKey.derive_public_child(xprv, 0) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m) {:ok, xpub2} = ExtendedKey.derive_public_child(xpub, 0) assert xpub1 == xpub2 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m_0_2147483647h) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m_0_2147483647h) {:ok, xpub1} = ExtendedKey.derive_public_child(xprv, 1) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0_2147483647h) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0_2147483647h) {:ok, xpub2} = ExtendedKey.derive_public_child(xpub, 1) assert xpub1 == xpub2 @@ -500,7 +514,7 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do t = @bip32_test_case_2 seed = t.seed |> Base.decode16!(case: :lower) - {:ok, xprv} = t.xprv_m |> ExtendedKey.parse_extended_key() + {:ok, xprv} = t.xprv_m |> ExtendedKey.parse() assert ExtendedKey.seed_to_master_private_key(seed) == {:ok, xprv} end @@ -509,12 +523,12 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do t = @bip32_test_case_2 seed = t.seed |> Base.decode16!(case: :lower) - {:ok, xprv} = t.xprv_m |> ExtendedKey.parse_extended_key() + {:ok, xprv} = t.xprv_m |> ExtendedKey.parse() deriv = %ExtendedKey.DerivationPath{child_nums: []} assert ExtendedKey.derive_extended_key(seed, deriv) == {:ok, xprv} - {:ok, xprv} = t.xprv_m_0_2147483647h |> ExtendedKey.parse_extended_key() + {:ok, xprv} = t.xprv_m_0_2147483647h |> ExtendedKey.parse() deriv = %ExtendedKey.DerivationPath{ child_nums: [0, 2_147_483_647 + @min_hardened_child_num] @@ -523,7 +537,7 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do assert ExtendedKey.derive_extended_key(seed, deriv) == {:ok, xprv} {:ok, xprv_m_0_2147483647h_1_2147483646h_2} = - t.xprv_m_0_2147483647h_1_2147483646h_2 |> ExtendedKey.parse_extended_key() + t.xprv_m_0_2147483647h_1_2147483646h_2 |> ExtendedKey.parse() deriv = %ExtendedKey.DerivationPath{ child_nums: [ @@ -544,14 +558,14 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do test "BIP32 tests 3: derive public key from private key" do t = @bip32_test_case_3 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} # check that to_extended_public_key is identity for xpub assert ExtendedKey.to_extended_public_key(xpub) == xpub - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m_0h) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m_0h) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m_0h) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m_0h) assert ExtendedKey.to_extended_public_key(xprv) == {:ok, xpub} assert ExtendedKey.to_extended_public_key(xpub) == xpub end @@ -559,25 +573,25 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do test "BIP32 tests 3: derive prv child from parent" do t = @bip32_test_case_3 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) {:ok, xprv_m_0h} = ExtendedKey.derive_private_child(xprv, @min_hardened_child_num) - assert ExtendedKey.parse_extended_key(t.xprv_m_0h) == {:ok, xprv_m_0h} + assert ExtendedKey.parse(t.xprv_m_0h) == {:ok, xprv_m_0h} end test "BIP32 tests 3: derive pub child from prv parent" do t = @bip32_test_case_3 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, xprv} = ExtendedKey.parse(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.display(xpub_m_0h) == t.xpub_m_0h end test "BIP32 tests 3: derive master prv key from seed" do t = @bip32_test_case_3 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) {:ok, s_xprv} = t.seed @@ -590,7 +604,7 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do test "BIP32 tests 3: derive child prv key from seed" do t = @bip32_test_case_3 - {:ok, xprv_m_0h} = ExtendedKey.parse_extended_key(t.xprv_m_0h) + {:ok, xprv_m_0h} = ExtendedKey.parse(t.xprv_m_0h) deriv = %ExtendedKey.DerivationPath{child_nums: [@min_hardened_child_num]} {:ok, s_xprv_m_0h} = @@ -605,7 +619,7 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do describe "Invalid Key testing" do test "invalid key testing" do for t <- @invalid_xkeys do - {err, _} = ExtendedKey.parse_extended_key(t) + {err, _} = ExtendedKey.parse(t) assert err == :error end end @@ -615,8 +629,8 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do test "derive prv and public key, sign msg, verify" do t = @bip32_test_case_1 - {:ok, xprv} = ExtendedKey.parse_extended_key(t.xprv_m) - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m) + {:ok, xprv} = ExtendedKey.parse(t.xprv_m) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m) {:ok, prv} = ExtendedKey.to_private_key(xprv) {:ok, pub} = ExtendedKey.to_public_key(xpub) @@ -632,7 +646,7 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do test "fail to derive hardened child from pubkey parent" do t = @bip32_test_case_3 - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m) {err, _msg} = ExtendedKey.derive_child_key(xpub, @min_hardened_child_num) assert :error == err end @@ -644,7 +658,7 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do child_nums: [@min_hardened_child_num, 1] } - {:ok, xpub} = ExtendedKey.parse_extended_key(t.xpub_m) + {:ok, xpub} = ExtendedKey.parse(t.xpub_m) {err, _msg} = ExtendedKey.derive_extended_key(xpub, deriv) assert :error == err end @@ -664,7 +678,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.display(child_key) == t.xprv_m_0h_1_2h_2 end test "successfully derive xpub child key with derivation path" do @@ -679,11 +693,11 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do ] } - {:ok, xprv_t1} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, xprv_t1} = ExtendedKey.parse(t.xprv_m) {: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.display(child_key) == t.xpub_m_0_2147483647h_1_2147483646h end test "test use of deriv path bip32 test 2" do @@ -699,24 +713,27 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do ] } - {:ok, m} = ExtendedKey.parse_extended_key(t.xprv_m) + {:ok, m} = ExtendedKey.parse(t.xprv_m) {:ok, child_key} = ExtendedKey.derive_extended_key(m, deriv) - assert ExtendedKey.parse_extended_key(t.xprv_m_0_2147483647h_1_2147483646h_2) == + assert ExtendedKey.parse(t.xprv_m_0_2147483647h_1_2147483646h_2) == {:ok, child_key} end end describe "Derivation Path parse/ser testing" do test "from_string/1" do - for t <- @strings_to_derivation_paths do + for t <- @derivation_paths_to_parse do + if ExtendedKey.DerivationPath.from_string(t.str) != {:ok, t.deriv}, + do: IO.puts(t.str) + assert ExtendedKey.DerivationPath.from_string(t.str) == {:ok, t.deriv} end end test "to_string/1" do - for t <- @derivation_paths_to_strings do + for t <- @derivation_paths_to_serialize do assert ExtendedKey.DerivationPath.to_string(t.deriv) == {:ok, t.str} end end @@ -728,4 +745,20 @@ defmodule Bitcoinex.Secp256k1.ExtendedKeyTest do end end end + + describe "Binary encoding (for PSBT)" do + test "to_bin/1 - serialize" do + for t <- @derivation_paths_to_serialize do + assert ExtendedKey.DerivationPath.to_bin(t.deriv) == {:ok, t.bin} + assert ExtendedKey.DerivationPath.from_bin(t.bin) == {:ok, t.deriv} + end + end + + test "to_bin/1 - parse" do + for t <- @derivation_paths_to_parse do + assert ExtendedKey.DerivationPath.to_bin(t.deriv) == {:ok, t.bin} + assert ExtendedKey.DerivationPath.from_bin(t.bin) == {:ok, t.deriv} + end + end + end end diff --git a/test/psbt_test.exs b/test/psbt_test.exs index bcf9385..c2ea453 100644 --- a/test/psbt_test.exs +++ b/test/psbt_test.exs @@ -82,18 +82,8 @@ defmodule Bitcoinex.PSBTTest do } ], expected_out: [ - %Bitcoinex.PSBT.Out{ - bip32_derivation: nil, - proprietary: nil, - redeem_script: nil, - witness_script: nil - }, - %Bitcoinex.PSBT.Out{ - bip32_derivation: nil, - proprietary: nil, - redeem_script: nil, - witness_script: nil - } + %Out{}, + %Out{} ] }, %{ @@ -126,36 +116,33 @@ defmodule Bitcoinex.PSBTTest do value: 9358 } ], - version: 2, - witnesses: nil + version: 2 } }, expected_in: [ %In{ - bip32_derivation: nil, - final_scriptsig: - "47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa88292", - final_scriptwitness: nil, - non_witness_utxo: nil, - partial_sig: nil, - por_commitment: nil, - proprietary: nil, - redeem_script: nil, - sighash_type: nil, - witness_script: nil, - witness_utxo: nil + final_scriptsig: %Bitcoinex.Script{ + items: [ + 71, + <<48, 68, 2, 32, 71, 89, 102, 23, 151, 192, 27, 3, 107, 37, 146, 137, 72, 104, 98, + 24, 52, 125, 137, 134, 75, 113, 158, 31, 127, 207, 87, 209, 229, 17, 101, 135, 2, + 32, 83, 9, 234, 191, 86, 170, 77, 136, 145, 255, 209, 17, 253, 241, 51, 111, 58, + 41, 218, 134, 109, 127, 132, 134, 215, 85, 70, 206, 237, 175, 147, 25, 1>>, + 33, + <<3, 92, 220, 97, 252, 123, 169, 113, 192, 181, 1, 166, 70, 162, 168, 59, 16, 44, + 180, 56, 129, 33, 124, 166, 130, 220, 134, 226, 215, 63, 168, 130, 146>> + ] + } }, %In{ - bip32_derivation: nil, - final_scriptsig: nil, - final_scriptwitness: nil, - non_witness_utxo: nil, - partial_sig: nil, - por_commitment: nil, - proprietary: nil, - redeem_script: "001485d13537f2e265405a34dbafa9e3dda01fb82308", - sighash_type: nil, - witness_script: nil, + redeem_script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<133, 209, 53, 55, 242, 226, 101, 64, 90, 52, 219, 175, 169, 227, 221, 160, 31, + 184, 35, 8>> + ] + }, witness_utxo: %Bitcoinex.Transaction.Out{ script_pub_key: "a9143545e6e33b832c47050f24d3eeb93c9c03948bc787", value: 100_000_000 @@ -163,25 +150,14 @@ defmodule Bitcoinex.PSBTTest do } ], expected_out: [ - %Bitcoinex.PSBT.Out{ - bip32_derivation: nil, - proprietary: nil, - redeem_script: nil, - witness_script: nil - }, - %Bitcoinex.PSBT.Out{ - bip32_derivation: nil, - proprietary: nil, - redeem_script: nil, - witness_script: nil - } + %Out{}, + %Out{} ] }, %{ psbt: "cHNidP8BAFICAAAAAZ38ZijCbFiZ/hvT3DOGZb/VXXraEPYiCXPfLTht7BJ2AQAAAAD/////AfA9zR0AAAAAFgAUezoAv9wU0neVwrdJAdCdpu8TNXkAAAAATwEENYfPAto/0AiAAAAAlwSLGtBEWx7IJ1UXcnyHtOTrwYogP/oPlMAVZr046QADUbdDiH7h1A3DKmBDck8tZFmztaTXPa7I+64EcvO8Q+IM2QxqT64AAIAAAACATwEENYfPAto/0AiAAAABuQRSQnE5zXjCz/JES+NTzVhgXj5RMoXlKLQH+uP2FzUD0wpel8itvFV9rCrZp+OcFyLrrGnmaLbyZnzB1nHIPKsM2QxqT64AAIABAACAAAEBKwBlzR0AAAAAIgAgLFSGEmxJeAeagU4TcV1l82RZ5NbMre0mbQUIZFuvpjIBBUdSIQKdoSzbWyNWkrkVNq/v5ckcOrlHPY5DtTODarRWKZyIcSEDNys0I07Xz5wf6l0F1EFVeSe+lUKxYusC4ass6AIkwAtSriIGAp2hLNtbI1aSuRU2r+/lyRw6uUc9jkO1M4NqtFYpnIhxENkMak+uAACAAAAAgAAAAAAiBgM3KzQjTtfPnB/qXQXUQVV5J76VQrFi6wLhqyzoAiTACxDZDGpPrgAAgAEAAIAAAAAAACICA57/H1R6HV+S36K6evaslxpL0DukpzSwMVaiVritOh75EO3kXMUAAACAAAAAgAEAAIAA", expected_global: %Global{ - proprietary: nil, unsigned_tx: %Bitcoinex.Transaction{ inputs: [ %Bitcoinex.Transaction.In{ @@ -198,47 +174,59 @@ defmodule Bitcoinex.PSBTTest do value: 499_990_000 } ], - version: 2, - witnesses: nil + version: 2 }, - version: nil, xpub: [ %{ - derivation: [2_147_483_822, 2_147_483_648], - master_pfp: 1_332_350_169, + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_822, 2_147_483_648] + }, + pfp: <<217, 12, 106, 79>>, xpub: - "tpubDBkJeJo2X94Yq3RVz65DoUgyLUkaDrkfyrn2VcgyCRSKCRonvKvCF2FpYDGJWDkdRHBajXJGpc63GnumUt63ySvqCu2XaTRGVTKMYGuFk9H" + Bitcoinex.ExtendedKey.parse!( + "tpubDBkJeJo2X94Yq3RVz65DoUgyLUkaDrkfyrn2VcgyCRSKCRonvKvCF2FpYDGJWDkdRHBajXJGpc63GnumUt63ySvqCu2XaTRGVTKMYGuFk9H" + ) }, %{ - derivation: [2_147_483_822, 2_147_483_649], - master_pfp: 1_332_350_169, + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_822, 2_147_483_649] + }, + pfp: <<217, 12, 106, 79>>, xpub: - "tpubDBkJeJo2X94YsvtBEU1eKoibEWiNv51nW5iHhs6VZp59jsE6nen8KZMFyGHuGbCvqjRqirgeMcfpVBkttpUUT6brm4duzSGoZeTbhqCNUu6" + Bitcoinex.ExtendedKey.parse!( + "tpubDBkJeJo2X94YsvtBEU1eKoibEWiNv51nW5iHhs6VZp59jsE6nen8KZMFyGHuGbCvqjRqirgeMcfpVBkttpUUT6brm4duzSGoZeTbhqCNUu6" + ) } ] }, expected_in: [ - %Bitcoinex.PSBT.In{ + %In{ bip32_derivation: [ %{ - derivation: [2_147_483_822, 2_147_483_648, 0], - public_key: "029da12cdb5b235692b91536afefe5c91c3ab9473d8e43b533836ab456299c8871", - pfp: 1_332_350_169 + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_822, 2_147_483_648, 0] + }, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 71_297_889_195_667_677_566_853_709_053_103_131_423_162_117_776_603_813_866_869_556_867_184_864_299_121, + y: + 104_942_558_329_072_212_830_372_841_913_067_137_391_352_852_664_610_839_861_441_469_392_637_640_386_592 + }, + pfp: <<217, 12, 106, 79>> }, %{ - derivation: [2_147_483_822, 2_147_483_649, 0], - public_key: "03372b34234ed7cf9c1fea5d05d441557927be9542b162eb02e1ab2ce80224c00b", - pfp: 1_332_350_169 + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_822, 2_147_483_649, 0] + }, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 24_953_540_938_576_426_582_583_323_081_660_174_044_181_452_719_495_711_599_804_679_737_956_351_918_091, + y: + 115_045_788_123_487_702_367_674_850_763_085_807_099_618_969_835_940_161_186_892_982_331_420_489_690_021 + }, + pfp: <<217, 12, 106, 79>> } ], - final_scriptsig: nil, - final_scriptwitness: nil, - non_witness_utxo: nil, - partial_sig: nil, - por_commitment: nil, - proprietary: nil, - redeem_script: nil, - sighash_type: nil, witness_script: "5221029da12cdb5b235692b91536afefe5c91c3ab9473d8e43b533836ab456299c88712103372b34234ed7cf9c1fea5d05d441557927be9542b162eb02e1ab2ce80224c00b52ae", witness_utxo: %Bitcoinex.Transaction.Out{ @@ -252,14 +240,18 @@ defmodule Bitcoinex.PSBTTest do %Out{ bip32_derivation: [ %{ - derivation: [2_147_483_648, 2_147_483_648, 2_147_483_649], - public_key: "039eff1f547a1d5f92dfa2ba7af6ac971a4bd03ba4a734b03156a256b8ad3a1ef9", - pfp: 3_311_199_469 + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_648, 2_147_483_648, 2_147_483_649] + }, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 71_916_192_309_307_030_987_819_255_271_417_383_252_226_513_354_309_312_442_460_376_541_014_510_935_801, + y: + 87_829_264_540_646_754_532_057_909_755_234_567_745_025_032_566_591_945_062_448_323_127_703_490_972_993 + }, + pfp: <<237, 228, 92, 197>> } - ], - proprietary: nil, - redeem_script: nil, - witness_script: nil + ] } ] }, @@ -267,7 +259,6 @@ defmodule Bitcoinex.PSBTTest do psbt: "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=", expected_global: %Global{ - proprietary: nil, unsigned_tx: %Bitcoinex.Transaction{ inputs: [ %Bitcoinex.Transaction.In{ @@ -284,38 +275,57 @@ defmodule Bitcoinex.PSBTTest do value: 199_908_000 } ], - version: 2, - witnesses: nil - }, - version: nil, - xpub: nil + version: 2 + } }, expected_in: [ - %Bitcoinex.PSBT.In{ + %In{ bip32_derivation: [ %{ - derivation: [2_147_483_648, 2_147_483_648, 2_147_483_652], - public_key: "03b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46", - pfp: 1_740_285_620 + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_648, 2_147_483_648, 2_147_483_652] + }, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 80_151_448_986_003_541_602_445_390_187_849_273_116_474_332_975_424_144_708_997_035_409_020_762_307_910, + y: + 69_508_417_946_258_487_178_124_295_602_214_619_390_077_167_556_721_617_739_677_223_796_084_805_268_603 + }, + pfp: <<180, 166, 186, 103>> }, %{ - derivation: [2_147_483_648, 2_147_483_648, 2_147_483_653], - public_key: "03de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd", - pfp: 1_740_285_620 + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_648, 2_147_483_648, 2_147_483_653] + }, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 100_565_082_940_006_144_500_918_712_336_860_239_214_176_819_872_577_604_419_994_557_980_044_395_840_445, + y: + 10_103_911_892_721_234_278_209_537_983_272_986_690_525_296_344_245_993_216_791_872_735_802_656_255_649 + }, + pfp: <<180, 166, 186, 103>> + } + ], + partial_sig: [ + %{ + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 80_151_448_986_003_541_602_445_390_187_849_273_116_474_332_975_424_144_708_997_035_409_020_762_307_910, + y: + 69_508_417_946_258_487_178_124_295_602_214_619_390_077_167_556_721_617_739_677_223_796_084_805_268_603 + }, + signature: + "304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a01" } ], - final_scriptsig: nil, - final_scriptwitness: nil, - non_witness_utxo: nil, - partial_sig: %{ - public_key: "03b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46", - signature: - "304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a01" + redeem_script: %Bitcoinex.Script{ + items: [ + 0, + 32, + <<119, 31, 209, 138, 212, 89, 102, 109, 212, 159, 61, 86, 78, 61, 188, 66, 244, 200, + 71, 116, 227, 96, 173, 161, 104, 22, 168, 237, 72, 141, 86, 129>> + ] }, - por_commitment: nil, - proprietary: nil, - redeem_script: "0020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681", - sighash_type: nil, witness_script: "522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae", witness_utxo: %Bitcoinex.Transaction.Out{ @@ -325,12 +335,7 @@ defmodule Bitcoinex.PSBTTest do } ], expected_out: [ - %Bitcoinex.PSBT.Out{ - bip32_derivation: nil, - proprietary: nil, - redeem_script: nil, - witness_script: nil - } + %Out{} ] }, %{ @@ -338,7 +343,6 @@ defmodule Bitcoinex.PSBTTest do psbt: "cHNidP8BAKcBAAAAAjHC7gs4NF4rUOrlta+j+wB8UHTEuLn0XY6FDUcGybQMAAAAAAD+////NUUKTkDqBbL9oqrAIk9199/ZANXi/8XEgguqQY8iiewAAAAAAP7///8CdImYAAAAAAAiACCs+u6eefBEoqCFYVWhxscCwh/WJZ+286/E8zNH9gRhd4CWmAAAAAAAF6kUV3ZMSDpAgQZkllBPVNL5uRPlwOOHAAAAAAABASuAlpgAAAAAACIAIDH8Jza8S0T6nWkCcU5GqgwxJ2rGEgWFgDSGiJVFJ5W0AQj9/QAEAEgwRQIhALL4SZucnmwtsJ2BguTQkajOkbvRTRcIMF2B/c26pnZDAiAwNPAWsW3b3PxNXZouG43Z2HJ4WufvpjM0x+VlprgFUAFHMEQCIGV66oyrbw0b9HXA8EeGKrIi88YhTGuhpQKdDxX1VivPAiAcxSrameybDohX8yINx2t452PyyqP6qUiTUMNnoAv+twFpUiECZ3pcsDl1tPNTASW/gFEm/PlWLEnQJN5h32F5qmC2U6AhA1fyyfYB3ma7Vg6JKICdCsQFD7/IchNleJnjTaTGbCFgIQP8V/0ULlUTx5q8mJ6eJh6GaCHkHXDkTnmFbpZRGDsQVVOuAAEBK4CWmAAAAAAAIgAgi3WHXCAbeRTULI6EPlb3Z3+J153IX4zK5bHRsqnrSO4BCPwEAEcwRAIgelTwDK+TOYwP6luGb5htloRgijKLoLmNrjk9imXolaICIFQ9Rq0MrOGcrYHC6BZIyyz+tB0Lm8FhqnARl7R+TpyaAUcwRAIgfHNbxYLcTt1yWeADHyo5ye4jtApn+YTgFzK16IsOW0QCIDcOnv2QYaZlc0etz9kfIrkpoepeTndtvEREKROzqqlCAWlSIQIIPVGeoWYEHRGxyDhpzTqE0uBZIjBj5DDXgBX5QWwecCECL5C1pXxiQ5uiuhZASuHYEUq+gXmXqE+wxPnV590o+HAhA0odK6A98KAdcHcI5pcbNfwR1oq0PsofJzNfvSKkdqCMU64AAQFpUiECPhqS90SDpMEqGW1sAlOsWJz63Vlk/z5sY6711XcFHtQhAk0OObM6tXeCqY/Qan0GUzheUJ7jt03EVVnm22OR0xN4IQNsC65rywLkfIV8SA7R0jiIyK1qZrg6sRHLa5JCr7HHJVOuIgICPhqS90SDpMEqGW1sAlOsWJz63Vlk/z5sY6711XcFHtQgAAAAAAAAAIACAACAAgAAAAAAAAAAAAAAAQAAAA0AAAAiAgJNDjmzOrV3gqmP0Gp9BlM4XlCe47dNxFVZ5ttjkdMTeCAAAAAAAAAAgAIAAIACAAAAAAAAAAAAAAABAAAADQAAACICA2wLrmvLAuR8hXxIDtHSOIjIrWpmuDqxEctrkkKvscclIAAAAAAAAACAAgAAgAIAAAAAAAAAAAAAAAEAAAANAAAAAAA=", expected_global: %Global{ - proprietary: nil, unsigned_tx: %Bitcoinex.Transaction{ inputs: [ %Bitcoinex.Transaction.In{ @@ -366,16 +370,11 @@ defmodule Bitcoinex.PSBTTest do value: 10_000_000 } ], - version: 1, - witnesses: nil - }, - version: nil, - xpub: nil + version: 1 + } }, expected_in: [ - %Bitcoinex.PSBT.In{ - bip32_derivation: nil, - final_scriptsig: nil, + %In{ final_scriptwitness: %Bitcoinex.Transaction.Witness{ txinwitness: [ "", @@ -384,22 +383,13 @@ defmodule Bitcoinex.PSBTTest do "522102677a5cb03975b4f3530125bf805126fcf9562c49d024de61df6179aa60b653a0210357f2c9f601de66bb560e8928809d0ac4050fbfc87213657899e34da4c66c21602103fc57fd142e5513c79abc989e9e261e866821e41d70e44e79856e9651183b105553ae" ] }, - non_witness_utxo: nil, - partial_sig: nil, - por_commitment: nil, - proprietary: nil, - redeem_script: nil, - sighash_type: nil, - witness_script: nil, witness_utxo: %Bitcoinex.Transaction.Out{ script_pub_key: "002031fc2736bc4b44fa9d6902714e46aa0c31276ac61205858034868895452795b4", value: 10_000_000 } }, - %Bitcoinex.PSBT.In{ - bip32_derivation: nil, - final_scriptsig: nil, + %In{ final_scriptwitness: %Bitcoinex.Transaction.Witness{ txinwitness: [ "", @@ -408,13 +398,6 @@ defmodule Bitcoinex.PSBTTest do "522102083d519ea166041d11b1c83869cd3a84d2e059223063e430d78015f9416c1e7021022f90b5a57c62439ba2ba16404ae1d8114abe817997a84fb0c4f9d5e7dd28f87021034a1d2ba03df0a01d707708e6971b35fc11d68ab43eca1f27335fbd22a476a08c53ae" ] }, - non_witness_utxo: nil, - partial_sig: nil, - por_commitment: nil, - proprietary: nil, - redeem_script: nil, - sighash_type: nil, - witness_script: nil, witness_utxo: %Bitcoinex.Transaction.Out{ script_pub_key: "00208b75875c201b7914d42c8e843e56f7677f89d79dc85f8ccae5b1d1b2a9eb48ee", @@ -423,60 +406,179 @@ defmodule Bitcoinex.PSBTTest do } ], expected_out: [ - %Bitcoinex.PSBT.Out{ + %Out{ bip32_derivation: [ %{ - derivation: [2_147_483_648, 2_147_483_650, 2, 0, 0, 1, 13], - public_key: "023e1a92f74483a4c12a196d6c0253ac589cfadd5964ff3e6c63aef5d577051ed4", - pfp: 0 + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_648, 2_147_483_650, 2, 0, 0, 1, 13] + }, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 28_090_348_957_135_603_138_302_582_488_634_744_063_769_373_744_003_534_936_250_327_398_955_197_210_324, + y: + 72_997_351_855_862_386_313_265_421_144_400_773_180_384_569_016_361_889_288_840_365_141_506_887_425_034 + }, + pfp: <<0, 0, 0, 0>> }, %{ - derivation: [2_147_483_648, 2_147_483_650, 2, 0, 0, 1, 13], - public_key: "024d0e39b33ab57782a98fd06a7d0653385e509ee3b74dc45559e6db6391d31378", - pfp: 0 + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_648, 2_147_483_650, 2, 0, 0, 1, 13] + }, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 34_853_223_431_373_393_811_263_769_947_202_683_171_474_774_417_547_322_886_690_942_546_874_362_696_568, + y: + 113_268_030_251_073_357_691_209_583_004_229_905_838_714_795_934_778_666_512_471_345_919_935_656_053_596 + }, + pfp: <<0, 0, 0, 0>> }, %{ - derivation: [2_147_483_648, 2_147_483_650, 2, 0, 0, 1, 13], - public_key: "036c0bae6bcb02e47c857c480ed1d23888c8ad6a66b83ab111cb6b9242afb1c725", - pfp: 0 + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_648, 2_147_483_650, 2, 0, 0, 1, 13] + }, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 48_870_426_774_663_552_239_665_427_301_830_100_617_674_007_218_870_368_410_715_268_929_150_665_213_733, + y: + 384_321_948_423_668_768_499_139_968_164_679_158_512_264_350_568_463_086_818_860_781_694_386_290_525 + }, + pfp: <<0, 0, 0, 0>> } ], - proprietary: nil, - redeem_script: nil, witness_script: "5221023e1a92f74483a4c12a196d6c0253ac589cfadd5964ff3e6c63aef5d577051ed421024d0e39b33ab57782a98fd06a7d0653385e509ee3b74dc45559e6db6391d3137821036c0bae6bcb02e47c857c480ed1d23888c8ad6a66b83ab111cb6b9242afb1c72553ae" }, - %Bitcoinex.PSBT.Out{ - bip32_derivation: nil, - proprietary: nil, - redeem_script: nil, - witness_script: nil - } + %Out{} ] }, %{ psbt: "cHNidP8BAAoAAAAAAAAAAAAAAA==", expected_global: %Global{ - proprietary: nil, unsigned_tx: %Bitcoinex.Transaction{ inputs: [], lock_time: 0, outputs: [], - witnesses: nil, version: 0 - }, - version: nil, - xpub: nil + } }, expected_in: [], expected_out: [] }, + %{ + psbt: + "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEA3wIAAAABJoFxNx7f8oXpN63upLN7eAAMBWbLs61kZBcTykIXG/YAAAAAakcwRAIgcLIkUSPmv0dNYMW1DAQ9TGkaXSQ18Jo0p2YqncJReQoCIAEynKnazygL3zB0DsA5BCJCLIHLRYOUV663b8Eu3ZWzASECZX0RjTNXuOD0ws1G23s59tnDjZpwq8ubLeXcjb/kzjH+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=", + expected_global: %Global{ + unsigned_tx: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "e47b5b7a879f13a8213815cf3dc3f5b35af1e217f412829bc4f75a8ca04909ab", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_294 + }, + %Bitcoinex.Transaction.In{ + prev_txid: "e47b5b7a879f13a8213815cf3dc3f5b35af1e217f412829bc4f75a8ca04909ab", + prev_vout: 1, + script_sig: "", + sequence_no: 4_294_967_294 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 199_900_000, + script_pub_key: "76a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac" + }, + %Bitcoinex.Transaction.Out{ + value: 9358, + script_pub_key: "76a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac" + } + ], + lock_time: 0 + } + }, + expected_in: [ + %In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "f61b1742ca13176464adb3cb66050c00787bb3a4eead37e985f2df1e37718126", + prev_vout: 0, + script_sig: + "473044022070b2245123e6bf474d60c5b50c043d4c691a5d2435f09a34a7662a9dc251790a022001329ca9dacf280bdf30740ec0390422422c81cb45839457aeb76fc12edd95b3012102657d118d3357b8e0f4c2cd46db7b39f6d9c38d9a70abcb9b2de5dc8dbfe4ce31", + sequence_no: 4_294_967_294 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 99_999_699, + script_pub_key: "76a914d0c59903c5bac2868760e90fd521a4665aa7652088ac" + }, + %Bitcoinex.Transaction.Out{ + value: 100_000_000, + script_pub_key: "a9143545e6e33b832c47050f24d3eeb93c9c03948bc787" + } + ], + lock_time: 1_257_139 + } + }, + %In{ + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 100_000_000, + script_pub_key: "a9143545e6e33b832c47050f24d3eeb93c9c03948bc787" + }, + redeem_script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<133, 209, 53, 55, 242, 226, 101, 64, 90, 52, 219, 175, 169, 227, 221, 160, 31, + 184, 35, 8>> + ] + } + } + ], + expected_out: [ + %Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_648, 2_147_483_648, 2_147_483_650] + }, + pfp: <<180, 166, 186, 103>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 106_218_583_072_196_447_736_380_334_552_715_158_727_992_653_635_463_477_362_034_291_905_132_141_629_081, + y: + 111_853_877_987_030_790_093_148_386_887_435_750_835_805_008_949_758_207_878_306_607_752_401_323_629_588 + } + } + ] + }, + %Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_648, 2_147_483_649, 2_147_483_650] + }, + pfp: <<180, 166, 186, 103>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 67_377_249_048_514_558_622_301_148_581_987_854_752_621_201_928_971_282_930_920_275_574_615_944_805_637, + y: + 33_878_338_431_620_821_020_481_726_477_149_418_114_572_000_720_118_999_798_151_283_966_732_510_331_525 + } + } + ] + } + ] + }, %{ psbt: "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQMEAQAAAAAAAA==", expected_global: %Global{ - proprietary: nil, unsigned_tx: %Bitcoinex.Transaction{ + version: 2, inputs: [ %Bitcoinex.Transaction.In{ prev_txid: "f61b1742ca13176464adb3cb66050c00787bb3a4eead37e985f2df1e37718126", @@ -485,29 +587,23 @@ defmodule Bitcoinex.PSBTTest do sequence_no: 4_294_967_294 } ], - lock_time: 1_257_139, outputs: [ %Bitcoinex.Transaction.Out{ - script_pub_key: "76a914d0c59903c5bac2868760e90fd521a4665aa7652088ac", - value: 99_999_699 + value: 99_999_699, + script_pub_key: "76a914d0c59903c5bac2868760e90fd521a4665aa7652088ac" }, %Bitcoinex.Transaction.Out{ - script_pub_key: "a9143545e6e33b832c47050f24d3eeb93c9c03948bc787", - value: 100_000_000 + value: 100_000_000, + script_pub_key: "a9143545e6e33b832c47050f24d3eeb93c9c03948bc787" } ], - witnesses: nil, - version: 2 - }, - version: nil, - xpub: nil + lock_time: 1_257_139 + } }, expected_in: [ - %Bitcoinex.PSBT.In{ - bip32_derivation: nil, - final_scriptsig: nil, - final_scriptwitness: nil, + %In{ non_witness_utxo: %Bitcoinex.Transaction{ + version: 1, inputs: [ %Bitcoinex.Transaction.In{ prev_txid: "e567952fb6cc33857f392efa3a46c995a28f69cca4bb1b37e0204dab1ec7a389", @@ -522,18 +618,16 @@ defmodule Bitcoinex.PSBTTest do sequence_no: 4_294_967_295 } ], - lock_time: 0, outputs: [ %Bitcoinex.Transaction.Out{ - script_pub_key: "76a91485cff1097fd9e008bb34af709c62197b38978a4888ac", - value: 200_000_000 + value: 200_000_000, + script_pub_key: "76a91485cff1097fd9e008bb34af709c62197b38978a4888ac" }, %Bitcoinex.Transaction.Out{ - script_pub_key: "a914339725ba21efd62ac753a9bcd067d6c7a6a39d0587", - value: 190_303_501_938 + value: 190_303_501_938, + script_pub_key: "a914339725ba21efd62ac753a9bcd067d6c7a6a39d0587" } ], - version: 1, witnesses: [ %Bitcoinex.Transaction.Witness{ txinwitness: [ @@ -547,29 +641,2296 @@ defmodule Bitcoinex.PSBTTest do "0223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab3" ] } - ] + ], + lock_time: 0 }, - partial_sig: nil, - por_commitment: nil, - proprietary: nil, - redeem_script: nil, - sighash_type: <<1, 0, 0, 0>>, - witness_script: nil, - witness_utxo: nil + sighash_type: 1 } ], expected_out: [ - %Bitcoinex.PSBT.Out{ - bip32_derivation: nil, - proprietary: nil, - redeem_script: nil, - witness_script: nil + %Out{}, + %Out{} + ] + }, + %{ + psbt: + "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACvABAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=", + expected_global: %Global{ + unsigned_tx: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [%Bitcoinex.Transaction.Out{value: 0, script_pub_key: "6a0100"}], + lock_time: 0 + } + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + unknown: [ + %{ + key: <<240, 1, 2, 3, 4, 5, 6, 7, 8, 9>>, + value: <<1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>> + } + ] + } + ], + expected_out: [ + %Out{} + ] + }, + %{ + psbt: + "cHNidP8BAJ0BAAAAAnEOp2q0XFy2Q45gflnMA3YmmBgFrp4N/ZCJASq7C+U1AQAAAAD/////GQmU1qizyMgsy8+y+6QQaqBmObhyqNRHRlwNQliNbWcAAAAAAP////8CAOH1BQAAAAAZdqkUtrwsDuVlWoQ9ea/t0MzD991kNAmIrGBa9AUAAAAAFgAUEYjvjkzgRJ6qyPsUHL9aEXbmoIgAAAAATwEEiLIeA55TDKyAAAAAPbyKXJdp8DGxfnf+oVGGAyIaGP0Y8rmlTGyMGsdcvDUC8jBYSxVdHH8c1FEgplPEjWULQxtnxbLBPyfXFCA3wWkQJ1acUDEAAIAAAACAAAAAgAABAR8A4fUFAAAAABYAFDO5gvkbKPFgySC0q5XljOUN2jpKIgIDMJaA8zx9446mpHzU7NZvH1pJdHxv+4gI7QkDkkPjrVxHMEQCIC1wTO2DDFapCTRL10K2hS3M0QPpY7rpLTjnUlTSu0JFAiAthsQ3GV30bAztoITyopHD2i1kBw92v5uQsZXn7yj3cgEiBgMwloDzPH3jjqakfNTs1m8fWkl0fG/7iAjtCQOSQ+OtXBgnVpxQMQAAgAAAAIAAAACAAAAAAAEAAAAAAQEfAOH1BQAAAAAWABQ4j7lEMH63fvRRl9CwskXgefAR3iICAsd3Fh9z0LfHK57nveZQKT0T8JW8dlatH1Jdpf0uELEQRzBEAiBMsftfhpyULg4mEAV2ElQ5F5rojcqKncO6CPeVOYj6pgIgUh9JynkcJ9cOJzybFGFphZCTYeJb4nTqIA1+CIJ+UU0BIgYCx3cWH3PQt8crnue95lApPRPwlbx2Vq0fUl2l/S4QsRAYJ1acUDEAAIAAAACAAAAAgAAAAAAAAAAAAAAiAgLSDKUC7iiWhtIYFb1DqAY3sGmOH7zb5MrtRF9sGgqQ7xgnVpxQMQAAgAAAAIAAAACAAAAAAAQAAAAA", + expected_global: %Global{ + unsigned_tx: %Bitcoinex.Transaction{ + version: 1, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "35e50bbb2a018990fd0d9eae051898267603cc597e608e43b65c5cb46aa70e71", + prev_vout: 1, + script_sig: "", + sequence_no: 4_294_967_295 + }, + %Bitcoinex.Transaction.In{ + prev_txid: "676d8d58420d5c4647d4a872b83966a06a10a4fbb2cfcb2cc8c8b3a8d6940919", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 100_000_000, + script_pub_key: "76a914b6bc2c0ee5655a843d79afedd0ccc3f7dd64340988ac" + }, + %Bitcoinex.Transaction.Out{ + value: 99_900_000, + script_pub_key: "00141188ef8e4ce0449eaac8fb141cbf5a1176e6a088" + } + ], + lock_time: 0 }, - %Bitcoinex.PSBT.Out{ - bip32_derivation: nil, - proprietary: nil, - redeem_script: nil, - witness_script: nil + xpub: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_697, 2_147_483_648, 2_147_483_648] + }, + pfp: <<39, 86, 156, 80>>, + xpub: %Bitcoinex.ExtendedKey{ + prefix: <<4, 136, 178, 30>>, + depth: <<3>>, + parent_fingerprint: <<158, 83, 12, 172>>, + child_num: <<128, 0, 0, 0>>, + chaincode: + <<61, 188, 138, 92, 151, 105, 240, 49, 177, 126, 119, 254, 161, 81, 134, 3, 34, + 26, 24, 253, 24, 242, 185, 165, 76, 108, 140, 26, 199, 92, 188, 53>>, + key: + <<2, 242, 48, 88, 75, 21, 93, 28, 127, 28, 212, 81, 32, 166, 83, 196, 141, 101, + 11, 67, 27, 103, 197, 178, 193, 63, 39, 215, 20, 32, 55, 193, 105>>, + checksum: <<230, 83, 80, 24>> + } + } + ] + }, + expected_in: [ + %In{ + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 100_000_000, + script_pub_key: "001433b982f91b28f160c920b4ab95e58ce50dda3a4a" + }, + partial_sig: [ + %{ + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 21_976_933_772_883_498_789_027_591_114_401_201_619_393_627_105_936_886_383_472_697_361_469_744_065_884, + y: + 7_200_326_197_606_395_130_472_957_753_047_368_877_161_908_639_095_988_897_070_963_103_467_471_435_399 + }, + signature: + "304402202d704ced830c56a909344bd742b6852dccd103e963bae92d38e75254d2bb424502202d86c437195df46c0ceda084f2a291c3da2d64070f76bf9b90b195e7ef28f77201" + } + ], + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_697, 2_147_483_648, 2_147_483_648, 0, 1] + }, + pfp: <<39, 86, 156, 80>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 21_976_933_772_883_498_789_027_591_114_401_201_619_393_627_105_936_886_383_472_697_361_469_744_065_884, + y: + 7_200_326_197_606_395_130_472_957_753_047_368_877_161_908_639_095_988_897_070_963_103_467_471_435_399 + } + } + ] + }, + %In{ + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 100_000_000, + script_pub_key: "0014388fb944307eb77ef45197d0b0b245e079f011de" + }, + partial_sig: [ + %{ + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 90_220_664_355_153_390_194_324_778_431_845_886_101_502_662_137_446_551_229_950_573_847_917_083_275_536, + y: + 30_671_849_324_763_586_853_276_572_668_087_977_292_899_353_337_776_873_336_350_612_915_119_803_985_620 + }, + signature: + "304402204cb1fb5f869c942e0e26100576125439179ae88dca8a9dc3ba08f7953988faa60220521f49ca791c27d70e273c9b14616985909361e25be274ea200d7e08827e514d01" + } + ], + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_697, 2_147_483_648, 2_147_483_648, 0, 0] + }, + pfp: <<39, 86, 156, 80>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 90_220_664_355_153_390_194_324_778_431_845_886_101_502_662_137_446_551_229_950_573_847_917_083_275_536, + y: + 30_671_849_324_763_586_853_276_572_668_087_977_292_899_353_337_776_873_336_350_612_915_119_803_985_620 + } + } + ] + } + ], + expected_out: [ + %Out{}, + %Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_697, 2_147_483_648, 2_147_483_648, 0, 4] + }, + pfp: <<39, 86, 156, 80>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 95_008_039_234_411_418_297_358_865_313_145_660_837_471_761_864_556_347_248_745_408_858_102_616_985_839, + y: + 58_958_258_549_746_503_926_788_822_872_770_747_559_389_384_344_394_656_102_791_551_503_991_185_090_332 + } + } + ] + } + ] + }, + %{ + psbt: "cHNidP8BAAoAAAAAAAAAAAAAAA==", + expected_global: %Global{ + unsigned_tx: %Bitcoinex.Transaction{ + version: 0, + inputs: [], + outputs: [], + lock_time: 0 + } + }, + expected_in: [], + expected_out: [] + }, + %{ + psbt: + "cHNidP8BAEwCAAAAAALT3/UFAAAAABl2qRTQxZkDxbrChodg6Q/VIaRmWqdlIIisAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4ezLhMAAAAA", + expected_global: %Global{ + unsigned_tx: %Bitcoinex.Transaction{ + version: 2, + inputs: [], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 99_999_699, + script_pub_key: "76a914d0c59903c5bac2868760e90fd521a4665aa7652088ac" + }, + %Bitcoinex.Transaction.Out{ + value: 100_000_000, + script_pub_key: "a9143545e6e33b832c47050f24d3eeb93c9c03948bc787" + } + ], + lock_time: 1_257_139 + } + }, + expected_in: [], + expected_out: [ + %Out{}, + %Out{} + ] + }, + # BIP 370 https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#test-vectors + %{ + psbt: + "cHNidP8BAgQCAAAAAQQBAQEFAQIB+wQCAAAAAAEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", + expected_global: %Global{ + tx_version: 2, + input_count: 1, + output_count: 2, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQQBAQEFAQIB+wQCAAAAAAEAUgIAAAABwaolbiFLlqGCL5PeQr/ztfP/jQUZMG41FddRWl6AWxIAAAAAAP////8BGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgAAAAABAR8Yxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAQ4gCwrZIUGcHIcZc11y3HOfnqngY40f5MHu8PmUQISBX8gBDwQAAAAAACICAtYB+EhGpnVfd2vgDj2d6PsQrMk1+4PEX7AWLUytWreSGPadhz5UAACAAQAAgAAAAIAAAAAAKgAAAAEDCAAIry8AAAAAAQQWABTEMPZMR1baMQ29GghVcu8pmSYnLAAiAgLjb7/1PdU0Bwz4/TlmFGgPNXqbhdtzQL8c+nRdKtezQBj2nYc+VAAAgAEAAIAAAACAAQAAAGQAAAABAwiLvesLAAAAAAEEFgAUTdGTrJZKVqwbnhzKhFT+L0dPhRMA", + expected_global: %Global{ + tx_version: 2, + input_count: 1, + output_count: 2, + version: 2 + }, + expected_in: [ + %In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQQBAQEFAQIB+wQCAAAAAAEAUgIAAAABwaolbiFLlqGCL5PeQr/ztfP/jQUZMG41FddRWl6AWxIAAAAAAP////8BGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgAAAAABAR8Yxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAQ4gCwrZIUGcHIcZc11y3HOfnqngY40f5MHu8PmUQISBX8gBDwQAAAAAARAE/v///wAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", + expected_global: %Global{ + tx_version: 2, + input_count: 1, + output_count: 2, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0, + sequence: 4_294_967_294 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAEQBP7///8BEQSMjcRiARIEECcAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", + expected_global: %Global{ + tx_version: 2, + fallback_locktime: 0, + input_count: 1, + output_count: 2, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0, + sequence: 4_294_967_294, + required_time_locktime: 1_657_048_460, + required_height_locktime: 10000 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQQBAQEFAQIBBgEBAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", + expected_global: %Global{ + tx_version: 2, + input_count: 1, + output_count: 2, + tx_modifiable: 1, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQQBAQEFAQIBBgECAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", + expected_global: %Global{ + tx_version: 2, + input_count: 1, + output_count: 2, + tx_modifiable: 2, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQQBAQEFAQIBBgEEAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", + expected_global: %Global{ + tx_version: 2, + input_count: 1, + output_count: 2, + tx_modifiable: 4, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQQBAQEFAQIBBgEIAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", + expected_global: %Bitcoinex.PSBT.Global{ + tx_version: 2, + input_count: 1, + output_count: 2, + tx_modifiable: 8, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQQBAQEFAQIBBgEDAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", + expected_global: %Global{ + tx_version: 2, + input_count: 1, + output_count: 2, + tx_modifiable: 3, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQQBAQEFAQIBBgEFAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", + expected_global: %Global{ + tx_version: 2, + input_count: 1, + output_count: 2, + tx_modifiable: 5, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQQBAQEFAQIBBgEGAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", + expected_global: %Global{ + tx_version: 2, + input_count: 1, + output_count: 2, + tx_modifiable: 6, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQQBAQEFAQIBBgEHAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", + expected_global: %Global{ + tx_version: 2, + input_count: 1, + output_count: 2, + tx_modifiable: 7, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQQBAQEFAQIBBgH/AfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", + expected_global: %Global{ + tx_version: 2, + input_count: 1, + output_count: 2, + tx_modifiable: 255, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + %{ + psbt: + "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAQYBBwH7BAIAAAAAAQBSAgAAAAHBqiVuIUuWoYIvk95Cv/O18/+NBRkwbjUV11FaXoBbEgAAAAAA/////wEYxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAAAAAAEBHxjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4BDiALCtkhQZwchxlzXXLcc5+eqeBjjR/kwe7w+ZRAhIFfyAEPBAAAAAABEAT+////AREEjI3EYgESBBAnAAAAIgIC1gH4SEamdV93a+AOPZ3o+xCsyTX7g8RfsBYtTK1at5IY9p2HPlQAAIABAACAAAAAgAAAAAAqAAAAAQMIAAivLwAAAAABBBYAFMQw9kxHVtoxDb0aCFVy7ymZJicsACICAuNvv/U91TQHDPj9OWYUaA81epuF23NAvxz6dF0q17NAGPadhz5UAACAAQAAgAAAAIABAAAAZAAAAAEDCIu96wsAAAAAAQQWABRN0ZOslkpWrBueHMqEVP4vR0+FEwA=", + expected_global: %Global{ + tx_version: 2, + fallback_locktime: 0, + input_count: 1, + output_count: 2, + tx_modifiable: 7, + version: 2 + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + non_witness_utxo: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "125b805e5a51d715356e3019058dfff3b5f3bf42de932f82a1964b216e25aac1", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + } + ], + lock_time: 0 + }, + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 999_999_000, + script_pub_key: "0014b0a3af144208412693ca7d166852b52db0aef06e" + }, + previous_txid: + <<11, 10, 217, 33, 65, 156, 28, 135, 25, 115, 93, 114, 220, 115, 159, 158, 169, 224, + 99, 141, 31, 228, 193, 238, 240, 249, 148, 64, 132, 129, 95, 200>>, + output_index: 0, + sequence: 4_294_967_294, + required_time_locktime: 1_657_048_460, + required_height_locktime: 10000 + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 96_798_430_025_534_287_057_818_182_799_781_202_983_716_126_197_033_126_318_275_350_637_861_242_845_074, + y: + 24_577_140_550_790_602_129_463_064_506_346_425_976_399_276_187_317_960_372_865_959_737_887_833_605_246 + } + } + ], + amount: 800_000_000, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<196, 48, 246, 76, 71, 86, 218, 49, 13, 189, 26, 8, 85, 114, 239, 41, 153, 38, 39, + 44>> + ] + } + }, + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] + }, + pfp: <<246, 157, 135, 62>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 102_872_461_497_842_797_785_413_923_910_218_265_357_521_021_030_644_560_335_939_528_912_485_972_685_632, + y: + 49_593_273_316_372_432_714_030_705_100_641_392_787_703_279_192_668_893_534_395_536_493_521_009_155_774 + } + } + ], + amount: 199_998_859, + script: %Bitcoinex.Script{ + items: [ + 0, + 20, + <<77, 209, 147, 172, 150, 74, 86, 172, 27, 158, 28, 202, 132, 84, 254, 47, 71, 79, + 133, 19>> + ] + } + } + ] + }, + # BIP 371 https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki#test-vectors + %{ + psbt: + "cHNidP8BAFICAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////AUjmBSoBAAAAFgAUdo4e60z0IIZgM/gKzv8PlyB0SWkAAAAAAAEBKwDyBSoBAAAAIlEgWiws9bUs8x+DrS6Npj/wMYPs2PYJx1EK6KSOA5EKB1chFv40kGTJjW4qhT+jybEr2LMEoZwZXGDvp+4jkwRtP6IyGQB3Ky2nVgAAgAEAAIAAAACAAQAAAAAAAAABFyD+NJBkyY1uKoU/o8mxK9izBKGcGVxg76fuI5MEbT+iMgAiAgNrdyptt02HU8mKgnlY3mx4qzMSEJ830+AwRIQkLs5z2Bh3Ky2nVAAAgAEAAIAAAACAAAAAAAAAAAAA", + expected_global: %Global{ + unsigned_tx: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "42b224669be683585854193052ef88b1efe2ee963af26c0dfe2730bfba4a7427", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 4_999_997_000, + script_pub_key: "0014768e1eeb4cf420866033f80aceff0f9720744969" + } + ], + lock_time: 0 + } + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 5_000_000_000, + script_pub_key: "51205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757" + }, + tap_bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_648, 1, 0] + }, + leaf_hashes: [], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 114_980_336_156_212_694_879_327_992_636_798_621_605_698_402_417_475_818_833_771_862_351_800_336_097_842, + y: + 80_701_412_123_039_057_594_876_775_965_687_848_606_566_558_952_919_681_215_239_681_970_556_263_873_620 + } + } + ], + tap_internal_key: %Bitcoinex.Secp256k1.Point{ + x: + 114_980_336_156_212_694_879_327_992_636_798_621_605_698_402_417_475_818_833_771_862_351_800_336_097_842, + y: + 80_701_412_123_039_057_594_876_775_965_687_848_606_566_558_952_919_681_215_239_681_970_556_263_873_620 + } + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 0] + }, + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 48_608_022_430_402_926_014_916_497_053_789_681_947_094_617_356_347_258_841_609_839_742_612_155_560_920, + y: + 69_442_595_448_928_048_809_603_412_737_824_612_252_030_373_964_082_523_340_701_998_153_322_752_075_027 + } + } + ] + } + ] + }, + %{ + psbt: + "cHNidP8BAFICAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////AUjmBSoBAAAAFgAUdo4e60z0IIZgM/gKzv8PlyB0SWkAAAAAAAEBKwDyBSoBAAAAIlEgWiws9bUs8x+DrS6Npj/wMYPs2PYJx1EK6KSOA5EKB1cBE0C7U+yRe62dkGrxuocYHEi4as5aritTYFpyXKdGJWMUdvxvW67a9PLuD0d/NvWPOXDVuCc7fkl7l68uPxJcl680IRb+NJBkyY1uKoU/o8mxK9izBKGcGVxg76fuI5MEbT+iMhkAdystp1YAAIABAACAAAAAgAEAAAAAAAAAARcg/jSQZMmNbiqFP6PJsSvYswShnBlcYO+n7iOTBG0/ojIAIgIDa3cqbbdNh1PJioJ5WN5seKszEhCfN9PgMESEJC7Oc9gYdystp1QAAIABAACAAAAAgAAAAAAAAAAAAA==", + expected_global: %Bitcoinex.PSBT.Global{ + unsigned_tx: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "42b224669be683585854193052ef88b1efe2ee963af26c0dfe2730bfba4a7427", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 4_999_997_000, + script_pub_key: "0014768e1eeb4cf420866033f80aceff0f9720744969" + } + ], + lock_time: 0 + } + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 5_000_000_000, + script_pub_key: "51205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757" + }, + tap_key_sig: + <<187, 83, 236, 145, 123, 173, 157, 144, 106, 241, 186, 135, 24, 28, 72, 184, 106, + 206, 90, 174, 43, 83, 96, 90, 114, 92, 167, 70, 37, 99, 20, 118, 252, 111, 91, 174, + 218, 244, 242, 238, 15, 71, 127, 54, 245, 143, 57, 112, 213, 184, 39, 59, 126, 73, + 123, 151, 175, 46, 63, 18, 92, 151, 175, 52>>, + tap_bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_648, 1, 0] + }, + leaf_hashes: [], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 114_980_336_156_212_694_879_327_992_636_798_621_605_698_402_417_475_818_833_771_862_351_800_336_097_842, + y: + 80_701_412_123_039_057_594_876_775_965_687_848_606_566_558_952_919_681_215_239_681_970_556_263_873_620 + } + } + ], + tap_internal_key: %Bitcoinex.Secp256k1.Point{ + x: + 114_980_336_156_212_694_879_327_992_636_798_621_605_698_402_417_475_818_833_771_862_351_800_336_097_842, + y: + 80_701_412_123_039_057_594_876_775_965_687_848_606_566_558_952_919_681_215_239_681_970_556_263_873_620 + } + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 0] + }, + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 48_608_022_430_402_926_014_916_497_053_789_681_947_094_617_356_347_258_841_609_839_742_612_155_560_920, + y: + 69_442_595_448_928_048_809_603_412_737_824_612_252_030_373_964_082_523_340_701_998_153_322_752_075_027 + } + } + ] + } + ] + }, + %{ + psbt: + "cHNidP8BAF4CAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////AUjmBSoBAAAAIlEgg2mORYxmZOFZXXXaJZfeHiLul9eY5wbEwKS1qYI810MAAAAAAAEBKwDyBSoBAAAAIlEgWiws9bUs8x+DrS6Npj/wMYPs2PYJx1EK6KSOA5EKB1chFv40kGTJjW4qhT+jybEr2LMEoZwZXGDvp+4jkwRtP6IyGQB3Ky2nVgAAgAEAAIAAAACAAQAAAAAAAAABFyD+NJBkyY1uKoU/o8mxK9izBKGcGVxg76fuI5MEbT+iMgABBSARJNp67JLM0GyVRWJkf0N7E4uVchqEvivyJ2u92rPmcSEHESTaeuySzNBslUViZH9DexOLlXIahL4r8idrvdqz5nEZAHcrLadWAACAAQAAgAAAAIAAAAAABQAAAAA=", + expected_global: %Global{ + unsigned_tx: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "42b224669be683585854193052ef88b1efe2ee963af26c0dfe2730bfba4a7427", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 4_999_997_000, + script_pub_key: + "512083698e458c6664e1595d75da2597de1e22ee97d798e706c4c0a4b5a9823cd743" + } + ], + lock_time: 0 + } + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 5_000_000_000, + script_pub_key: "51205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757" + }, + tap_bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_648, 1, 0] + }, + leaf_hashes: [], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 114_980_336_156_212_694_879_327_992_636_798_621_605_698_402_417_475_818_833_771_862_351_800_336_097_842, + y: + 80_701_412_123_039_057_594_876_775_965_687_848_606_566_558_952_919_681_215_239_681_970_556_263_873_620 + } + } + ], + tap_internal_key: %Bitcoinex.Secp256k1.Point{ + x: + 114_980_336_156_212_694_879_327_992_636_798_621_605_698_402_417_475_818_833_771_862_351_800_336_097_842, + y: + 80_701_412_123_039_057_594_876_775_965_687_848_606_566_558_952_919_681_215_239_681_970_556_263_873_620 + } + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + tap_internal_key: %Bitcoinex.Secp256k1.Point{ + x: + 7_754_432_814_978_735_047_277_584_654_213_252_760_875_963_706_567_224_418_638_150_419_547_067_508_337, + y: + 42_423_437_181_898_177_373_695_716_172_273_752_501_072_424_002_872_139_093_103_823_791_254_979_333_346 + }, + tap_bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_648, 0, 5] + }, + leaf_hashes: [], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 7_754_432_814_978_735_047_277_584_654_213_252_760_875_963_706_567_224_418_638_150_419_547_067_508_337, + y: + 42_423_437_181_898_177_373_695_716_172_273_752_501_072_424_002_872_139_093_103_823_791_254_979_333_346 + } + } + ] + } + ] + }, + %{ + psbt: + "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgg2mORYxmZOFZXXXaJZfeHiLul9eY5wbEwKS1qYI810MAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJiFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wG99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwEV8uSQr3zEXE94UR82BXzlxaXFYyWin7RN/CA/NW4fgjICyxOsaCSN6AaqajZZzzwD62gh0JyBFKToaP696GW7bSrMBCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wJfG5v6l/3FP9XJEmZkIEOQG6YqhD1v35fZ4S8HQqabOIyBDILC/FvARtT6nvmFZJKp/J+XSmtIOoRVdhIZ2w7rRsqzAYhXBUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsDNlw4V9T/AyC+VD9Vg/6kZt2FyvgFzaKiZE68HT0ALCRFfLkkK98xFxPeFEfNgV85cWlxWMlop+0TfwgPzVuH4IyD6D3o87zsdDAps59JuF62gsuXJLRnvrUi0GFnLikUcqazAIRYssTrGgkjegGqmo2Wc88A+toIdCcgRSk6Gj+vehlu20jkBzZcOFfU/wMgvlQ/VYP+pGbdhcr4Bc2iomROvB09ACwl3Ky2nVgAAgAEAAIACAACAAAAAAAAAAAAhFkMgsL8W8BG1Pqe+YVkkqn8n5dKa0g6hFV2EhnbDutGyOQERXy5JCvfMRcT3hRHzYFfOXFpcVjJaKftE38ID81bh+HcrLadWAACAAQAAgAEAAIAAAAAAAAAAACEWUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAFAHxGHl0hFvoPejzvOx0MCmzn0m4XraCy5cktGe+tSLQYWcuKRRypOQFvfWIFnpSXoaSiZ1admHbaYBAa/zjjUpubk5zn+RrpcHcrLadWAACAAQAAgAMAAIAAAAAAAAAAAAEXIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrAARgg8DYuL3Wm9CClvePrIh2WrmcgzyX4GJDJWx13WstRXmUAAQUgESTaeuySzNBslUViZH9DexOLlXIahL4r8idrvdqz5nEhBxEk2nrskszQbJVFYmR/Q3sTi5VyGoS+K/Ina73as+ZxGQB3Ky2nVgAAgAEAAIAAAACAAAAAAAUAAAAA", + expected_global: %Global{ + unsigned_tx: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "2695697f810d60b5bc56eb7ff9c6f0546e5572f90120662ea7f90b236587d49b", + prev_vout: 1, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 4_999_997_000, + script_pub_key: + "512083698e458c6664e1595d75da2597de1e22ee97d798e706c4c0a4b5a9823cd743" + } + ], + lock_time: 0 + } + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 5_000_000_000, + script_pub_key: "5120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b692" + }, + tap_leaf_script: [ + %{ + control_block: + <<193, 80, 146, 155, 116, 193, 160, 73, 84, 183, 139, 75, 96, 53, 233, 122, 94, 7, + 138, 90, 15, 40, 236, 150, 213, 71, 191, 238, 154, 206, 128, 58, 192, 111, 125, + 98, 5, 158, 148, 151, 161, 164, 162, 103, 86, 157, 152, 118, 218, 96, 16, 26, + 255, 56, 227, 82, 155, 155, 147, 156, 231, 249, 26, 233, 112, 17, 95, 46, 73, + 10, 247, 204, 69, 196, 247, 133, 17, 243, 96, 87, 206, 92, 90, 92, 86, 50, 90, + 41, 251, 68, 223, 194, 3, 243, 86, 225, 248>>, + leaf_version: 192, + script: %Bitcoinex.Script{ + items: [ + 32, + <<44, 177, 58, 198, 130, 72, 222, 128, 106, 166, 163, 101, 156, 243, 192, 62, + 182, 130, 29, 9, 200, 17, 74, 78, 134, 143, 235, 222, 134, 91, 182, 210>>, + 172 + ] + } + }, + %{ + control_block: + <<193, 80, 146, 155, 116, 193, 160, 73, 84, 183, 139, 75, 96, 53, 233, 122, 94, 7, + 138, 90, 15, 40, 236, 150, 213, 71, 191, 238, 154, 206, 128, 58, 192, 151, 198, + 230, 254, 165, 255, 113, 79, 245, 114, 68, 153, 153, 8, 16, 228, 6, 233, 138, + 161, 15, 91, 247, 229, 246, 120, 75, 193, 208, 169, 166, 206>>, + leaf_version: 192, + script: %Bitcoinex.Script{ + items: [ + 32, + <<67, 32, 176, 191, 22, 240, 17, 181, 62, 167, 190, 97, 89, 36, 170, 127, 39, + 229, 210, 154, 210, 14, 161, 21, 93, 132, 134, 118, 195, 186, 209, 178>>, + 172 + ] + } + }, + %{ + control_block: + <<193, 80, 146, 155, 116, 193, 160, 73, 84, 183, 139, 75, 96, 53, 233, 122, 94, 7, + 138, 90, 15, 40, 236, 150, 213, 71, 191, 238, 154, 206, 128, 58, 192, 205, 151, + 14, 21, 245, 63, 192, 200, 47, 149, 15, 213, 96, 255, 169, 25, 183, 97, 114, + 190, 1, 115, 104, 168, 153, 19, 175, 7, 79, 64, 11, 9, 17, 95, 46, 73, 10, 247, + 204, 69, 196, 247, 133, 17, 243, 96, 87, 206, 92, 90, 92, 86, 50, 90, 41, 251, + 68, 223, 194, 3, 243, 86, 225, 248>>, + leaf_version: 192, + script: %Bitcoinex.Script{ + items: [ + 32, + <<250, 15, 122, 60, 239, 59, 29, 12, 10, 108, 231, 210, 110, 23, 173, 160, 178, + 229, 201, 45, 25, 239, 173, 72, 180, 24, 89, 203, 138, 69, 28, 169>>, + 172 + ] + } + } + ], + tap_bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_650, 0, 0] + }, + leaf_hashes: [ + <<205, 151, 14, 21, 245, 63, 192, 200, 47, 149, 15, 213, 96, 255, 169, 25, 183, + 97, 114, 190, 1, 115, 104, 168, 153, 19, 175, 7, 79, 64, 11, 9>> + ], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 20_214_902_921_207_623_608_562_994_083_326_228_570_924_245_815_299_146_006_330_164_270_236_901_816_018, + y: + 33_295_055_140_301_703_998_324_163_407_344_732_932_777_411_489_229_368_797_248_015_921_617_797_465_334 + } + }, + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_649, 0, 0] + }, + leaf_hashes: [ + <<17, 95, 46, 73, 10, 247, 204, 69, 196, 247, 133, 17, 243, 96, 87, 206, 92, 90, + 92, 86, 50, 90, 41, 251, 68, 223, 194, 3, 243, 86, 225, 248>> + ], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 30_362_719_820_274_234_030_344_172_757_366_317_297_290_310_294_146_862_871_341_483_479_788_794_073_522, + y: + 61_647_782_036_840_226_561_126_690_482_215_573_260_410_436_215_868_166_657_038_664_786_860_649_378_740 + } + }, + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{child_nums: []}, + leaf_hashes: [], + pfp: <<124, 70, 30, 93>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 36_444_060_476_547_731_421_425_013_472_121_489_344_383_018_981_262_552_973_668_657_287_772_036_414_144, + y: + 22_537_504_475_708_154_238_330_251_540_244_790_414_456_712_057_027_634_449_505_794_721_772_594_235_652 + } + }, + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_651, 0, 0] + }, + leaf_hashes: [ + <<111, 125, 98, 5, 158, 148, 151, 161, 164, 162, 103, 86, 157, 152, 118, 218, 96, + 16, 26, 255, 56, 227, 82, 155, 155, 147, 156, 231, 249, 26, 233, 112>> + ], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 113_105_558_507_633_336_913_885_034_341_920_459_137_683_993_564_483_976_665_524_654_145_797_510_995_113, + y: + 107_072_043_951_624_069_052_082_507_734_100_608_508_887_739_811_142_108_528_627_392_964_071_353_710_384 + } + } + ], + tap_internal_key: %Bitcoinex.Secp256k1.Point{ + x: + 36_444_060_476_547_731_421_425_013_472_121_489_344_383_018_981_262_552_973_668_657_287_772_036_414_144, + y: + 22_537_504_475_708_154_238_330_251_540_244_790_414_456_712_057_027_634_449_505_794_721_772_594_235_652 + }, + tap_merkle_root: + <<240, 54, 46, 47, 117, 166, 244, 32, 165, 189, 227, 235, 34, 29, 150, 174, 103, 32, + 207, 37, 248, 24, 144, 201, 91, 29, 119, 90, 203, 81, 94, 101>> + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + tap_internal_key: %Bitcoinex.Secp256k1.Point{ + x: + 7_754_432_814_978_735_047_277_584_654_213_252_760_875_963_706_567_224_418_638_150_419_547_067_508_337, + y: + 42_423_437_181_898_177_373_695_716_172_273_752_501_072_424_002_872_139_093_103_823_791_254_979_333_346, + z: 0 + }, + tap_bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_648, 0, 5] + }, + leaf_hashes: [], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 7_754_432_814_978_735_047_277_584_654_213_252_760_875_963_706_567_224_418_638_150_419_547_067_508_337, + y: + 42_423_437_181_898_177_373_695_716_172_273_752_501_072_424_002_872_139_093_103_823_791_254_979_333_346, + z: 0 + } + } + ] + } + ] + }, + %{ + psbt: + "cHNidP8BAF4CAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////AUjmBSoBAAAAIlEgCoy9yG3hzhwPnK6yLW33ztNoP+Qj4F0eQCqHk0HW9vUAAAAAAAEBKwDyBSoBAAAAIlEgWiws9bUs8x+DrS6Npj/wMYPs2PYJx1EK6KSOA5EKB1chFv40kGTJjW4qhT+jybEr2LMEoZwZXGDvp+4jkwRtP6IyGQB3Ky2nVgAAgAEAAIAAAACAAQAAAAAAAAABFyD+NJBkyY1uKoU/o8mxK9izBKGcGVxg76fuI5MEbT+iMgABBSBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wAEGbwLAIiBzblcpAP4SUliaIUPI88efcaBBLSNTr3VelwHHgmlKAqwCwCIgYxxfO1gyuPvev7GXBM7rMjwh9A96JPQ9aO8MwmsSWWmsAcAiIET6pJoDON5IjI3//s37bzKfOAvVZu8gyN9tgT6rHEJzrCEHRPqkmgM43kiMjf/+zftvMp84C9Vm7yDI322BPqscQnM5AfBreYuSoQ7ZqdC7/Trxc6U7FhfaOkFZygCCFs2Fay4Odystp1YAAIABAACAAQAAgAAAAAADAAAAIQdQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wAUAfEYeXSEHYxxfO1gyuPvev7GXBM7rMjwh9A96JPQ9aO8MwmsSWWk5ARis5AmIl4Xg6nDO67jhyokqenjq7eDy4pbPQ1lhqPTKdystp1YAAIABAACAAgAAgAAAAAADAAAAIQdzblcpAP4SUliaIUPI88efcaBBLSNTr3VelwHHgmlKAjkBKaW0kVCQFi11mv0/4Pk/ozJgVtC0CIy5M8rngmy42Cx3Ky2nVgAAgAEAAIADAACAAAAAAAMAAAAA", + expected_global: %Global{ + unsigned_tx: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "42b224669be683585854193052ef88b1efe2ee963af26c0dfe2730bfba4a7427", + prev_vout: 0, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 4_999_997_000, + script_pub_key: + "51200a8cbdc86de1ce1c0f9caeb22d6df7ced3683fe423e05d1e402a879341d6f6f5" + } + ], + lock_time: 0 + } + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 5_000_000_000, + script_pub_key: "51205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757" + }, + tap_bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_648, 1, 0] + }, + leaf_hashes: [], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 114_980_336_156_212_694_879_327_992_636_798_621_605_698_402_417_475_818_833_771_862_351_800_336_097_842, + y: + 80_701_412_123_039_057_594_876_775_965_687_848_606_566_558_952_919_681_215_239_681_970_556_263_873_620 + } + } + ], + tap_internal_key: %Bitcoinex.Secp256k1.Point{ + x: + 114_980_336_156_212_694_879_327_992_636_798_621_605_698_402_417_475_818_833_771_862_351_800_336_097_842, + y: + 80_701_412_123_039_057_594_876_775_965_687_848_606_566_558_952_919_681_215_239_681_970_556_263_873_620 + } + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + tap_internal_key: %Bitcoinex.Secp256k1.Point{ + x: + 36_444_060_476_547_731_421_425_013_472_121_489_344_383_018_981_262_552_973_668_657_287_772_036_414_144, + y: + 22_537_504_475_708_154_238_330_251_540_244_790_414_456_712_057_027_634_449_505_794_721_772_594_235_652 + }, + tap_tree: %{ + leaves: [ + %{ + depth: 2, + leaf_version: 192, + script: %Bitcoinex.Script{ + items: [ + 32, + <<115, 110, 87, 41, 0, 254, 18, 82, 88, 154, 33, 67, 200, 243, 199, 159, 113, + 160, 65, 45, 35, 83, 175, 117, 94, 151, 1, 199, 130, 105, 74, 2>>, + 172 + ] + } + }, + %{ + depth: 2, + leaf_version: 192, + script: %Bitcoinex.Script{ + items: [ + 32, + <<99, 28, 95, 59, 88, 50, 184, 251, 222, 191, 177, 151, 4, 206, 235, 50, 60, + 33, 244, 15, 122, 36, 244, 61, 104, 239, 12, 194, 107, 18, 89, 105>>, + 172 + ] + } + }, + %{ + depth: 1, + leaf_version: 192, + script: %Bitcoinex.Script{ + items: [ + 32, + <<68, 250, 164, 154, 3, 56, 222, 72, 140, 141, 255, 254, 205, 251, 111, 50, + 159, 56, 11, 213, 102, 239, 32, 200, 223, 109, 129, 62, 171, 28, 66, 115>>, + 172 + ] + } + } + ] + }, + tap_bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_649, 0, 3] + }, + leaf_hashes: [ + <<240, 107, 121, 139, 146, 161, 14, 217, 169, 208, 187, 253, 58, 241, 115, 165, + 59, 22, 23, 218, 58, 65, 89, 202, 0, 130, 22, 205, 133, 107, 46, 14>> + ], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 31_200_121_508_428_702_019_893_244_742_884_023_762_479_223_940_651_285_251_383_304_714_523_995_030_131, + y: + 53_120_362_633_623_697_201_506_777_401_802_198_637_852_190_256_815_688_867_012_397_976_660_138_781_348, + z: 0 + } + }, + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{child_nums: []}, + leaf_hashes: [], + pfp: <<124, 70, 30, 93>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 36_444_060_476_547_731_421_425_013_472_121_489_344_383_018_981_262_552_973_668_657_287_772_036_414_144, + y: + 22_537_504_475_708_154_238_330_251_540_244_790_414_456_712_057_027_634_449_505_794_721_772_594_235_652 + } + }, + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_650, 0, 3] + }, + leaf_hashes: [ + <<24, 172, 228, 9, 136, 151, 133, 224, 234, 112, 206, 235, 184, 225, 202, 137, 42, + 122, 120, 234, 237, 224, 242, 226, 150, 207, 67, 89, 97, 168, 244, 202>> + ], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 44_829_100_993_385_313_407_048_989_888_172_542_182_954_521_609_934_891_498_742_790_273_562_110_482_793, + y: + 35_170_189_511_235_529_380_611_046_465_697_762_403_784_417_654_141_736_675_144_354_496_257_103_432_480, + z: 0 + } + }, + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_651, 0, 3] + }, + leaf_hashes: [ + <<41, 165, 180, 145, 80, 144, 22, 45, 117, 154, 253, 63, 224, 249, 63, 163, 50, + 96, 86, 208, 180, 8, 140, 185, 51, 202, 231, 130, 108, 184, 216, 44>> + ], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 52_210_932_321_595_760_052_581_731_536_224_183_934_983_599_287_982_449_328_637_004_030_112_510_331_394, + y: + 77_569_177_239_768_622_921_463_492_097_741_805_392_046_359_270_612_182_587_656_363_386_108_779_135_734, + z: 0 + } + } + ] + } + ] + }, + %{ + psbt: + "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgg2mORYxmZOFZXXXaJZfeHiLul9eY5wbEwKS1qYI810MAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJBFCyxOsaCSN6AaqajZZzzwD62gh0JyBFKToaP696GW7bSzZcOFfU/wMgvlQ/VYP+pGbdhcr4Bc2iomROvB09ACwlAv4GNl1fW/+tTi6BX+0wfxOD17xhudlvrVkeR4Cr1/T1eJVHU404z2G8na4LJnHmu0/A5Wgge/NLMLGXdfmk9eUEUQyCwvxbwEbU+p75hWSSqfyfl0prSDqEVXYSGdsO60bIRXy5JCvfMRcT3hRHzYFfOXFpcVjJaKftE38ID81bh+EDh8atvq/omsjbyGDNxncHUKKt2jYD5H5mI2KvvR7+4Y7sfKlKfdowV8AzjTsKDzcB+iPhCi+KPbvZAQ8MpEYEaQRT6D3o87zsdDAps59JuF62gsuXJLRnvrUi0GFnLikUcqW99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwQOwfA3kgZGHIM0IoVCMyZwirAx8NpKJT7kWq+luMkgNNi2BUkPjNE+APmJmJuX4hX6o28S3uNpPS2szzeBwXV/ZiFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wG99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwEV8uSQr3zEXE94UR82BXzlxaXFYyWin7RN/CA/NW4fgjICyxOsaCSN6AaqajZZzzwD62gh0JyBFKToaP696GW7bSrMBCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wJfG5v6l/3FP9XJEmZkIEOQG6YqhD1v35fZ4S8HQqabOIyBDILC/FvARtT6nvmFZJKp/J+XSmtIOoRVdhIZ2w7rRsqzAYhXBUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsDNlw4V9T/AyC+VD9Vg/6kZt2FyvgFzaKiZE68HT0ALCRFfLkkK98xFxPeFEfNgV85cWlxWMlop+0TfwgPzVuH4IyD6D3o87zsdDAps59JuF62gsuXJLRnvrUi0GFnLikUcqazAIRYssTrGgkjegGqmo2Wc88A+toIdCcgRSk6Gj+vehlu20jkBzZcOFfU/wMgvlQ/VYP+pGbdhcr4Bc2iomROvB09ACwl3Ky2nVgAAgAEAAIACAACAAAAAAAAAAAAhFkMgsL8W8BG1Pqe+YVkkqn8n5dKa0g6hFV2EhnbDutGyOQERXy5JCvfMRcT3hRHzYFfOXFpcVjJaKftE38ID81bh+HcrLadWAACAAQAAgAEAAIAAAAAAAAAAACEWUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAFAHxGHl0hFvoPejzvOx0MCmzn0m4XraCy5cktGe+tSLQYWcuKRRypOQFvfWIFnpSXoaSiZ1admHbaYBAa/zjjUpubk5zn+RrpcHcrLadWAACAAQAAgAMAAIAAAAAAAAAAAAEXIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrAARgg8DYuL3Wm9CClvePrIh2WrmcgzyX4GJDJWx13WstRXmUAAQUgESTaeuySzNBslUViZH9DexOLlXIahL4r8idrvdqz5nEhBxEk2nrskszQbJVFYmR/Q3sTi5VyGoS+K/Ina73as+ZxGQB3Ky2nVgAAgAEAAIAAAACAAAAAAAUAAAAA", + expected_global: %Global{ + unsigned_tx: %Bitcoinex.Transaction{ + version: 2, + inputs: [ + %Bitcoinex.Transaction.In{ + prev_txid: "2695697f810d60b5bc56eb7ff9c6f0546e5572f90120662ea7f90b236587d49b", + prev_vout: 1, + script_sig: "", + sequence_no: 4_294_967_295 + } + ], + outputs: [ + %Bitcoinex.Transaction.Out{ + value: 4_999_997_000, + script_pub_key: + "512083698e458c6664e1595d75da2597de1e22ee97d798e706c4c0a4b5a9823cd743" + } + ], + lock_time: 0 + } + }, + expected_in: [ + %Bitcoinex.PSBT.In{ + witness_utxo: %Bitcoinex.Transaction.Out{ + value: 5_000_000_000, + script_pub_key: "5120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b692" + }, + tap_script_sig: [ + %{ + leaf_hash: + <<205, 151, 14, 21, 245, 63, 192, 200, 47, 149, 15, 213, 96, 255, 169, 25, 183, + 97, 114, 190, 1, 115, 104, 168, 153, 19, 175, 7, 79, 64, 11, 9>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 20_214_902_921_207_623_608_562_994_083_326_228_570_924_245_815_299_146_006_330_164_270_236_901_816_018, + y: + 33_295_055_140_301_703_998_324_163_407_344_732_932_777_411_489_229_368_797_248_015_921_617_797_465_334 + }, + signature: + <<191, 129, 141, 151, 87, 214, 255, 235, 83, 139, 160, 87, 251, 76, 31, 196, 224, + 245, 239, 24, 110, 118, 91, 235, 86, 71, 145, 224, 42, 245, 253, 61, 94, 37, 81, + 212, 227, 78, 51, 216, 111, 39, 107, 130, 201, 156, 121, 174, 211, 240, 57, 90, + 8, 30, 252, 210, 204, 44, 101, 221, 126, 105, 61, 121>> + }, + %{ + leaf_hash: + <<17, 95, 46, 73, 10, 247, 204, 69, 196, 247, 133, 17, 243, 96, 87, 206, 92, 90, + 92, 86, 50, 90, 41, 251, 68, 223, 194, 3, 243, 86, 225, 248>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 30_362_719_820_274_234_030_344_172_757_366_317_297_290_310_294_146_862_871_341_483_479_788_794_073_522, + y: + 61_647_782_036_840_226_561_126_690_482_215_573_260_410_436_215_868_166_657_038_664_786_860_649_378_740 + }, + signature: + <<225, 241, 171, 111, 171, 250, 38, 178, 54, 242, 24, 51, 113, 157, 193, 212, 40, + 171, 118, 141, 128, 249, 31, 153, 136, 216, 171, 239, 71, 191, 184, 99, 187, 31, + 42, 82, 159, 118, 140, 21, 240, 12, 227, 78, 194, 131, 205, 192, 126, 136, 248, + 66, 139, 226, 143, 110, 246, 64, 67, 195, 41, 17, 129, 26>> + }, + %{ + leaf_hash: + <<111, 125, 98, 5, 158, 148, 151, 161, 164, 162, 103, 86, 157, 152, 118, 218, 96, + 16, 26, 255, 56, 227, 82, 155, 155, 147, 156, 231, 249, 26, 233, 112>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 113_105_558_507_633_336_913_885_034_341_920_459_137_683_993_564_483_976_665_524_654_145_797_510_995_113, + y: + 107_072_043_951_624_069_052_082_507_734_100_608_508_887_739_811_142_108_528_627_392_964_071_353_710_384 + }, + signature: + <<236, 31, 3, 121, 32, 100, 97, 200, 51, 66, 40, 84, 35, 50, 103, 8, 171, 3, 31, + 13, 164, 162, 83, 238, 69, 170, 250, 91, 140, 146, 3, 77, 139, 96, 84, 144, 248, + 205, 19, 224, 15, 152, 153, 137, 185, 126, 33, 95, 170, 54, 241, 45, 238, 54, + 147, 210, 218, 204, 243, 120, 28, 23, 87, 246>> + } + ], + tap_leaf_script: [ + %{ + control_block: + <<193, 80, 146, 155, 116, 193, 160, 73, 84, 183, 139, 75, 96, 53, 233, 122, 94, 7, + 138, 90, 15, 40, 236, 150, 213, 71, 191, 238, 154, 206, 128, 58, 192, 111, 125, + 98, 5, 158, 148, 151, 161, 164, 162, 103, 86, 157, 152, 118, 218, 96, 16, 26, + 255, 56, 227, 82, 155, 155, 147, 156, 231, 249, 26, 233, 112, 17, 95, 46, 73, + 10, 247, 204, 69, 196, 247, 133, 17, 243, 96, 87, 206, 92, 90, 92, 86, 50, 90, + 41, 251, 68, 223, 194, 3, 243, 86, 225, 248>>, + leaf_version: 192, + script: %Bitcoinex.Script{ + items: [ + 32, + <<44, 177, 58, 198, 130, 72, 222, 128, 106, 166, 163, 101, 156, 243, 192, 62, + 182, 130, 29, 9, 200, 17, 74, 78, 134, 143, 235, 222, 134, 91, 182, 210>>, + 172 + ] + } + }, + %{ + control_block: + <<193, 80, 146, 155, 116, 193, 160, 73, 84, 183, 139, 75, 96, 53, 233, 122, 94, 7, + 138, 90, 15, 40, 236, 150, 213, 71, 191, 238, 154, 206, 128, 58, 192, 151, 198, + 230, 254, 165, 255, 113, 79, 245, 114, 68, 153, 153, 8, 16, 228, 6, 233, 138, + 161, 15, 91, 247, 229, 246, 120, 75, 193, 208, 169, 166, 206>>, + leaf_version: 192, + script: %Bitcoinex.Script{ + items: [ + 32, + <<67, 32, 176, 191, 22, 240, 17, 181, 62, 167, 190, 97, 89, 36, 170, 127, 39, + 229, 210, 154, 210, 14, 161, 21, 93, 132, 134, 118, 195, 186, 209, 178>>, + 172 + ] + } + }, + %{ + control_block: + <<193, 80, 146, 155, 116, 193, 160, 73, 84, 183, 139, 75, 96, 53, 233, 122, 94, 7, + 138, 90, 15, 40, 236, 150, 213, 71, 191, 238, 154, 206, 128, 58, 192, 205, 151, + 14, 21, 245, 63, 192, 200, 47, 149, 15, 213, 96, 255, 169, 25, 183, 97, 114, + 190, 1, 115, 104, 168, 153, 19, 175, 7, 79, 64, 11, 9, 17, 95, 46, 73, 10, 247, + 204, 69, 196, 247, 133, 17, 243, 96, 87, 206, 92, 90, 92, 86, 50, 90, 41, 251, + 68, 223, 194, 3, 243, 86, 225, 248>>, + leaf_version: 192, + script: %Bitcoinex.Script{ + items: [ + 32, + <<250, 15, 122, 60, 239, 59, 29, 12, 10, 108, 231, 210, 110, 23, 173, 160, 178, + 229, 201, 45, 25, 239, 173, 72, 180, 24, 89, 203, 138, 69, 28, 169>>, + 172 + ] + } + } + ], + tap_bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_650, 0, 0] + }, + leaf_hashes: [ + <<205, 151, 14, 21, 245, 63, 192, 200, 47, 149, 15, 213, 96, 255, 169, 25, 183, + 97, 114, 190, 1, 115, 104, 168, 153, 19, 175, 7, 79, 64, 11, 9>> + ], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 20_214_902_921_207_623_608_562_994_083_326_228_570_924_245_815_299_146_006_330_164_270_236_901_816_018, + y: + 33_295_055_140_301_703_998_324_163_407_344_732_932_777_411_489_229_368_797_248_015_921_617_797_465_334 + } + }, + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_649, 0, 0] + }, + leaf_hashes: [ + <<17, 95, 46, 73, 10, 247, 204, 69, 196, 247, 133, 17, 243, 96, 87, 206, 92, 90, + 92, 86, 50, 90, 41, 251, 68, 223, 194, 3, 243, 86, 225, 248>> + ], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 30_362_719_820_274_234_030_344_172_757_366_317_297_290_310_294_146_862_871_341_483_479_788_794_073_522, + y: + 61_647_782_036_840_226_561_126_690_482_215_573_260_410_436_215_868_166_657_038_664_786_860_649_378_740 + } + }, + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{child_nums: []}, + leaf_hashes: [], + pfp: <<124, 70, 30, 93>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 36_444_060_476_547_731_421_425_013_472_121_489_344_383_018_981_262_552_973_668_657_287_772_036_414_144, + y: + 22_537_504_475_708_154_238_330_251_540_244_790_414_456_712_057_027_634_449_505_794_721_772_594_235_652 + } + }, + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_651, 0, 0] + }, + leaf_hashes: [ + <<111, 125, 98, 5, 158, 148, 151, 161, 164, 162, 103, 86, 157, 152, 118, 218, 96, + 16, 26, 255, 56, 227, 82, 155, 155, 147, 156, 231, 249, 26, 233, 112>> + ], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 113_105_558_507_633_336_913_885_034_341_920_459_137_683_993_564_483_976_665_524_654_145_797_510_995_113, + y: + 107_072_043_951_624_069_052_082_507_734_100_608_508_887_739_811_142_108_528_627_392_964_071_353_710_384 + } + } + ], + tap_internal_key: %Bitcoinex.Secp256k1.Point{ + x: + 36_444_060_476_547_731_421_425_013_472_121_489_344_383_018_981_262_552_973_668_657_287_772_036_414_144, + y: + 22_537_504_475_708_154_238_330_251_540_244_790_414_456_712_057_027_634_449_505_794_721_772_594_235_652 + }, + tap_merkle_root: + <<240, 54, 46, 47, 117, 166, 244, 32, 165, 189, 227, 235, 34, 29, 150, 174, 103, 32, + 207, 37, 248, 24, 144, 201, 91, 29, 119, 90, 203, 81, 94, 101>> + } + ], + expected_out: [ + %Bitcoinex.PSBT.Out{ + tap_internal_key: %Bitcoinex.Secp256k1.Point{ + x: + 7_754_432_814_978_735_047_277_584_654_213_252_760_875_963_706_567_224_418_638_150_419_547_067_508_337, + y: + 42_423_437_181_898_177_373_695_716_172_273_752_501_072_424_002_872_139_093_103_823_791_254_979_333_346, + z: 0 + }, + tap_bip32_derivation: [ + %{ + derivation: %Bitcoinex.ExtendedKey.DerivationPath{ + child_nums: [2_147_483_734, 2_147_483_649, 2_147_483_648, 0, 5] + }, + leaf_hashes: [], + pfp: <<119, 43, 45, 167>>, + public_key: %Bitcoinex.Secp256k1.Point{ + x: + 7_754_432_814_978_735_047_277_584_654_213_252_760_875_963_706_567_224_418_638_150_419_547_067_508_337, + y: + 42_423_437_181_898_177_373_695_716_172_273_752_501_072_424_002_872_139_093_103_823_791_254_979_333_346, + z: 0 + } + } + ] } ] } diff --git a/test/script_test.exs b/test/script_test.exs index d7072cb..8d9605b 100644 --- a/test/script_test.exs +++ b/test/script_test.exs @@ -4,6 +4,7 @@ defmodule Bitcoinex.ScriptTest do alias Bitcoinex.{Script, Utils} alias Bitcoinex.Secp256k1.Point + alias Bitcoinex.Taproot @raw_multisig_scripts [ "522103a882d414e478039cd5b52a92ffb13dd5e6bd4515497439dffd691a0f12af957521036ce31db9bdd543e72fe3039a1f1c047dab87037c36a669ff90e28da1848f640d210311ffd36c70776538d079fbae117dc38effafb33304af83ce4894589747aee1ef53ae", @@ -33,6 +34,14 @@ defmodule Bitcoinex.ScriptTest do wsh_addr: "bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej", sh_addr: "" }, + # from tx: a6e48d3b1c63598598c0f79886bdd059a9a472b071d34c706bedc2288d2cfdcf + %{ + script_hex: + "52210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae", + network: :mainnet, + wsh_addr: "bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej", + sh_addr: "" + }, # from tx: 578e8ab81e6eff430fd37eaa460377387d75f0221881fe13cf9e5b400d98b0e3 %{ script_hex: @@ -51,6 +60,17 @@ defmodule Bitcoinex.ScriptTest do } ] + @raw_sorted_multisig_with_data [ + # from tx: a6e48d3b1c63598598c0f79886bdd059a9a472b071d34c706bedc2288d2cfdcf + %{ + script_hex: + "52210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae", + network: :mainnet, + wsh_addr: "bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej", + sh_addr: "" + } + ] + @p2pk_scripts [ # from tx df2b060fa2e5e9c8ed5eaf6a45c13753ec8c63282b2688322eba40cd98ea067a "4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac", @@ -162,7 +182,319 @@ defmodule Bitcoinex.ScriptTest do "bc1gmk9yu" ] - describe "test basics functions" do + # from https://github.com/bitcoin/bips/blob/master/bip-0341/wallet-test-vectors.json + @bip_341_script_pubkey_test_vectors [ + %{ + given: %{ + internal_pubkey: "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d", + script_tree: nil + }, + intermediary: %{ + merkle_root: nil, + tweak: "b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70", + tweaked_pubkey: "53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343" + }, + expected: %{ + script_pubkey: "512053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + bip350_address: "bc1p2wsldez5mud2yam29q22wgfh9439spgduvct83k3pm50fcxa5dps59h4z5" + } + }, + %{ + given: %{ + internal_pubkey: "187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27", + script_tree: + Taproot.TapLeaf.from_string( + # id: 0, + # version + 192, + # script + "20d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8ac" + ) + }, + intermediary: %{ + leaf_hashes: [ + "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21" + ], + merkle_root: "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21", + tweak: "cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001", + tweaked_pubkey: "147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3" + }, + expected: %{ + script_pubkey: "5120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + bip350_address: "bc1pz37fc4cn9ah8anwm4xqqhvxygjf9rjf2resrw8h8w4tmvcs0863sa2e586", + script_path_control_blocks: [ + "c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27" + ] + } + }, + %{ + given: %{ + internal_pubkey: "93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820", + script_tree: + Taproot.TapLeaf.from_string( + # id: 0, + # version + 192, + # script + "20b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007ac" + ) + }, + intermediary: %{ + leaf_hashes: [ + "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b" + ], + merkle_root: "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b", + tweak: "6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30", + tweaked_pubkey: "e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e" + }, + expected: %{ + script_pubkey: "5120e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + bip350_address: "bc1punvppl2stp38f7kwv2u2spltjuvuaayuqsthe34hd2dyy5w4g58qqfuag5", + script_path_control_blocks: [ + "c093478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820" + ] + } + }, + %{ + given: %{ + internal_pubkey: "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + script_tree: { + Taproot.TapLeaf.from_string( + # id: 0, + # version + 192, + # script + "20387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48ac" + ), + Taproot.TapLeaf.from_string( + # id: 1, + # version + 250, + # script + "06424950333431" + ) + } + }, + intermediary: %{ + leaf_hashes: [ + "8ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7", + "f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a" + ], + merkle_root: "6c2dc106ab816b73f9d07e3cd1ef2c8c1256f519748e0813e4edd2405d277bef", + tweak: "9e0517edc8259bb3359255400b23ca9507f2a91cd1e4250ba068b4eafceba4a9", + tweaked_pubkey: "712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5" + }, + expected: %{ + script_pubkey: "5120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5", + bip350_address: "bc1pwyjywgrd0ffr3tx8laflh6228dj98xkjj8rum0zfpd6h0e930h6saqxrrm", + script_path_control_blocks: [ + "c0ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a", + "faee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf37865928ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7" + ] + } + }, + %{ + given: %{ + internal_pubkey: "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + script_tree: { + Taproot.TapLeaf.from_string( + # id: 0, + # version + 192, + # script + "2044b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fdac" + ), + Taproot.TapLeaf.from_string( + # id: 1, + # version + 192, + # script + "07546170726f6f74" + ) + } + }, + intermediary: %{ + leaf_hashes: [ + "64512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89", + "2cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb" + ], + merkle_root: "ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc", + tweak: "639f0281b7ac49e742cd25b7f188657626da1ad169209078e2761cefd91fd65e", + tweaked_pubkey: "77e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220" + }, + expected: %{ + script_pubkey: "512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220", + bip350_address: "bc1pwl3s54fzmk0cjnpl3w9af39je7pv5ldg504x5guk2hpecpg2kgsqaqstjq", + script_path_control_blocks: [ + "c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd82cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb", + "c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd864512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89" + ] + } + }, + %{ + given: %{ + internal_pubkey: "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + script_tree: { + Taproot.TapLeaf.from_string( + # id: 0, + 192, + # script + "2072ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69ac" + ), + { + Taproot.TapLeaf.from_string( + # id: 1, + 192, + # script + "202352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8ac" + ), + Taproot.TapLeaf.from_string( + # id: 2, + 192, + # script + "207337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186aac" + ) + } + } + }, + intermediary: %{ + leaf_hashes: [ + "2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", + "ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c", + "9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf6" + ], + merkle_root: "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + tweak: "b57bfa183d28eeb6ad688ddaabb265b4a41fbf68e5fed2c72c74de70d5a786f4", + tweaked_pubkey: "91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605" + }, + expected: %{ + script_pubkey: "512091b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + bip350_address: "bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e", + script_path_control_blocks: [ + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fffe578e9ea769027e4f5a3de40732f75a88a6353a09d767ddeb66accef85e553", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" + ] + } + }, + %{ + given: %{ + internal_pubkey: "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + script_tree: { + Taproot.TapLeaf.from_string( + # id: 0, + # version + 192, + # script + "2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac" + ), + { + Taproot.TapLeaf.from_string( + # id: 1, + # version + 192, + # script + "20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac" + ), + Taproot.TapLeaf.from_string( + # id: 2, + # version + 192, + # script + "20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac" + ) + } + } + }, + intermediary: %{ + leaf_hashes: [ + "f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711", + "d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7" + ], + merkle_root: "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + tweak: "6579138e7976dc13b6a92f7bfd5a2fc7684f5ea42419d43368301470f3b74ed9", + tweaked_pubkey: "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831" + }, + expected: %{ + script_pubkey: "512075169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + bip350_address: "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", + script_path_control_blocks: [ + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d3cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312dd7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + ] + } + } + ] + + # https://github.com/bitcoin/bips/blob/master/bip-0067.mediawiki#test-vectors + @bip67_test_vectors_m 2 + @bip67_tests [ + %{ + pubkeys: [ + "02ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8", + "02fe6f0a5a297eb38c391581c4413e084773ea23954d93f7753db7dc0adc188b2f" + ], + sorted: [ + "02fe6f0a5a297eb38c391581c4413e084773ea23954d93f7753db7dc0adc188b2f", + "02ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8" + ], + script: + "522102fe6f0a5a297eb38c391581c4413e084773ea23954d93f7753db7dc0adc188b2f2102ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f852ae", + address: "39bgKC7RFbpoCRbtD5KEdkYKtNyhpsNa3Z" + }, + # (Already sorted, no action required) + %{ + pubkeys: [ + "02632b12f4ac5b1d1b72b2a3b508c19172de44f6f46bcee50ba33f3f9291e47ed0", + "027735a29bae7780a9755fae7a1c4374c656ac6a69ea9f3697fda61bb99a4f3e77", + "02e2cc6bd5f45edd43bebe7cb9b675f0ce9ed3efe613b177588290ad188d11b404" + ], + sorted: [ + "02632b12f4ac5b1d1b72b2a3b508c19172de44f6f46bcee50ba33f3f9291e47ed0", + "027735a29bae7780a9755fae7a1c4374c656ac6a69ea9f3697fda61bb99a4f3e77", + "02e2cc6bd5f45edd43bebe7cb9b675f0ce9ed3efe613b177588290ad188d11b404" + ], + script: + "522102632b12f4ac5b1d1b72b2a3b508c19172de44f6f46bcee50ba33f3f9291e47ed021027735a29bae7780a9755fae7a1c4374c656ac6a69ea9f3697fda61bb99a4f3e772102e2cc6bd5f45edd43bebe7cb9b675f0ce9ed3efe613b177588290ad188d11b40453ae", + address: "3CKHTjBKxCARLzwABMu9yD85kvtm7WnMfH" + }, + %{ + pubkeys: [ + "030000000000000000000000000000000000004141414141414141414141414141", + "020000000000000000000000000000000000004141414141414141414141414141", + "020000000000000000000000000000000000004141414141414141414141414140", + "030000000000000000000000000000000000004141414141414141414141414140" + ], + sorted: [ + "020000000000000000000000000000000000004141414141414141414141414140", + "020000000000000000000000000000000000004141414141414141414141414141", + "030000000000000000000000000000000000004141414141414141414141414140", + "030000000000000000000000000000000000004141414141414141414141414141" + ], + script: + "522102000000000000000000000000000000000000414141414141414141414141414021020000000000000000000000000000000000004141414141414141414141414141210300000000000000000000000000000000000041414141414141414141414141402103000000000000000000000000000000000000414141414141414141414141414154ae", + address: "32V85igBri9zcfBRVupVvwK18NFtS37FuD" + }, + %{ + pubkeys: [ + "022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da", + "03e3818b65bcc73a7d64064106a859cc1a5a728c4345ff0b641209fba0d90de6e9", + "021f2f6e1e50cb6a953935c3601284925decd3fd21bc445712576873fb8c6ebc18" + ], + sorted: [ + "021f2f6e1e50cb6a953935c3601284925decd3fd21bc445712576873fb8c6ebc18", + "022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da", + "03e3818b65bcc73a7d64064106a859cc1a5a728c4345ff0b641209fba0d90de6e9" + ], + script: + "5221021f2f6e1e50cb6a953935c3601284925decd3fd21bc445712576873fb8c6ebc1821022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da2103e3818b65bcc73a7d64064106a859cc1a5a728c4345ff0b641209fba0d90de6e953ae", + address: "3Q4sF6tv9wsdqu2NtARzNCpQgwifm2rAba" + } + ] + + describe "test basic functions" do test "test new/0 and empty?/1" do s = Script.new() assert Script.empty?(s) @@ -497,35 +829,14 @@ defmodule Bitcoinex.ScriptTest do assert p2wpkh == p2wpkh2 end - test "test create_p2tr" do - s = Script.new() - script_hex = "51200101010101010101010101010101010101010101010101010101010101010101" - pubkey_hex = "0101010101010101010101010101010101010101010101010101010101010101" - bin = Base.decode16!(pubkey_hex, case: :lower) - - {:ok, p2tr} = Script.push_data(s, bin) - {:ok, p2tr} = Script.push_op(p2tr, 0x51) - assert Script.is_p2tr?(p2tr) - - s_hex = Script.to_hex(p2tr) - assert s_hex == script_hex - - {:ok, p2tr_from_bin} = Script.create_p2tr(bin) - assert Script.to_hex(p2tr_from_bin) == script_hex - - {:ok, pubkey} = Point.lift_x(bin) - {:ok, p2tr_from_pk} = Script.create_p2tr(pubkey) - assert Script.to_hex(p2tr_from_pk) == script_hex - end - - test "test is_multi?" do + test "test is_multisig?" do for ms <- @raw_multisig_scripts do {:ok, multi} = Script.parse_script(ms) - assert Script.is_multi?(multi) + assert Script.is_multisig?(multi) end end - test "test create_multi" do + test "test create_multisig" do # these keys will be added to multisig as compressed keys {:ok, pk1} = Point.parse_public_key( @@ -542,8 +853,8 @@ defmodule Bitcoinex.ScriptTest do "0411ffd36c70776538d079fbae117dc38effafb33304af83ce4894589747aee1ef992f63280567f52f5ba870678b4ab4ff6c8ea600bd217870a8b4f1f09f3a8e83" ) - {:ok, multi} = Script.create_multi(2, [pk1, pk2, pk3]) - assert Script.is_multi?(multi) + {:ok, multisig} = Script.create_multisig(2, [pk1, pk2, pk3]) + assert Script.is_multisig?(multisig) {:ok, pk1} = Point.parse_public_key( @@ -560,24 +871,24 @@ defmodule Bitcoinex.ScriptTest do "02a5264fbc1be9b9a7d03d9637a5534ce8d59a06c4c1f30802fe52e7bf6c1dd971" ) - {:ok, multi} = Script.create_multi(1, [pk1, pk2, pk3]) - assert Script.is_multi?(multi) + {:ok, multi} = Script.create_multisig(1, [pk1, pk2, pk3]) + assert Script.is_multisig?(multi) - {:ok, multi} = Script.create_multi(2, [pk1, pk2, pk3]) - assert Script.is_multi?(multi) + {:ok, multi} = Script.create_multisig(2, [pk1, pk2, pk3]) + assert Script.is_multisig?(multi) - {:ok, multi} = Script.create_multi(3, [pk1, pk2, pk3]) - assert Script.is_multi?(multi) + {:ok, multi} = Script.create_multisig(3, [pk1, pk2, pk3]) + assert Script.is_multisig?(multi) # m cant be greater than n in m-of-n - {:error, _msg} = Script.create_multi(4, [pk1, pk2, pk3]) + {:error, _msg} = Script.create_multisig(4, [pk1, pk2, pk3]) # m cant be 0 - {:error, _msg} = Script.create_multi(0, [pk1, pk2, pk3]) + {:error, _msg} = Script.create_multisig(0, [pk1, pk2, pk3]) # m cant be greater than n in m-of-n - {:error, _msg} = Script.create_multi(0, []) + {:error, _msg} = Script.create_multisig(0, []) end - test "test create_p2sh_multi" do + test "test create_p2sh_multisig" do {:ok, pk1} = Point.parse_public_key( "04a882d414e478039cd5b52a92ffb13dd5e6bd4515497439dffd691a0f12af9575fa349b5694ed3155b136f09e63975a1700c9f4d4df849323dac06cf3bd6458cd" @@ -593,9 +904,9 @@ defmodule Bitcoinex.ScriptTest do "0411ffd36c70776538d079fbae117dc38effafb33304af83ce4894589747aee1ef992f63280567f52f5ba870678b4ab4ff6c8ea600bd217870a8b4f1f09f3a8e83" ) - {:ok, p2sh, multi} = Script.create_p2sh_multi(2, [pk1, pk2, pk3]) + {:ok, p2sh, multi} = Script.create_p2sh_multisig(2, [pk1, pk2, pk3]) assert Script.is_p2sh?(p2sh) - assert Script.is_multi?(multi) + assert Script.is_multisig?(multi) {:ok, pk1} = Point.parse_public_key( @@ -612,19 +923,19 @@ defmodule Bitcoinex.ScriptTest do "02a5264fbc1be9b9a7d03d9637a5534ce8d59a06c4c1f30802fe52e7bf6c1dd971" ) - {:ok, p2sh, multi} = Script.create_p2sh_multi(2, [pk1, pk2, pk3]) + {:ok, p2sh, multi} = Script.create_p2sh_multisig(2, [pk1, pk2, pk3]) assert Script.is_p2sh?(p2sh) - assert Script.is_multi?(multi) + assert Script.is_multisig?(multi) # m cant be greater than n in m-of-n - {:error, _msg} = Script.create_p2sh_multi(4, [pk1, pk2, pk3]) + {:error, _msg} = Script.create_p2sh_multisig(4, [pk1, pk2, pk3]) # m cant be 0 - {:error, _msg} = Script.create_p2sh_multi(0, [pk1, pk2, pk3]) + {:error, _msg} = Script.create_p2sh_multisig(0, [pk1, pk2, pk3]) # m cant be greater than n in m-of-n - {:error, _msg} = Script.create_p2sh_multi(0, []) + {:error, _msg} = Script.create_p2sh_multisig(0, []) end - test "test create_p2wsh_multi" do + test "test create_p2wsh_multisig" do {:ok, pk1} = Point.parse_public_key( "04a882d414e478039cd5b52a92ffb13dd5e6bd4515497439dffd691a0f12af9575fa349b5694ed3155b136f09e63975a1700c9f4d4df849323dac06cf3bd6458cd" @@ -640,9 +951,9 @@ defmodule Bitcoinex.ScriptTest do "0411ffd36c70776538d079fbae117dc38effafb33304af83ce4894589747aee1ef992f63280567f52f5ba870678b4ab4ff6c8ea600bd217870a8b4f1f09f3a8e83" ) - {:ok, p2wsh, multi} = Script.create_p2wsh_multi(2, [pk1, pk2, pk3]) + {:ok, p2wsh, multi} = Script.create_p2wsh_multisig(2, [pk1, pk2, pk3]) assert Script.is_p2wsh?(p2wsh) - assert Script.is_multi?(multi) + assert Script.is_multisig?(multi) {:ok, pk1} = Point.parse_public_key( @@ -659,16 +970,16 @@ defmodule Bitcoinex.ScriptTest do "02a5264fbc1be9b9a7d03d9637a5534ce8d59a06c4c1f30802fe52e7bf6c1dd971" ) - {:ok, p2wsh, multi} = Script.create_p2wsh_multi(2, [pk1, pk2, pk3]) + {:ok, p2wsh, multi} = Script.create_p2wsh_multisig(2, [pk1, pk2, pk3]) assert Script.is_p2wsh?(p2wsh) - assert Script.is_multi?(multi) + assert Script.is_multisig?(multi) # m cant be greater than n in m-of-n - {:error, _msg} = Script.create_p2wsh_multi(4, [pk1, pk2, pk3]) + {:error, _msg} = Script.create_p2wsh_multisig(4, [pk1, pk2, pk3]) # m cant be 0 - {:error, _msg} = Script.create_p2wsh_multi(0, [pk1, pk2, pk3]) + {:error, _msg} = Script.create_p2wsh_multisig(0, [pk1, pk2, pk3]) # m cant be greater than n in m-of-n - {:error, _msg} = Script.create_p2wsh_multi(0, []) + {:error, _msg} = Script.create_p2wsh_multisig(0, []) end test "test create scripts from pubkey" do @@ -1010,7 +1321,7 @@ defmodule Bitcoinex.ScriptTest do test "test serialize/parse multisig scripts" do for ms <- @raw_multisig_scripts do {:ok, multi} = Script.parse_script(ms) - assert Script.is_multi?(multi) + assert Script.is_multisig?(multi) assert Script.to_hex(multi) == ms end end @@ -1154,13 +1465,38 @@ defmodule Bitcoinex.ScriptTest do assert Script.to_address(s, :mainnet) == {:ok, addr} end - test "test raw multisig to address" do + test "test raw unsorted multisig to address" 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, m, pks} = Script.extract_multisig_policy(ms) + + if multi.sh_addr != "" do + {:ok, p2sh, multi_script} = Script.create_p2sh_multisig(m, pks, bip67_sort: false) + + {:ok, addr} = Script.to_address(p2sh, multi.network) + + assert multi_script == ms + assert addr == multi.sh_addr + end + + if multi.wsh_addr != "" do + {:ok, p2wsh, multi_script} = Script.create_p2wsh_multisig(m, pks, bip67_sort: false) + + {:ok, addr} = Script.to_address(p2wsh, multi.network) + + assert multi_script == ms + assert addr == multi.wsh_addr + end + end + end + + test "raw sorted multisig to address" do + for multi <- @raw_sorted_multisig_with_data do + {:ok, ms} = Script.parse_script(multi.script_hex) + {:ok, m, pks} = Script.extract_multisig_policy(ms) if multi.sh_addr != "" do - {:ok, p2sh, multi_script} = Script.create_p2sh_multi(m, pks) + {:ok, p2sh, multi_script} = Script.create_p2sh_multisig(m, pks) {:ok, addr} = Script.to_address(p2sh, multi.network) @@ -1169,7 +1505,7 @@ defmodule Bitcoinex.ScriptTest do end if multi.wsh_addr != "" do - {:ok, p2wsh, multi_script} = Script.create_p2wsh_multi(m, pks) + {:ok, p2wsh, multi_script} = Script.create_p2wsh_multisig(m, pks) {:ok, addr} = Script.to_address(p2wsh, multi.network) @@ -1307,16 +1643,16 @@ defmodule Bitcoinex.ScriptTest 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) + {:ok, m, pks} = Script.extract_multisig_policy(ms) + {:ok, ms2} = Script.create_multisig(m, pks, bip67_sort: false) 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) + {:ok, m, pks} = Script.extract_multisig_policy(ms) + {:ok, ms2} = Script.create_multisig(m, pks, bip67_sort: false) assert ms == ms2 end @@ -1365,4 +1701,43 @@ defmodule Bitcoinex.ScriptTest do assert Bitcoinex.Secp256k1.Point.sec(pubkey3) == pk3 end end + + describe "test bip341 create_p2tr" do + test "test bip341 test vectors" do + for t <- @bip_341_script_pubkey_test_vectors do + {:ok, p} = Point.lift_x(t.given.internal_pubkey) + {:ok, script} = Script.create_p2tr(p, t.given.script_tree) + assert Script.to_hex(script) == t.expected.script_pubkey + assert Script.to_address(script, :mainnet) == {:ok, t.expected.bip350_address} + end + end + end + + describe "bip 67" do + def parse_pk(pk) do + {:ok, pk} = Point.parse_public_key(pk) + pk + end + + test "sorting pubkeys" do + for t <- @bip67_tests do + pubkeys = Enum.map(t.pubkeys, &parse_pk/1) + sorted_pubkeys = Enum.map(t.sorted, &parse_pk/1) + + pubkeys = Script.lexicographical_sort_pubkeys(pubkeys) + + zip_list = Enum.zip(pubkeys, sorted_pubkeys) + Enum.each(zip_list, fn {pubkey, sorted_pubkey} -> assert pubkey == sorted_pubkey end) + end + end + + test "script creation" do + for t <- @bip67_tests do + pubkeys = Enum.map(t.pubkeys, &parse_pk/1) + {:ok, script, redeem_script} = Script.create_p2sh_multisig(@bip67_test_vectors_m, pubkeys) + assert Script.to_hex(redeem_script) == t.script + assert Script.to_address(script, :mainnet) == {:ok, t.address} + end + end + end end diff --git a/test/secp256k1/schnorr_adaptor_test_vectors.csv b/test/secp256k1/schnorr_adaptor_test_vectors.csv new file mode 100644 index 0000000..1d77803 --- /dev/null +++ b/test/secp256k1/schnorr_adaptor_test_vectors.csv @@ -0,0 +1,21 @@ +private_key_hex,public_key_sec,tweak_secret,tweak_point_sec,message_hash,aux_rand,untweaked_adaptor_signature,tweaked_signature,was_negated +"01138d3785033d091a3fdb849d9a1291f5a6d1d24a81bdddf8881b10204e6136","02a13ad24a8a69cb200f25fbb73c2d3f403a0718ca4b02864b4c26b033cacfe2ff","665900ff1ca9e0bd80c12815122c6d928505e2b23e0d2121341fb1d9bf474f3c","03911b512d73c3602d8ae4455bee5cc82d442c00d9754b403f5b3eb83e98ed2ed9","a10616fb1ec1e3e212c30ccc0fbb5f8a3e37c9a821e35aed3e7431776ffd7c8b","eb41b70f96364551f2a8dea5177d8f7b0456dde9b21df232c9cfe1801b5d7646","99e9a06a6e865d6fa64aa4c9464e8215db7031d5a170433fe3b7d15ab5d10098fa3daffb44ed51687508d3e64825f8aaf7a4ae4ad6c57965d0caadc1e3f7b65d","99e9a06a6e865d6fa64aa4c9464e8215db7031d5a170433fe3b7d15ab5d100986096b0fa61973225f5c9fbfb5a52663ec1fbb4166589fa4b4518010ed308c458",false +"20778aa439316b57aa4637c831f974a3b87b2c4e6eb95f073fda0cfa79bbdaa3","02bf1d005c2b11c3bfac8f5d518ad7b4796b6dcc37f418dcc9ba79d22abca18746","86e8589b1adfe4421f98dd88031345a3b3d0c5bc92fd50c4c50df38d64afd274","0255ac067d96a6c45deef413141ae55a7a00ca277b13bee4bac667fced556ead18","dc909e9617b5e440b3e0a2e85a7c82cc2e31e3fc650a6b119e60c65e98f0118f","20eee2ddffddd43f02d884b1844ecc6e387cc25300d0f835ff4973b2126a514b","22e48a3678437d52795bd83000ff0a2510db0cd0de65d3c8b25b222b0632bebbd6d19cef77faa6e6c60412b3782f8092745486b49476472fc1410da6c3510f00","22e48a3678437d52795bd83000ff0a2510db0cd0de65d3c8b25b222b0632bebb4fe944545d1ac2a4a66b352b751c3aeec083c0f80178f66afc331a195ea13c8c",true +"7a34b0f93dbbd0c91b588a16fe4bd5d23f1903671f6de4403213cc3ce214ce38","027fb795e7e29f605bca05761496ef55de514d1d4cb67b1bee84597ded249ab024","6828ca09e3098a5a71b3be510794c1159c0eff6db384a830575c62a0006eaae0","033dd2db0ac9870447c63d4003eb183b0545fb86ccf929111666138c31bae62a28","be4a83480af33dd41908f009b2d9d4cb218d230acb7c1d48c40a97985e2bb2a4","4a1d8c39d92fa7405efe687837683cbc8e20f01621132161324e78d8b6c469f5","1021d59e8395ed425dc40625f2fea249549a86621a2edce57a114bbf8a1127622cea4b231d718eb55b2a253ce9dc23a1e76824d74ec8174951fed0fec49de82a","1021d59e8395ed425dc40625f2fea249549a86621a2edce57a114bbf8a112762c4c181193a68045ae97666ebe247628b060802504a8c0f54ba74cceb94657e8b",true +"547abc10d9ad870d554edc78028d21893f915d718b5d7849fa127a37213cddb7","0217f7e8f23c85208c10e06d55e90abafcd0bdbda49098a63a11cba7c1b16cc4b0","65c8f90e572f1828c167377f64f40796ee8aec5ce2baee05c762cf812f33883b","0269aa06097d53b56a77e39b46cde9b337084fb70a1f8c610518bb3e9c6b228445","a4c968687391d781c22729a941b6564df3c94a8eb9433f6b73f0eebba0e94318","d9fa09c670a30b031a1e0e3f72743506044f6b39a89e73c0bf87d6accd5b94ad","593acb43862897e5af5f5429e9e9142d09c75378ec70128f700c869cb6bacbfa0d527137644c31169cace2948c580d7a96347a6637b61f95ffcc6ec513b88d03","593acb43862897e5af5f5429e9e9142d09c75378ec70128f700c869cb6bacbfa731b6a45bb7b493f5e141a13f14c151184bf66c31a710d9bc72f3e4642ec153e",false +"593cc37fd28d28005adbedfcdecd18a071330e7ad7d853d738c77c61bbb83ff9","02c923dae361f1bc224a246a0592000c051b03b821ab26f5edbf69db6256d8a665","467b336868572cef5496b4f011ebb73839c7cf0c7763f1161eb1bfbdeec40d26","03d31be8ac7b423907dfe5c9834d3900247ac43b7dcadc8465ebc54b128c8910f5","81f3576f1a142901d056da8588b164a8464efa4f41cbceddccdc6f235ce49f59","86d81f4359857bbf39fbb128103d3bad5f46da4a52eea554f30d5954cbde7cb4","c435b682a7763d516da6e6483668e56fcef6ded57ccfd90ef225270d47cdc3f718482a85147e73108256664fd3eea00f5fd74042d8ceb331b678d631500b2712","c435b682a7763d516da6e6483668e56fcef6ded57ccfd90ef225270d47cdc3f7d1ccf71cac2746212dbfb15fc202e8d5e0be4e1d10b3625757997500317d5b2d",true +"2eba8f149e45f2af5c2afe5f51990d204cf240d22fc9e2078c48d480cf5ca6bf","02cd768be3e2d46d7734ebb40c87c08177b16abc092a6ae89d0aae0ff965dff8eb","ba297393b1c9aa874ba18f0351b56d52ead9a2d0a740be41779e9fe9cac59734","02b86184c2c2ab98441bb98c63bf0d98d38b8aa7e25ade0dcc116f7f6ff6c0a4db","47966fb320c357639f981bc883cbfc84d23153f09d88aa671a21a6199690ba8f","40fb47e380da85678b0ddd36c99c049f31c912ea83efbca9bc069a45151aeb4f","34bddf1a2bcc8893189cc3f719393b83d8077dbe85a8ce2bb02c4847815260550f31a7e7d3ef1661db2d70d2e408948fb21739ba17fe6a8f970559a0b4ba6bf0","34bddf1a2bcc8893189cc3f719393b83d8077dbe85a8ce2bb02c484781526055c95b1b7b85b8c0e926ceffd635be01e29cf0dc8abf3f28d10ea3f98a7f800324",false +"11ca0283f12577e4f37d0addd595c16a24ecba5ad9e6c2c6ea484525507ee953","028aad61e58c99b051ba6f95eb920e293d0f6d6fe4b9b1d55dff16b2ec368c587e","c6b5be059c076f99855f2854ac597e3d5a43310c6442592e1e5d1a8dbfaef62d","0317409274a1cf439bb11c3c0a01a7a7fbcec7ecefbee1330dc7fba93c530bc4ea","6fa2ad2be58417feca91d9c7d79b1077becd0c60a002be0e3fefaf86dc63a4cf","909676b82df8909a1a86b0a470f1fd5865a1fbc615381ead571b0b25c0b7da70","91d927a751000f0c7c72a76bf0ad15a315b131205ca964e03f8aec6925eadc8d07d3f00cad97c89b46a42fd84d47669ded22c11bd73e1ecc78fce1e2d77817bf","91d927a751000f0c7c72a76bf0ad15a315b131205ca964e03f8aec6925eadc8dce89ae12499f3834cc03582cf9a0e4db4765f2283b8077fa9759fc7097270dec",false +"29e17ae4aa2b13df44c3131c6c237c29be522e2a6eeb711a8d1f24eb137aaf0d","02edcdb8d9f4eaf8f2c9be1314886d56b1ff9f7a1892f86df38c366533356b5452","32f2f3121b5d265815c0527bea181b23e87e0d594e3af6d263eda72e7c72f645","032eba5528c77755be8ceb944dd1e886ae747c6e1390b90235941dc6e18ba7c9de","5be83e6ada36f33de34535159ab5c4fde57a60826eae6a20547a8a8203cdae8f","540c1ef21bd5a46c0a6e31a1922717aeda414903c07ed68060f17df40e7d29a0","ce472e6c3d7e75ec2683a295344c12890c2f99ddfab53a292ed1817e10f9f1069c2963013648549d786aeeca133da9ed186ff51e43a05eaa193f98743a955657","ce472e6c3d7e75ec2683a295344c12890c2f99ddfab53a292ed1817e10f9f10669366fef1aeb2e4562aa9c4e29258ec92ff1e7c4f56567d7b551f145be226012",true +"3504bee0a1d65cdbed68e8decfe9075dca5db9dc97b6aeff701ff6dac421c5c1","02c016e5f5b76175063f6336480320cd7dc23076a6d700ae34cfdb48268dac65b9","a2113b65d633a93e4196c3d68dadc7e227f8dd9d06e4d2bc5b5a3ac55ee567f5","02abbee001326b50704b9b17efcf98702dc74600749d0730328d2d5f04ccd2cde1","fdd6b40c8bb467cfc316d310ccdd0ef9ec6efd89932bccdfb8d1381e96ff5480","fbb700a915376fda2a157861af255025b510694919cba7fb5fd7a7f0744b65c1","e2b92af7dc9e15c4db7eae369fa8ae47283723930ccdd5b61848864cf7c9fffbc83aa93cdce4a1f0027b29fd82c5293c59bfa277c08ad4c48e4a243557e06265","e2b92af7dc9e15c4db7eae369fa8ae47283723930ccdd5b61848864cf7c9fffb26296dd706b0f8b1c0e46626f517615a31c6c4dab9a6020832efe96ff8fafa70",true +"84ca840ee3800300d023a01a439fbe62051efa7711781d5e17dccf24f8aba61c","026688fd24a8d11df5e166b14edab4083718859bd97928a70e05d8673ade2e8c16","fbfe17a13ae16e0adf77cf96e9af145d2fc02580ce19cf98023a3b5f9ad04f85","0221e822b2b559396c6eabdc9a5d2cff7c27861287c4ee0579a9ea435fa6b0b8d6","35cc81ff675a06d4042035116930fd52dc0a8512066e65fa454ac1f05eca8dc8","fc98beeab724efccb4ea955d51ce5cc300fad15e2811a7917fed46dc04639285","b42581ccd0779f6371209ff597344213e8e48851b6c95e80c51a75736f88d0ac078c830c97fd15e9d42ac9dc4038477df6dc171a862061bed25012d1541e4759","b42581ccd0779f6371209ff597344213e8e48851b6c95e80c51a75736f88d0ac0b8e6b6b5d1ba7def4b2fa455689331f81cace80674f32628fe835fe89843915",true +"79a7b3f13d359db306ea1778dd4c7b891793e5e57025b35e8ed7895d570b86ea","02ea1a0c4081c7a9f86352e00b135e6fd2bd0ad17c50fd22a9517472e62af05380","be025a17089052495761ef09365783b24ccf6844bf476ce54f445d0416f512b2","02987da0d941a03f75cdfe7582b90e58a80d0c80bf3073bdc42f8dc65b94da05e0","8f62d16c18211ee53a15f200c24a7e4020a04843f62c27e3ae263c1f795b5e61","a32a6d8dbd4976e31474a405592bf6280b799597e6e946203a4ac12062a29f06","010ac477501ecb0ef5b4bd8b81d1ab09de7fd1c790153e1af59101f53492a07fcdd686259bc74c3828a07a2149b8d2f3601c67133bac4672912021cff27bf547","010ac477501ecb0ef5b4bd8b81d1ab09de7fd1c790153e1af59101f53492a07f0fd42c0e9336f9eed13e8b1813614f41134cfece7c64d98d41dbc4cbdb86e295",true +"b12d7d24c6f0b0afd29b43cef391e02b5182356366344f60c12f361c44eb0c1b","02f2c0830b948cfc2b7ac9aa7c6390955e0d6e20fd67930e5f6a87b8904ee57ae0","1a41e58047f2ecb0a60028d491355b2c2dca60b9894d624332f2c707220117e4","027b6ee8e1bd3272edcc9ba857f0a1d677a455e8cb18258866508a591d9f4fda39","d48ae86c6552cd7c5d536cd4bd28520ff89fc328816fe2d4ccbdedbfbbed10d4","7e6aba5e4f518dcd4d8e1ba1289e3d5b2fef7c5e7b4b94b0b4afe38557f07bd2","19d508f771a4db7e5a1ccce0d5565686bf9c9ff506650c48c6c057c7e725042fbe9c7badacfdca549b4c484e712567233ea4a17021ceba8ac9e804d753baf13a","19d508f771a4db7e5a1ccce0d5565686bf9c9ff506650c48c6c057c7e725042fd8de612df4f0b705414c7123025ac24f6c6f0229ab1c1ccdfcdacbde75bc091e",false +"d671ee3655eb17d4512d896d851953ec8bbf5b8cc7bacd005e5d3b6442d264b3","0269aafad3bb6e1d2694a2570d7e1513e0154b1f1c7ad2ae9fa7d926bbefae213a","5152217b5100c40ebf949d863f101ad72e1d51e24be6b7edc222c26933b0e20c","02acdd14736689da6d7cd8f200ab26759ebf9c65ef1207d17488c3b3b82fecf52c","28c2b4a6f7c5e7499beb923caf7d92646c0b11af4af6873f0d52c9c10e8150c4","a5fcda2417c758bac50a05e2e29cea8ec69a166e70496ad6cdddcef0c23bc5a2","ada74b4cf2f6f67c91c8f3011a612cb44f3b3760bb139d9d80fa5cd7657d53bfb7877e86424e3faa3f123162cd0b4479027be74d40a9569de0da29402e22e0da","ada74b4cf2f6f67c91c8f3011a612cb44f3b3760bb139d9d80fa5cd7657d53bf08d9a001934f03b8fea6cee90c1b5f5175ea5c48dd476e4fe32a8d1c919d81a5",false +"29d8887c2b4dc6ad8b4e96dacd1798ad7bc1f67e458386e2577c13fce3faf282","02842cb699537d1a0d46c5728a7341890c767b17b38cd49c758874dbd65ec698c3","332bc36dfc79a7369eaf37bf1637db0b740cdd1079e212d1480178f20a3d0176","02cea3bc0b5ec890822f52e8eba87690d1d92012f86622229fb5537db321a371ed","8bc125e47d1b0593b721656d01dd4f1f68ddfc3b1a2758197569404d0bd0836c","00f1be297d9ee2b7559d61b8dc8e9a781366bec75e126acc53b8bd0e3c84755e","c4dc81aae55b1e645bc1d45f5b30e18079f36d27449268919ccf889f89b8844a28f4c4d83ca844df41f97544dc1e8ce70db632b388a96a2708cd0994be7882f0","c4dc81aae55b1e645bc1d45f5b30e18079f36d27449268919ccf889f89b8844a5c2088463921ec15e0a8ad03f25667f281c30fc4028b7cf850ce8286c8b58466",false +"feda1cf986c989c081e1c6dc8b434715ee2b00f28a0823577a56c8a64e979736","02eaba7f222004910e7be62ed1c8a78e9dca7e6b31fdd5c5e4d0751d239bd63a20","06ab57ab98caad717e6a53baf5d1202ed25b258dec3a96a32200ac9eb78d4fa6","023238fe1444f7038dbc56fdf20246ff3c133a1053caa9aea700a63fe8eea172d9","79d4fb0e147bdb23b4b82d73fcb937d63bd61d94e6ad469c77a0678a3e5c15ed","66a8ee476af678dc112ff3aef4388e3f26b68db2cbe7c545d40baf1d8062447b","1e03ce47176eca24579c88907f17acdedea160054115f59a744822b1a06d68fb87219dacf6d9e6afe333a79c40892b8b1ba7c7151711d655f02110ab3bef2a4b","1e03ce47176eca24579c88907f17acdedea160054115f59a744822b1a06d68fb8dccf5588fa49421619dfb57365a4bb9ee02eca3034c6cf91221bd49f37c79f1",false +"723ec1fac7ab8087de8f3d6cbcd7fce6ad497a32683406031efbbb534af5e4f0","02ad3e90516e2aa3a4d848ca71848f8bef22b6449ca8ff719346717c3d08c022a0","7454898bbb9418ad0287f9861b0b9245d4ba592fa2d263eae113ca5afda3a6ac","02eb538ee72c2672479361070b692f2bd05536e4f535431fed7167262d05ee6393","12432a48bd8f9d319c2673fdd7e16ddf60d2300b984da1f24fa53f1a0149f4b0","1174a245a991e949616646358d4bd79f561289343a9929a1adbf245c8efdba51","fbb9d7d05da1d35097619164867b20267a69171b8e7d2c47ea11a7076abbd2cffb74559a030c3d7c804af55698f18f1bb30dc4b707b2dae19033cb57b2c3ec4d","fbb9d7d05da1d35097619164867b20267a69171b8e7d2c47ea11a7076abbd2cf6fc8df25bea0562982d2eedcb3fd2162cd1940fffb3c9e90b1753725e03151b8",false +"ce1ed0fc2587a09dbbb3edbeb1e7d2260e694e47219ff0b5803e3633fb6e3b8b","0297fce00d0ae1a7ca4a21a248bbb56d02236b4d9071589dd83a7fda160cab6a17","0df5e4cd6a732400b909810cb2423c81231b42c1b8de953aa0abc93acccf79ef","02a08986f8334a064dd81f79e0d46a451395f901401e0dc9c3dfbe21fd6fd803fa","31432c7343d699c883557306ff017beae7ce991c525edacee0fae6cd77693f27","55bd3426ff7f94c16a439d1d7a3ae8975e2eb660ddf8695c1b1cfc0bcc711b8e","41d93b7c672c3e43dd2c8f220acb5ac7dc05a787f8eb51ba74a75272b68bf3fe5b5d97b2d663ad2c36466345d7508bc815034f9f98accdaedff225301f038efe","41d93b7c672c3e43dd2c8f220acb5ac7dc05a787f8eb51ba74a75272b68bf3fe69537c8040d6d12cef4fe4528992c849381e9261518b62e9809dee6aebd308ed",false +"a99059144b0309a1ae1c742225b10fdbbf3cdd4e89536f91c227d58175220dd4","021c840d44397a1598ffc1b967acadab2a49308ce4b2749725a3f0af7887e63f82","82e0a36bfc1d3c1fa16272abbde846f6573c7b50c1f1d1dbc45bd6d3f9237da8","03e1bc5c665861bc71da5e12509b96bcbae9f8f3faad490155fc55e00fa5313712","fd8d2ec5dcacef0072f4666268d0877c5a46f5863b4f82a579b4ccfda67c4ced","6a4c6660d79d46131e1605a25e9b8b17a8d3646e53b4d42bc49fafd26e1bef3a","662d14456a5ab31a1012cecc068b141ca8d41757e73ec703aacc955bb54e4e433560e1951c0277557426a7310dde160b8c01bc083f424e7631b844dedd38280d","662d14456a5ab31a1012cecc068b141ca8d41757e73ec703aacc955bb54e4e43b2803e291fe53b35d2c434854ff5cf13ef741d9e2c991cd62d2ecc97b44aeba6",true +"e761546d8eef5cd892957115b0e896fb066d28b14179dff8bef75735ed968011","02a09f5a0da4c7c66d5d83b6427e64100762c342ebfd8825eb8050bc6db0ffc333","a739f4b46219bd5b6f2b8d0c945ec0beee7591d2dcdc32b44853d60a6cc19476","026167160fd64b079f5b47735c2c5e27817a8cb877e3c03c940b9808b0d87ccaf7","4ff6144e72807179659e93c8b786042a5fd2bfb08785b293eae5833cf5b490ed","5d815476dca33ef58022bae294b784d0757c80ed49dc20fc86bf7375f345c30d","6e2c6e1ebb9e22e6592bb2f0ca126b09f167d1c0c29d6f8296fe65db07a5b69ea6cdc7f44c2f71bc54be5b8051a6ced0f36c0db67169485500c67e45bd6c19f5","6e2c6e1ebb9e22e6592bb2f0ca126b09f167d1c0c29d6f8296fe65db07a5b69e4e07bca8ae492f17c3e9e88ce6058f912732c2a29efcdacd8947f5c359f76d2a",false +"1fe1a3cd19735058b4517c13f7d225a0dd0b7176e8b970f72337dfbe92ea1394","026c605773a227b87c6bffad7adad8b8699a2b3b84d61a874e12bc7ddaf4051daa","c013f96c065555a9185ed60e3a37edaa9b37c83a9a87ea164075822c2e962414","0362f76db1fb4049445b220cde65785502e22aff575baa8444bdff70320a7137a6","77e0910f8dd5b3a54c54b11106323848e26dcd142209f16206735665ae740651","0430912f3219a070d6f14d8ed4bea2c961ef002efe1815cd1fec456562fec2af","2cf0452d6c63c38f4396099c1fe24843757c5acc83f9ed105e93f2ae11b65d217dc9110ff8c01b5a5b9b5e6259836902baaa6a7bcfb9dd7571680cb5b9bfb8e2","2cf0452d6c63c38f4396099c1fe24843757c5acc83f9ed105e93f2ae11b65d21bdb517a3f26ac5b1433c88541f4b7b56da217f27e47a939af0c4e9165b5fd60f",true diff --git a/test/secp256k1/schnorr_test.exs b/test/secp256k1/schnorr_test.exs index d5aa295..fe4a8aa 100644 --- a/test/secp256k1/schnorr_test.exs +++ b/test/secp256k1/schnorr_test.exs @@ -4,7 +4,10 @@ defmodule Bitcoinex.Secp256k1.SchnorrTest do alias Bitcoinex.Utils alias Bitcoinex.Secp256k1 - alias Bitcoinex.Secp256k1.{Point, PrivateKey, Schnorr, Signature} + alias Bitcoinex.Secp256k1.{Params, Point, PrivateKey, Schnorr, Signature} + # alias Bitcoinex.Secp256k1.{PrivateKey} + + @n Params.curve().n # BIP340 official test vectors: # https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv @@ -153,6 +156,25 @@ defmodule Bitcoinex.Secp256k1.SchnorrTest do } ] + def get_rand_values_for_encrypted_sig() do + sk_int = :rand.uniform(@n - 1) + {:ok, sk} = PrivateKey.new(sk_int) + sk = Secp256k1.force_even_y(sk) + pk = PrivateKey.to_point(sk) + + # tweak + tweak_int = :rand.uniform(@n - 1) + {:ok, tweak} = PrivateKey.new(tweak_int) + tweak_point = PrivateKey.to_point(tweak) + + msg = :rand.uniform(@n - 1) |> :binary.encode_unsigned() + z = Utils.double_sha256(msg) |> :binary.decode_unsigned() + + aux = :rand.uniform(@n - 1) + + {sk, pk, tweak, tweak_point, z, aux} + end + describe "sign/3" do test "sign" do for t <- @schnorr_signatures_with_secrets do @@ -260,7 +282,7 @@ defmodule Bitcoinex.Secp256k1.SchnorrTest do for _ <- 1..1000 do secret = - 32 + 31 |> :crypto.strong_rand_bytes() |> :binary.decode_unsigned() @@ -271,4 +293,49 @@ defmodule Bitcoinex.Secp256k1.SchnorrTest do end end end + + describe "encrypted signature testing" do + test "encrypted_sign/4 and verify_encrypted_signature/5" do + for _ <- 1..1000 do + {sk, pk, _tweak, tweak_point, z, aux} = get_rand_values_for_encrypted_sig() + + # create adaptor sig + {:ok, ut_sig, was_negated} = Schnorr.encrypted_sign(sk, z, aux, tweak_point) + assert Schnorr.verify_encrypted_signature(ut_sig, pk, z, tweak_point, was_negated) + end + end + + test "encrypt & decrypt signature" do + for _ <- 1..1000 do + {sk, pk, tweak, tweak_point, z, aux} = get_rand_values_for_encrypted_sig() + + # create adaptor sig + {:ok, ut_sig, was_negated} = Schnorr.encrypted_sign(sk, z, aux, tweak_point) + assert Schnorr.verify_encrypted_signature(ut_sig, pk, z, tweak_point, was_negated) + + # decrypt to real Schnorr Signature using tweak + sig = Schnorr.decrypt_signature(ut_sig, tweak, was_negated) + # ensure valid Schnorr signature + assert Schnorr.verify_signature(pk, z, sig) + end + end + + test "encrypt & recover descryption key" do + for _ <- 1..1000 do + {sk, pk, tweak, tweak_point, z, aux} = get_rand_values_for_encrypted_sig() + + # create adaptor sig + {:ok, ut_sig, was_negated} = Schnorr.encrypted_sign(sk, z, aux, tweak_point) + assert Schnorr.verify_encrypted_signature(ut_sig, pk, z, tweak_point, was_negated) + + # decrypt to real Schnorr Signature using tweak + sig = Schnorr.decrypt_signature(ut_sig, tweak, was_negated) + # ensure valid Schnorr signature + assert Schnorr.verify_signature(pk, z, sig) + + recovered_tweak = Schnorr.recover_decryption_key(ut_sig, sig, was_negated) + assert recovered_tweak == tweak + end + end + end end diff --git a/test/secp256k1/secp256k1_test.exs b/test/secp256k1/secp256k1_test.exs index a723d01..4a6c4f5 100644 --- a/test/secp256k1/secp256k1_test.exs +++ b/test/secp256k1/secp256k1_test.exs @@ -3,7 +3,7 @@ defmodule Bitcoinex.Secp256k1.Secp256k1Test do doctest Bitcoinex.Secp256k1 alias Bitcoinex.Secp256k1 - alias Bitcoinex.Secp256k1.{Signature} + alias Bitcoinex.Secp256k1.{Signature, PrivateKey} @valid_der_signatures [ %{ @@ -165,4 +165,19 @@ defmodule Bitcoinex.Secp256k1.Secp256k1Test do end end end + + describe "force_even_y/1" do + test "force_even_y" do + for _ <- 1..1000 do + secret = + 31 + |> :crypto.strong_rand_bytes() + |> :binary.decode_unsigned() + + privkey = Secp256k1.force_even_y(%PrivateKey{d: secret}) + pubkey = PrivateKey.to_point(privkey) + assert rem(pubkey.y, 2) == 0 + end + end + end end diff --git a/test/segwit_test.exs b/test/segwit_test.exs index 2d4bcc7..4b1832c 100644 --- a/test/segwit_test.exs +++ b/test/segwit_test.exs @@ -108,19 +108,19 @@ defmodule Bitcoinex.SegwitTest do end end - describe "is_valid_segswit_address?/1" do + describe "is_valid_segwit_address?/1" do test "return true given valid address" do for {address, _hexscript} <- @valid_segwit_address_hexscript_pairs_mainnet ++ @valid_segwit_address_hexscript_pairs_testnet ++ @valid_segwit_address_hexscript_pairs_regtest do - assert Segwit.is_valid_segswit_address?(address) + assert Segwit.is_valid_segwit_address?(address) end end test "return false given invalid address" do for address <- @invalid_segwit_addresses do - refute Segwit.is_valid_segswit_address?(address) + refute Segwit.is_valid_segwit_address?(address) end end end diff --git a/test/taproot_test.exs b/test/taproot_test.exs new file mode 100644 index 0000000..11640ef --- /dev/null +++ b/test/taproot_test.exs @@ -0,0 +1,288 @@ +defmodule TaprootTest do + use ExUnit.Case + doctest Bitcoinex.Script + + alias Bitcoinex.Taproot + alias Bitcoinex.Secp256k1.Point + + @bip_341_script_pubkey_test_vectors [ + %{ + given: %{ + internal_pubkey: "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d", + script_tree: nil + }, + intermediary: %{ + merkle_root: <<>>, + tweak: "b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70", + tweaked_pubkey: "53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343" + }, + expected: %{ + script_pubkey: "512053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + bip350_address: "bc1p2wsldez5mud2yam29q22wgfh9439spgduvct83k3pm50fcxa5dps59h4z5" + } + }, + %{ + given: %{ + internal_pubkey: "187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27", + script_tree: + Taproot.TapLeaf.from_string( + # id: 0, + # version + 192, + # script + "20d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8ac" + ) + }, + intermediary: %{ + leaf_hashes: [ + "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21" + ], + merkle_root: "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21", + tweak: "cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001", + tweaked_pubkey: "147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3" + }, + expected: %{ + script_pubkey: "5120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + bip350_address: "bc1pz37fc4cn9ah8anwm4xqqhvxygjf9rjf2resrw8h8w4tmvcs0863sa2e586", + script_path_control_blocks: [ + "c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27" + ] + } + }, + %{ + given: %{ + internal_pubkey: "93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820", + script_tree: + Taproot.TapLeaf.from_string( + # id: 0, + # version + 192, + # script + "20b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007ac" + ) + }, + intermediary: %{ + leaf_hashes: [ + "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b" + ], + merkle_root: "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b", + tweak: "6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30", + tweaked_pubkey: "e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e" + }, + expected: %{ + script_pubkey: "5120e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + bip350_address: "bc1punvppl2stp38f7kwv2u2spltjuvuaayuqsthe34hd2dyy5w4g58qqfuag5", + script_path_control_blocks: [ + "c093478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820" + ] + } + }, + %{ + given: %{ + internal_pubkey: "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + script_tree: { + Taproot.TapLeaf.from_string( + # id: 0, + # version + 192, + # script + "20387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48ac" + ), + Taproot.TapLeaf.from_string( + # id: 1, + # version + 250, + # script + "06424950333431" + ) + } + }, + intermediary: %{ + leaf_hashes: [ + "8ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7", + "f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a" + ], + merkle_root: "6c2dc106ab816b73f9d07e3cd1ef2c8c1256f519748e0813e4edd2405d277bef", + tweak: "9e0517edc8259bb3359255400b23ca9507f2a91cd1e4250ba068b4eafceba4a9", + tweaked_pubkey: "712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5" + }, + expected: %{ + script_pubkey: "5120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5", + bip350_address: "bc1pwyjywgrd0ffr3tx8laflh6228dj98xkjj8rum0zfpd6h0e930h6saqxrrm", + script_path_control_blocks: [ + "c0ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a", + "faee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf37865928ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7" + ] + } + }, + %{ + given: %{ + internal_pubkey: "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + script_tree: { + Taproot.TapLeaf.from_string( + # id: 0, + # version + 192, + # script + "2044b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fdac" + ), + Taproot.TapLeaf.from_string( + # id: 1, + # version + 192, + # script + "07546170726f6f74" + ) + } + }, + intermediary: %{ + leaf_hashes: [ + "64512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89", + "2cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb" + ], + merkle_root: "ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc", + tweak: "639f0281b7ac49e742cd25b7f188657626da1ad169209078e2761cefd91fd65e", + tweaked_pubkey: "77e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220" + }, + expected: %{ + script_pubkey: "512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220", + bip350_address: "bc1pwl3s54fzmk0cjnpl3w9af39je7pv5ldg504x5guk2hpecpg2kgsqaqstjq", + script_path_control_blocks: [ + "c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd82cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb", + "c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd864512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89" + ] + } + }, + %{ + given: %{ + internal_pubkey: "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + script_tree: { + Taproot.TapLeaf.from_string( + # id: 0, + 192, + # script + "2072ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69ac" + ), + { + Taproot.TapLeaf.from_string( + # id: 1, + 192, + # script + "202352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8ac" + ), + Taproot.TapLeaf.from_string( + # id: 2, + 192, + # script + "207337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186aac" + ) + } + } + }, + intermediary: %{ + leaf_hashes: [ + "2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", + "ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c", + "9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf6" + ], + merkle_root: "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + tweak: "b57bfa183d28eeb6ad688ddaabb265b4a41fbf68e5fed2c72c74de70d5a786f4", + tweaked_pubkey: "91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605" + }, + expected: %{ + script_pubkey: "512091b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + bip350_address: "bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e", + script_path_control_blocks: [ + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fffe578e9ea769027e4f5a3de40732f75a88a6353a09d767ddeb66accef85e553", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" + ] + } + }, + %{ + given: %{ + internal_pubkey: "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + script_tree: { + Taproot.TapLeaf.from_string( + # id: 0, + # version + 192, + # script + "2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac" + ), + { + Taproot.TapLeaf.from_string( + # id: 1, + # version + 192, + # script + "20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac" + ), + Taproot.TapLeaf.from_string( + # id: 2, + # version + 192, + # script + "20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac" + ) + } + } + }, + intermediary: %{ + leaf_hashes: [ + "f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711", + "d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7" + ], + merkle_root: "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + tweak: "6579138e7976dc13b6a92f7bfd5a2fc7684f5ea42419d43368301470f3b74ed9", + tweaked_pubkey: "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831" + }, + expected: %{ + script_pubkey: "512075169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + bip350_address: "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", + script_path_control_blocks: [ + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d3cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312dd7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + ] + } + } + ] + + describe "taproot script_tree" do + test "calculate_taptweak/2 & tweak_pubkey" do + for t <- @bip_341_script_pubkey_test_vectors do + {:ok, pk} = Point.lift_x(t.given.internal_pubkey) + {_, hash} = Taproot.merkelize_script_tree(t.given.script_tree) + tweak = Taproot.calculate_taptweak(pk, hash) + tweak_hex = tweak |> :binary.encode_unsigned() |> Base.encode16(case: :lower) + assert tweak_hex == t.intermediary.tweak + + tweaked_pubkey = Taproot.tweak_pubkey(pk, hash) + tweaked_pubkey_hex = tweaked_pubkey |> Point.x_bytes() |> Base.encode16(case: :lower) + assert tweaked_pubkey_hex == t.intermediary.tweaked_pubkey + end + end + + test "merkelize_script_tree/1" do + for t <- @bip_341_script_pubkey_test_vectors do + {_, hash} = Taproot.merkelize_script_tree(t.given.script_tree) + assert Base.encode16(hash, case: :lower) == t.intermediary.merkle_root + end + end + + test "build_control_block/3" do + for t <- @bip_341_script_pubkey_test_vectors do + {:ok, pk} = Point.lift_x(t.given.internal_pubkey) + + unless t.given.script_tree == nil do + for {c_control_block, idx} <- Enum.with_index(t.expected.script_path_control_blocks) do + control_block = Taproot.build_control_block(pk, t.given.script_tree, idx) + assert Base.encode16(control_block, case: :lower) == c_control_block + end + end + end + end + end +end diff --git a/test/transaction_test.exs b/test/transaction_test.exs index e4a7e7c..6c85010 100644 --- a/test/transaction_test.exs +++ b/test/transaction_test.exs @@ -3,6 +3,10 @@ defmodule Bitcoinex.TransactionTest do doctest Bitcoinex.Transaction alias Bitcoinex.Transaction + alias Bitcoinex.Utils + alias Bitcoinex.Script + alias Bitcoinex.Taproot + alias Bitcoinex.Secp256k1.{PrivateKey, Schnorr, Signature} @txn_serialization_1 %{ tx_hex: @@ -29,6 +33,417 @@ defmodule Bitcoinex.TransactionTest do "0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000" } + # https://github.com/bitcoin/bips/blob/master/bip-0341/wallet-test-vectors.jsonå + @bip341_test_vector %{ + given: %{ + unsigned_tx: + "02000000097de20cbff686da83a54981d2b9bab3586f4ca7e48f57f5b55963115f3b334e9c010000000000000000d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd990000000000fffffffff8e1f583384333689228c5d28eac13366be082dc57441760d957275419a418420000000000fffffffff0689180aa63b30cb162a73c6d2a38b7eeda2a83ece74310fda0843ad604853b0100000000feffffffaa5202bdf6d8ccd2ee0f0202afbbb7461d9264a25e5bfd3c5a52ee1239e0ba6c0000000000feffffff956149bdc66faa968eb2be2d2faa29718acbfe3941215893a2a3446d32acd050000000000000000000e664b9773b88c09c32cb70a2a3e4da0ced63b7ba3b22f848531bbb1d5d5f4c94010000000000000000e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf0000000000ffffffffa778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af10100000000ffffffff0200ca9a3b000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac807840cb0000000020ac9a87f5594be208f8532db38cff670c450ed2fea8fcdefcc9a663f78bab962b0065cd1d", + inputs: [ + %{ + prev_scriptpubkey: + "512053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + amount_sats: 420_000_000 + }, + %{ + prev_scriptpubkey: + "5120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + amount_sats: 462_000_000 + }, + %{ + prev_scriptpubkey: "76a914751e76e8199196d454941c45d1b3a323f1433bd688ac", + amount_sats: 294_000_000 + }, + %{ + prev_scriptpubkey: + "5120e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + amount_sats: 504_000_000 + }, + %{ + prev_scriptpubkey: + "512091b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + amount_sats: 630_000_000 + }, + %{ + prev_scriptpubkey: "00147dd65592d0ab2fe0d0257d571abf032cd9db93dc", + amount_sats: 378_000_000 + }, + %{ + prev_scriptpubkey: + "512075169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + amount_sats: 672_000_000 + }, + %{ + prev_scriptpubkey: + "5120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5", + amount_sats: 546_000_000 + }, + %{ + prev_scriptpubkey: + "512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220", + amount_sats: 588_000_000 + } + ] + }, + intermediary: %{ + hash_amounts: "58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde6", + hash_outputs: "a2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc5", + hash_prevouts: "e3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f", + hash_script_pubkeys: "23ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e21", + hash_sequences: "18959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e" + }, + input_spending: [ + %{ + given: %{ + txin_index: 0, + internal_privkey: "6b973d88838f27366ed61c9ad6367663045cb456e28335c109e30717ae0c6baa", + merkle_root: nil, + hash_type: 3 + }, + intermediary: %{ + internal_pubkey: "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d", + tweak: "b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70", + tweaked_privkey: "2405b971772ad26915c8dcdf10f238753a9b837e5f8e6a86fd7c0cce5b7296d9", + sigmsg: + "0003020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e0000000000d0418f0e9a36245b9a50ec87f8bf5be5bcae434337b87139c3a5b1f56e33cba0", + precomputed_used: [ + "hash_amounts", + "hash_prevouts", + "hash_script_pubkeys", + "hash_sequences" + ], + sig_hash: "2514a6272f85cfa0f45eb907fcb0d121b808ed37c6ea160a5a9046ed5526d555" + }, + expected: %{ + witness: [ + "ed7c1647cb97379e76892be0cacff57ec4a7102aa24296ca39af7541246d8ff14d38958d4cc1e2e478e4d4a764bbfd835b16d4e314b72937b29833060b87276c03" + ] + } + }, + %{ + given: %{ + txin_index: 1, + internal_privkey: "1e4da49f6aaf4e5cd175fe08a32bb5cb4863d963921255f33d3bc31e1343907f", + merkle_root: "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21", + hash_type: 131 + }, + intermediary: %{ + internal_pubkey: "187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27", + tweak: "cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001", + tweaked_privkey: "ea260c3b10e60f6de018455cd0278f2f5b7e454be1999572789e6a9565d26080", + sigmsg: + "0083020000000065cd1d00d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd9900000000808f891b00000000225120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3ffffffffffcef8fb4ca7efc5433f591ecfc57391811ce1e186a3793024def5c884cba51d", + precomputed_used: [], + sig_hash: "325a644af47e8a5a2591cda0ab0723978537318f10e6a63d4eed783b96a71a4d" + }, + expected: %{ + witness: [ + "052aedffc554b41f52b521071793a6b88d6dbca9dba94cf34c83696de0c1ec35ca9c5ed4ab28059bd606a4f3a657eec0bb96661d42921b5f50a95ad33675b54f83" + ] + } + }, + %{ + given: %{ + txin_index: 3, + internal_privkey: "d3c7af07da2d54f7a7735d3d0fc4f0a73164db638b2f2f7c43f711f6d4aa7e64", + merkle_root: "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b", + hash_type: 1 + }, + intermediary: %{ + internal_pubkey: "93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820", + tweak: "6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30", + tweaked_privkey: "97323385e57015b75b0339a549c56a948eb961555973f0951f555ae6039ef00d", + sigmsg: + "0001020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957ea2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc50003000000", + precomputed_used: [ + "hash_amounts", + "hash_outputs", + "hash_prevouts", + "hash_script_pubkeys", + "hash_sequences" + ], + sig_hash: "bf013ea93474aa67815b1b6cc441d23b64fa310911d991e713cd34c7f5d46669" + }, + expected: %{ + witness: [ + "ff45f742a876139946a149ab4d9185574b98dc919d2eb6754f8abaa59d18b025637a3aa043b91817739554f4ed2026cf8022dbd83e351ce1fabc272841d2510a01" + ] + } + }, + %{ + given: %{ + txin_index: 4, + internal_privkey: "f36bb07a11e469ce941d16b63b11b9b9120a84d9d87cff2c84a8d4affb438f4e", + merkle_root: "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + hash_type: 0 + }, + intermediary: %{ + internal_pubkey: "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + tweak: "b57bfa183d28eeb6ad688ddaabb265b4a41fbf68e5fed2c72c74de70d5a786f4", + tweaked_privkey: "a8e7aa924f0d58854185a490e6c41f6efb7b675c0f3331b7f14b549400b4d501", + sigmsg: + "0000020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957ea2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc50004000000", + precomputed_used: [ + "hash_amounts", + "hash_outputs", + "hash_prevouts", + "hash_script_pubkeys", + "hash_sequences" + ], + sig_hash: "4f900a0bae3f1446fd48490c2958b5a023228f01661cda3496a11da502a7f7ef" + }, + expected: %{ + witness: [ + "b4010dd48a617db09926f729e79c33ae0b4e94b79f04a1ae93ede6315eb3669de185a17d2b0ac9ee09fd4c64b678a0b61a0a86fa888a273c8511be83bfd6810f" + ] + } + }, + %{ + given: %{ + txin_index: 6, + internal_privkey: "415cfe9c15d9cea27d8104d5517c06e9de48e2f986b695e4f5ffebf230e725d8", + merkle_root: "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + hash_type: 2 + }, + intermediary: %{ + internal_pubkey: "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + tweak: "6579138e7976dc13b6a92f7bfd5a2fc7684f5ea42419d43368301470f3b74ed9", + tweaked_privkey: "241c14f2639d0d7139282aa6abde28dd8a067baa9d633e4e7230287ec2d02901", + sigmsg: + "0002020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e0006000000", + precomputed_used: [ + "hash_amounts", + "hash_prevouts", + "hash_script_pubkeys", + "hash_sequences" + ], + sig_hash: "15f25c298eb5cdc7eb1d638dd2d45c97c4c59dcaec6679cfc16ad84f30876b85" + }, + expected: %{ + witness: [ + "a3785919a2ce3c4ce26f298c3d51619bc474ae24014bcdd31328cd8cfbab2eff3395fa0a16fe5f486d12f22a9cedded5ae74feb4bbe5351346508c5405bcfee002" + ] + } + }, + %{ + given: %{ + txin_index: 7, + internal_privkey: "c7b0e81f0a9a0b0499e112279d718cca98e79a12e2f137c72ae5b213aad0d103", + merkle_root: "6c2dc106ab816b73f9d07e3cd1ef2c8c1256f519748e0813e4edd2405d277bef", + hash_type: 130 + }, + intermediary: %{ + internal_pubkey: "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + tweak: "9e0517edc8259bb3359255400b23ca9507f2a91cd1e4250ba068b4eafceba4a9", + tweaked_privkey: "65b6000cd2bfa6b7cf736767a8955760e62b6649058cbc970b7c0871d786346b", + sigmsg: + "0082020000000065cd1d00e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf00000000804c8b2000000000225120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5ffffffff", + precomputed_used: [], + sig_hash: "cd292de50313804dabe4685e83f923d2969577191a3e1d2882220dca88cbeb10" + }, + expected: %{ + witness: [ + "ea0c6ba90763c2d3a296ad82ba45881abb4f426b3f87af162dd24d5109edc1cdd11915095ba47c3a9963dc1e6c432939872bc49212fe34c632cd3ab9fed429c482" + ] + } + }, + %{ + given: %{ + txin_index: 8, + internal_privkey: "77863416be0d0665e517e1c375fd6f75839544eca553675ef7fdf4949518ebaa", + merkle_root: "ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc", + hash_type: 129 + }, + intermediary: %{ + internal_pubkey: "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + tweak: "639f0281b7ac49e742cd25b7f188657626da1ad169209078e2761cefd91fd65e", + tweaked_privkey: "ec18ce6af99f43815db543f47b8af5ff5df3b2cb7315c955aa4a86e8143d2bf5", + sigmsg: + "0081020000000065cd1da2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc500a778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af101000000002b0c230000000022512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220ffffffff", + precomputed_used: [ + "hash_outputs" + ], + sig_hash: "cccb739eca6c13a8a89e6e5cd317ffe55669bbda23f2fd37b0f18755e008edd2" + }, + expected: %{ + witness: [ + "bbc9584a11074e83bc8c6759ec55401f0ae7b03ef290c3139814f545b58a9f8127258000874f44bc46db7646322107d4d86aec8e73b8719a61fff761d75b5dd981" + ] + } + } + ], + auxiliary: %{ + signed_tx: + "020000000001097de20cbff686da83a54981d2b9bab3586f4ca7e48f57f5b55963115f3b334e9c010000000000000000d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd990000000000fffffffff8e1f583384333689228c5d28eac13366be082dc57441760d957275419a41842000000006b4830450221008f3b8f8f0537c420654d2283673a761b7ee2ea3c130753103e08ce79201cf32a022079e7ab904a1980ef1c5890b648c8783f4d10103dd62f740d13daa79e298d50c201210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff0689180aa63b30cb162a73c6d2a38b7eeda2a83ece74310fda0843ad604853b0100000000feffffffaa5202bdf6d8ccd2ee0f0202afbbb7461d9264a25e5bfd3c5a52ee1239e0ba6c0000000000feffffff956149bdc66faa968eb2be2d2faa29718acbfe3941215893a2a3446d32acd050000000000000000000e664b9773b88c09c32cb70a2a3e4da0ced63b7ba3b22f848531bbb1d5d5f4c94010000000000000000e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf0000000000ffffffffa778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af10100000000ffffffff0200ca9a3b000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac807840cb0000000020ac9a87f5594be208f8532db38cff670c450ed2fea8fcdefcc9a663f78bab962b0141ed7c1647cb97379e76892be0cacff57ec4a7102aa24296ca39af7541246d8ff14d38958d4cc1e2e478e4d4a764bbfd835b16d4e314b72937b29833060b87276c030141052aedffc554b41f52b521071793a6b88d6dbca9dba94cf34c83696de0c1ec35ca9c5ed4ab28059bd606a4f3a657eec0bb96661d42921b5f50a95ad33675b54f83000141ff45f742a876139946a149ab4d9185574b98dc919d2eb6754f8abaa59d18b025637a3aa043b91817739554f4ed2026cf8022dbd83e351ce1fabc272841d2510a010140b4010dd48a617db09926f729e79c33ae0b4e94b79f04a1ae93ede6315eb3669de185a17d2b0ac9ee09fd4c64b678a0b61a0a86fa888a273c8511be83bfd6810f0247304402202b795e4de72646d76eab3f0ab27dfa30b810e856ff3a46c9a702df53bb0d8cc302203ccc4d822edab5f35caddb10af1be93583526ccfbade4b4ead350781e2f8adcd012102f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90141a3785919a2ce3c4ce26f298c3d51619bc474ae24014bcdd31328cd8cfbab2eff3395fa0a16fe5f486d12f22a9cedded5ae74feb4bbe5351346508c5405bcfee0020141ea0c6ba90763c2d3a296ad82ba45881abb4f426b3f87af162dd24d5109edc1cdd11915095ba47c3a9963dc1e6c432939872bc49212fe34c632cd3ab9fed429c4820141bbc9584a11074e83bc8c6759ec55401f0ae7b03ef290c3139814f545b58a9f8127258000874f44bc46db7646322107d4d86aec8e73b8719a61fff761d75b5dd9810065cd1d" + } + } + + # https://gist.github.com/giacomocaironi/e41a45195b2ac6863ec46e8f86324757 + @bip341_sighash_all %{ + sighash_flag: 0x00, + unsigned_tx: + "02000000025f6092ec9bb430830dfc344260dd5a03cf355186e774be49b2fe5c362f56cb8d00000000000000000061431892d76aa28b5ed1e3da8800fa0d7190c4b4f22be5f416d2d07e573b32e10100000000000000000100ca9a3b000000001976a914682dfdbc97ab5c31300f36d3c12c6fd854b1b35a88ac00000000", + signed_tx: + "020000000001025f6092ec9bb430830dfc344260dd5a03cf355186e774be49b2fe5c362f56cb8d00000000000000000061431892d76aa28b5ed1e3da8800fa0d7190c4b4f22be5f416d2d07e573b32e10100000000000000000100ca9a3b000000001976a914682dfdbc97ab5c31300f36d3c12c6fd854b1b35a88ac0247304402203120452eed289de04e17740232b5f97fac0bc91e4cbb7750bb3d9f4f3c09477b02207e4e363c8d7914f707ff3ddf84e3201f9e402a7dccd02be5d7739d91b0f91adf01210271be339aeae9ed2c6a5a7f8ac5f49638da387612be881c7ed2fb3848b0ef8a6c01408608a76e87a5be42162284e8d7efc6cf71470351b36e07914fd0cfcb7beae98378fd9f664e274c9c2a2744197da522fdf1e3aba999b318e2587be098d90d453300000000", + inputs: [ + %{ + prev_scriptpubkey: "0014196a5bea745288a7f947993c28e3a0f2108d2e0a", + value: 500_000_000, + privkey: "6b3973ee2ce444ada0147716925f6f77569350804835498593dd3be95163d558", + pubkey: "0271be339aeae9ed2c6a5a7f8ac5f49638da387612be881c7ed2fb3848b0ef8a6c" + }, + %{ + prev_scriptpubkey: "512029d942d0408906b359397b6f87c5145814a9aefc8c396dd05efa8b5b73576bf2", + value: 600_000_000, + privkey: "cf3780a32ef3b2d70366f0124ee40195a251044e82a13146106be75ee049ac02", + # We don't know what aux was used, so this can't be recreated :/ + signature: + "8608a76e87a5be42162284e8d7efc6cf71470351b36e07914fd0cfcb7beae98378fd9f664e274c9c2a2744197da522fdf1e3aba999b318e2587be098d90d4533" + } + ], + intermediary: %{ + data: + "0000020000000000000032553b113292dfa8216546e721388a6c19c76626ca65dc187e0348d6ed445f815733468db74734c00efa0b466bca091d8f1aab074af2538f36bd0a734a5940c5423cd73484fc5e3e0a623442846c279c2216f25a2f32d161fea6c5821a1adde7af5570f5a1810b7af78caf4bc70a660f0df51e42baf91d4de5b2328de0e83dfc8cdee56004a241f9c79cc55b7d79eaed04909d84660502a2d4e9c357c2047cf50001000000", + sighash: "07333acfe6dce8196f1ad62b2e039a3d9f0b6627bf955be767c519c0f8789ff4", + sha_prevouts: %{ + data: + "5f6092ec9bb430830dfc344260dd5a03cf355186e774be49b2fe5c362f56cb8d0000000061431892d76aa28b5ed1e3da8800fa0d7190c4b4f22be5f416d2d07e573b32e101000000", + hash: "32553b113292dfa8216546e721388a6c19c76626ca65dc187e0348d6ed445f81" + }, + sha_amounts: %{ + data: "0065cd1d000000000046c32300000000", + hash: "5733468db74734c00efa0b466bca091d8f1aab074af2538f36bd0a734a5940c5" + }, + sha_scriptpubkeys: %{ + data: + "160014196a5bea745288a7f947993c28e3a0f2108d2e0a22512029d942d0408906b359397b6f87c5145814a9aefc8c396dd05efa8b5b73576bf2", + hash: "423cd73484fc5e3e0a623442846c279c2216f25a2f32d161fea6c5821a1adde7" + }, + sha_sequences: %{ + data: "0000000000000000", + hash: "af5570f5a1810b7af78caf4bc70a660f0df51e42baf91d4de5b2328de0e83dfc" + }, + sha_outputs: %{ + data: "00ca9a3b000000001976a914682dfdbc97ab5c31300f36d3c12c6fd854b1b35a88ac", + hash: "8cdee56004a241f9c79cc55b7d79eaed04909d84660502a2d4e9c357c2047cf5" + } + } + } + + # SIGHASH_ANYONECANPAY(ALL) + @bip341_sighash_anyonecanpay_all %{ + sighash_flag: 0x81, + unsigned_tx: + "02000000015c82840e7a0e5283c5516e742352566408de5c40d45ab0a2f872b37f188976c200000000000000000002003b5808000000001600141192fac5233e4eefa18859396b74851de18f8f4700e1f5050000000022512032c22a6e048b9d4183f612bc1b73a58fc0d4e7f548fd71b732063645d43f420200000000", + signed_tx: + "020000000001015c82840e7a0e5283c5516e742352566408de5c40d45ab0a2f872b37f188976c200000000000000000002003b5808000000001600141192fac5233e4eefa18859396b74851de18f8f4700e1f5050000000022512032c22a6e048b9d4183f612bc1b73a58fc0d4e7f548fd71b732063645d43f4202014153fd82ff31642b92ae43cf0010e2aac2c51a781cb2ce8c72f80477a4900d2f3a4bb1eb986bc000bd5b055c62872ac8c426eb69186b3f2e46656189d1ba97a3078100000000", + inputs: [ + %{ + prev_scriptpubkey: "5120fe7633a26b281a80ee75d344b07ec97e738d4038de288b6caf7d38e06a6c3ee1", + value: 250_000_000, + privkey: "3c1d300faf1d8706fd07137e1cc1d59967ccc0efa6212fc03b2ac7c382fa9133", + # has sighash anyonecanpay appended + signature: + "53fd82ff31642b92ae43cf0010e2aac2c51a781cb2ce8c72f80477a4900d2f3a4bb1eb986bc000bd5b055c62872ac8c426eb69186b3f2e46656189d1ba97a30781" + } + ], + intermediary: %{ + data: + "00810200000000000000d070f96ca70c4dea1042a92e6abf04883e75bd3ad7dd4dcdf18153cda431cbd8005c82840e7a0e5283c5516e742352566408de5c40d45ab0a2f872b37f188976c20000000080b2e60e00000000225120fe7633a26b281a80ee75d344b07ec97e738d4038de288b6caf7d38e06a6c3ee100000000", + sighash: "11998278e8f4fe9ec6e360642a91536a5498a30cf711712ed3d9c25dfede876b", + sha_outputs: %{ + data: + "003b5808000000001600141192fac5233e4eefa18859396b74851de18f8f4700e1f5050000000022512032c22a6e048b9d4183f612bc1b73a58fc0d4e7f548fd71b732063645d43f4202", + hash: "d070f96ca70c4dea1042a92e6abf04883e75bd3ad7dd4dcdf18153cda431cbd8" + } + } + } + + @bip69_txs [ + %{ + tx_hex: + "0100000011aad553bb1650007e9982a8ac79d227cd8c831e1573b11f25573a37664e5f3e64000000006a47304402205438cedd30ee828b0938a863e08d810526123746c1f4abee5b7bc2312373450c02207f26914f4275f8f0040ab3375bacc8c5d610c095db8ed0785de5dc57456591a601210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffffc26f3eb7932f7acddc5ddd26602b77e7516079b03090a16e2c2f5485d1fde028000000006b483045022100f81d98c1de9bb61063a5e6671d191b400fda3a07d886e663799760393405439d0220234303c9af4bad3d665f00277fe70cdd26cd56679f114a40d9107249d29c979401210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffff456a9e597129f5df2e11b842833fc19a94c563f57449281d3cd01249a830a1f0000000006a47304402202310b00924794ef68a8f09564fd0bb128838c66bc45d1a3f95c5cab52680f166022039fc99138c29f6c434012b14aca651b1c02d97324d6bd9dd0ffced0782c7e3bd01210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffff571fb3e02278217852dd5d299947e2b7354a639adc32ec1fa7b82cfb5dec530e000000006b483045022100d276251f1f4479d8521269ec8b1b45c6f0e779fcf1658ec627689fa8a55a9ca50220212a1e307e6182479818c543e1b47d62e4fc3ce6cc7fc78183c7071d245839df01210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffff5d8de50362ff33d3526ac3602e9ee25c1a349def086a7fc1d9941aaeb9e91d38010000006b4830450221008768eeb1240451c127b88d89047dd387d13357ce5496726fc7813edc6acd55ac022015187451c3fb66629af38fdb061dfb39899244b15c45e4a7ccc31064a059730d01210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffff60ad3408b89ea19caf3abd5e74e7a084344987c64b1563af52242e9d2a8320f3000000006b4830450221009be4261ec050ebf33fa3d47248c7086e4c247cafbb100ea7cee4aa81cd1383f5022008a70d6402b153560096c849d7da6fe61c771a60e41ff457aac30673ceceafee01210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffffe9b483a8ac4129780c88d1babe41e89dc10a26dedbf14f80a28474e9a11104de010000006b4830450221009bc40eee321b39b5dc26883f79cd1f5a226fc6eed9e79e21d828f4c23190c57e022078182fd6086e265589105023d9efa4cba83f38c674a499481bd54eee196b033f01210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffffe28db9462d3004e21e765e03a45ecb147f136a20ba8bca78ba60ebfc8e2f8b3b000000006a47304402200fb572b7c6916515452e370c2b6f97fcae54abe0793d804a5a53e419983fae1602205191984b6928bf4a1e25b00e5b5569a0ce1ecb82db2dea75fe4378673b53b9e801210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffff7a1ef65ff1b7b7740c662ab6c9735ace4a16279c23a1db5709ed652918ffff54010000006a47304402206bc218a925f7280d615c8ea4f0131a9f26e7fc64cff6eeeb44edb88aba14f1910220779d5d67231bc2d2d93c3c5ab74dcd193dd3d04023e58709ad7ffbf95161be6201210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffff850cecf958468ca7ffa6a490afe13b8c271b1326b0ddc1fdfdf9f3c7e365fdba000000006a473044022047df98cc26bd2bfdc5b2b97c27aead78a214810ff023e721339292d5ce50823d02205fe99dc5f667908974dae40cc7a9475af7fa6671ba44f64a00fcd01fa12ab523012102ca46fa75454650afba1784bc7b079d687e808634411e4beff1f70e44596308a1ffffffff8640e312040e476cf6727c60ca3f4a3ad51623500aacdda96e7728dbdd99e8a5000000006a47304402205566aa84d3d84226d5ab93e6f253b57b3ef37eb09bb73441dae35de86271352a02206ee0b7f800f73695a2073a2967c9ad99e19f6ddf18ce877adf822e408ba9291e01210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffff91c1889c5c24b93b56e643121f7a05a34c10c5495c450504c7b5afcb37e11d7a000000006b483045022100df61d45bbaa4571cdd6c5c822cba458cdc55285cdf7ba9cd5bb9fc18096deb9102201caf8c771204df7fd7c920c4489da7bc3a60e1d23c1a97e237c63afe53250b4a01210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffff2470947216eb81ea0eeeb4fe19362ec05767db01c3aa3006bb499e8b6d6eaa26010000006a473044022031501a0b2846b8822a32b9947b058d89d32fc758e009fc2130c2e5effc925af70220574ef3c9e350cef726c75114f0701fd8b188c6ec5f84adce0ed5c393828a5ae001210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffff0abcd77d65cc14363f8262898335f184d6da5ad060ff9e40bf201741022c2b40010000006b483045022100a6ac110802b699f9a2bff0eea252d32e3d572b19214d49d8bb7405efa2af28f1022033b7563eb595f6d7ed7ec01734e17b505214fe0851352ed9c3c8120d53268e9a01210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffffa43bebbebf07452a893a95bfea1d5db338d23579be172fe803dce02eeb7c037d010000006b483045022100ebc77ed0f11d15fe630fe533dc350c2ddc1c81cfeb81d5a27d0587163f58a28c02200983b2a32a1014bab633bfc9258083ac282b79566b6b3fa45c1e6758610444f401210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffffb102113fa46ce949616d9cda00f6b10231336b3928eaaac6bfe42d1bf3561d6c010000006a473044022010f8731929a55c1c49610722e965635529ed895b2292d781b183d465799906b20220098359adcbc669cd4b294cc129b110fe035d2f76517248f4b7129f3bf793d07f01210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffffb861fab2cde188499758346be46b5fbec635addfc4e7b0c8a07c0a908f2b11b4000000006a47304402207328142bb02ef5d6496a210300f4aea71f67683b842fa3df32cae6c88b49a9bb022020f56ddff5042260cfda2c9f39b7dec858cc2f4a76a987cd2dc25945b04e15fe01210391064d5b2d1c70f264969046fcff853a7e2bfde5d121d38dc5ebd7bc37c2b210ffffffff027064d817000000001976a9144a5fba237213a062f6f57978f796390bdcf8d01588ac00902f50090000001976a9145be32612930b8323add2212a4ec03c1562084f8488ac00000000", + ordered_inputs: [ + %{txid: "0e53ec5dfb2cb8a71fec32dc9a634a35b7e24799295ddd5278217822e0b31f57", vout: 0}, + %{txid: "26aa6e6d8b9e49bb0630aac301db6757c02e3619feb4ee0eea81eb1672947024", vout: 1}, + %{txid: "28e0fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2", vout: 0}, + %{txid: "381de9b9ae1a94d9c17f6a08ef9d341a5ce29e2e60c36a52d333ff6203e58d5d", vout: 1}, + %{txid: "3b8b2f8efceb60ba78ca8bba206a137f14cb5ea4035e761ee204302d46b98de2", vout: 0}, + %{txid: "402b2c02411720bf409eff60d05adad684f135838962823f3614cc657dd7bc0a", vout: 1}, + %{txid: "54ffff182965ed0957dba1239c27164ace5a73c9b62a660c74b7b7f15ff61e7a", vout: 1}, + %{txid: "643e5f4e66373a57251fb173151e838ccd27d279aca882997e005016bb53d5aa", vout: 0}, + %{txid: "6c1d56f31b2de4bfc6aaea28396b333102b1f600da9c6d6149e96ca43f1102b1", vout: 1}, + %{txid: "7a1de137cbafb5c70405455c49c5104ca3057a1f1243e6563bb9245c9c88c191", vout: 0}, + %{txid: "7d037ceb2ee0dc03e82f17be7935d238b35d1deabf953a892a4507bfbeeb3ba4", vout: 1}, + %{txid: "a5e899dddb28776ea9ddac0a502316d53a4a3fca607c72f66c470e0412e34086", vout: 0}, + %{txid: "b4112b8f900a7ca0c8b0e7c4dfad35c6be5f6be46b3458974988e1cdb2fa61b8", vout: 0}, + %{txid: "bafd65e3c7f3f9fdfdc1ddb026131b278c3be1af90a4a6ffa78c4658f9ec0c85", vout: 0}, + %{txid: "de0411a1e97484a2804ff1dbde260ac19de841bebad1880c782941aca883b4e9", vout: 1}, + %{txid: "f0a130a84912d03c1d284974f563c5949ac13f8342b8112edff52971599e6a45", vout: 0}, + %{txid: "f320832a9d2e2452af63154bc687493484a0e7745ebd3aaf9ca19eb80834ad60", vout: 0} + ], + ordered_outputs: [ + %{value: 400_057_456, scriptpubkey: "76a9144a5fba237213a062f6f57978f796390bdcf8d01588ac"}, + %{ + value: 40_000_000_000, + scriptpubkey: "76a9145be32612930b8323add2212a4ec03c1562084f8488ac" + } + ] + }, + %{ + tx_hex: + "010000000255605dc6f5c3dc148b6da58442b0b2cd422be385eab2ebea4119ee9c268d28350000000049483045022100aa46504baa86df8a33b1192b1b9367b4d729dc41e389f2c04f3e5c7f0559aae702205e82253a54bf5c4f65b7428551554b2045167d6d206dfe6a2e198127d3f7df1501ffffffff55605dc6f5c3dc148b6da58442b0b2cd422be385eab2ebea4119ee9c268d2835010000004847304402202329484c35fa9d6bb32a55a70c0982f606ce0e3634b69006138683bcd12cbb6602200c28feb1e2555c3210f1dddb299738b4ff8bbe9667b68cb8764b5ac17b7adf0001ffffffff0200e1f505000000004341046a0765b5865641ce08dd39690aade26dfbf5511430ca428a3089261361cef170e3929a68aee3d8d4848b0c5111b0a37b82b86ad559fd2a745b44d8e8d9dfdc0cac00180d8f000000004341044a656f065871a353f216ca26cef8dde2f03e8c16202d2e8ad769f02032cb86a5eb5e56842e92e19141d60a01928f8dd2c875a390f67c1f6c94cfc617c0ea45afac00000000", + ordered_inputs: [ + %{ + txid: "35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055", + vout: 0 + }, + %{ + txid: "35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055", + vout: 1 + } + ], + ordered_outputs: [ + %{ + value: 100_000_000, + scriptpubkey: + "41046a0765b5865641ce08dd39690aade26dfbf5511430ca428a3089261361cef170e3929a68aee3d8d4848b0c5111b0a37b82b86ad559fd2a745b44d8e8d9dfdc0cac" + }, + %{ + value: 2_400_000_000, + scriptpubkey: + "41044a656f065871a353f216ca26cef8dde2f03e8c16202d2e8ad769f02032cb86a5eb5e56842e92e19141d60a01928f8dd2c875a390f67c1f6c94cfc617c0ea45afac" + } + ] + }, + # fake tx, same as above but with amount changed. signatures invalid + %{ + tx_hex: + "010000000255605dc6f5c3dc148b6da58442b0b2cd422be385eab2ebea4119ee9c268d28350000000049483045022100aa46504baa86df8a33b1192b1b9367b4d729dc41e389f2c04f3e5c7f0559aae702205e82253a54bf5c4f65b7428551554b2045167d6d206dfe6a2e198127d3f7df1501ffffffff55605dc6f5c3dc148b6da58442b0b2cd422be385eab2ebea4119ee9c268d2835010000004847304402202329484c35fa9d6bb32a55a70c0982f606ce0e3634b69006138683bcd12cbb6602200c28feb1e2555c3210f1dddb299738b4ff8bbe9667b68cb8764b5ac17b7adf0001ffffffff0200e1f505000000004341044a656f065871a353f216ca26cef8dde2f03e8c16202d2e8ad769f02032cb86a5eb5e56842e92e19141d60a01928f8dd2c875a390f67c1f6c94cfc617c0ea45afac00e1f505000000004341046a0765b5865641ce08dd39690aade26dfbf5511430ca428a3089261361cef170e3929a68aee3d8d4848b0c5111b0a37b82b86ad559fd2a745b44d8e8d9dfdc0cac00000000", + ordered_inputs: [ + %{ + txid: "35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055", + vout: 0 + }, + %{ + txid: "35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055", + vout: 1 + } + ], + ordered_outputs: [ + %{ + value: 100_000_000, + scriptpubkey: + "41044a656f065871a353f216ca26cef8dde2f03e8c16202d2e8ad769f02032cb86a5eb5e56842e92e19141d60a01928f8dd2c875a390f67c1f6c94cfc617c0ea45afac" + }, + %{ + value: 100_000_000, + scriptpubkey: + "41046a0765b5865641ce08dd39690aade26dfbf5511430ca428a3089261361cef170e3929a68aee3d8d4848b0c5111b0a37b82b86ad559fd2a745b44d8e8d9dfdc0cac" + } + ] + } + ] + describe "decode/1" do test "decodes legacy bitcoin transaction" do txn_test = @txn_serialization_1 @@ -233,4 +648,196 @@ defmodule Bitcoinex.TransactionTest do assert "76a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac" == out_1.script_pub_key end end + + describe "bip341_sighash" do + test "BIP341 test vector" do + t = @bip341_test_vector + {:ok, unsigned_tx} = Transaction.decode(t.given.unsigned_tx) + + sha_prevouts = Transaction.bip341_sha_prevouts(unsigned_tx.inputs) + assert sha_prevouts == Utils.hex_to_bin(t.intermediary.hash_prevouts) + + prev_amounts = Enum.map(t.given.inputs, fn input -> input.amount_sats end) + sha_amounts = Transaction.bip341_sha_amounts(prev_amounts) + assert sha_amounts == Utils.hex_to_bin(t.intermediary.hash_amounts) + + prev_scriptpubkeys = + Enum.map(t.given.inputs, fn input -> + {:ok, s} = Script.parse_script(input.prev_scriptpubkey) + Script.serialize_with_compact_size(s) + end) + + sha_scriptpubkeys = Transaction.bip341_sha_scriptpubkeys(prev_scriptpubkeys) + assert sha_scriptpubkeys == Utils.hex_to_bin(t.intermediary.hash_script_pubkeys) + + sha_sequences = Transaction.bip341_sha_sequences(unsigned_tx.inputs) + assert sha_sequences == Utils.hex_to_bin(t.intermediary.hash_sequences) + + sha_outputs = Transaction.bip341_sha_outputs(unsigned_tx.outputs) + assert sha_outputs == Utils.hex_to_bin(t.intermediary.hash_outputs) + + # test sighash for each input + for i <- t.input_spending do + sigmsg = + Transaction.bip341_sigmsg( + unsigned_tx, + i.given.hash_type, + 0, + i.given.txin_index, + prev_amounts, + prev_scriptpubkeys + ) + + assert Base.encode16(sigmsg, case: :lower) == i.intermediary.sigmsg + + sighash = Taproot.tagged_hash_tapsighash(sigmsg) + assert Base.encode16(sighash, case: :lower) == i.intermediary.sig_hash + + {:ok, sk} = + i.given.internal_privkey + |> Base.decode16!(case: :lower) + |> :binary.decode_unsigned() + |> PrivateKey.new() + + merkle_root = + if i.given.merkle_root == nil do + <<>> + else + i.given.merkle_root + |> Utils.hex_to_bin() + end + + tweaked_sk = Taproot.tweak_privkey(sk, merkle_root) + + assert tweaked_sk.d |> :binary.encode_unsigned() |> Base.encode16(case: :lower) == + i.intermediary.tweaked_privkey + + # BIP341 declares test vectors to all use aux=0 + {:ok, sig} = Schnorr.sign(tweaked_sk, :binary.decode_unsigned(sighash), 0) + + hash_byte = + if i.given.hash_type == 0x00 do + <<>> + else + <> + end + + assert Base.encode16(Signature.serialize_signature(sig) <> hash_byte, case: :lower) == + Enum.at(i.expected.witness, 0) + end + end + + test "SIGHASH_ALL" do + t = @bip341_sighash_all + {:ok, unsigned_tx} = Transaction.decode(t.unsigned_tx) + # intermediary hashes + sha_prevouts = Transaction.bip341_sha_prevouts(unsigned_tx.inputs) + assert sha_prevouts == Utils.hex_to_bin(t.intermediary.sha_prevouts.hash) + + prev_amounts = Enum.map(t.inputs, fn input -> input.value end) + sha_amounts = Transaction.bip341_sha_amounts(prev_amounts) + assert sha_amounts == Utils.hex_to_bin(t.intermediary.sha_amounts.hash) + + prev_scriptpubkeys = + Enum.map(t.inputs, fn input -> + {:ok, s} = Script.parse_script(input.prev_scriptpubkey) + Script.serialize_with_compact_size(s) + end) + + sha_scriptpubkeys = Transaction.bip341_sha_scriptpubkeys(prev_scriptpubkeys) + assert sha_scriptpubkeys == Utils.hex_to_bin(t.intermediary.sha_scriptpubkeys.hash) + + sha_sequences = Transaction.bip341_sha_sequences(unsigned_tx.inputs) + assert sha_sequences == Utils.hex_to_bin(t.intermediary.sha_sequences.hash) + + sha_outputs = Transaction.bip341_sha_outputs(unsigned_tx.outputs) + assert sha_outputs == Utils.hex_to_bin(t.intermediary.sha_outputs.hash) + + sigmsg = + Transaction.bip341_sigmsg( + unsigned_tx, + t.sighash_flag, + 0, + 1, + prev_amounts, + prev_scriptpubkeys + ) + + assert sigmsg == Utils.hex_to_bin(t.intermediary.data) + sighash = Taproot.tagged_hash_tapsighash(sigmsg) + assert sighash == Utils.hex_to_bin(t.intermediary.sighash) + end + + test "SIGHASH_ANYONECANPAY_ALL" do + t = @bip341_sighash_anyonecanpay_all + {:ok, unsigned_tx} = Transaction.decode(t.unsigned_tx) + + sha_outputs = Transaction.bip341_sha_outputs(unsigned_tx.outputs) + assert sha_outputs == Utils.hex_to_bin(t.intermediary.sha_outputs.hash) + + prev_amounts = Enum.map(t.inputs, fn input -> input.value end) + + prev_scriptpubkeys = + Enum.map(t.inputs, fn input -> + {:ok, s} = Script.parse_script(input.prev_scriptpubkey) + Script.serialize_with_compact_size(s) + end) + + sigmsg = + Transaction.bip341_sigmsg( + unsigned_tx, + t.sighash_flag, + 0, + 0, + prev_amounts, + prev_scriptpubkeys + ) + + assert sigmsg == Utils.hex_to_bin(t.intermediary.data) + + sighash = Taproot.tagged_hash_tapsighash(sigmsg) + assert sighash == Utils.hex_to_bin(t.intermediary.sighash) + + sighash2 = + Transaction.bip341_sighash( + unsigned_tx, + t.sighash_flag, + 0, + 0, + prev_amounts, + prev_scriptpubkeys + ) + + assert sighash2 == Utils.hex_to_bin(t.intermediary.sighash) + end + end + + describe "bip 69" do + test "input sorting" do + for t <- @bip69_txs do + {:ok, tx} = Transaction.decode(t.tx_hex) + + # randomly shuffle inputs then sort them again + for _ <- 1..10 do + rand_inputs = Enum.shuffle(tx.inputs) + sorted_inputs = Transaction.In.lexicographical_sort_inputs(rand_inputs) + zip_list = Enum.zip(sorted_inputs, t.ordered_inputs) + + Enum.each(zip_list, fn {sorted_input, ordered_input} -> + assert sorted_input.prev_txid == ordered_input.txid + assert sorted_input.prev_vout == ordered_input.vout + end) + + shuffled_outputs = Enum.shuffle(tx.outputs) + sorted_outputs = Transaction.Out.lexicographical_sort_outputs(shuffled_outputs) + zip_list = Enum.zip(sorted_outputs, t.ordered_outputs) + + Enum.each(zip_list, fn {sorted_output, ordered_output} -> + assert sorted_output.value == ordered_output.value + assert sorted_output.script_pub_key == ordered_output.scriptpubkey + end) + end + end + end + end end