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 62cded6..61af54e 100644 --- a/lib/psbt.ex +++ b/lib/psbt.ex @@ -11,6 +11,7 @@ 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 @@ -25,6 +26,7 @@ defmodule Bitcoinex.PSBT do @magic 0x70736274 @separator 0xFF + @spec separator :: 255 def separator, do: @separator @doc """ @@ -103,7 +105,7 @@ defmodule Bitcoinex.PSBT do }} end - @spec from_tx(Transaction.t()) :: {:ok, Bitcoinex.PSBT.t()} + @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) @@ -125,6 +127,24 @@ defmodule Bitcoinex.PSBT do %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 @@ -132,6 +152,7 @@ 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 @@ -157,11 +178,12 @@ 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 @@ -169,6 +191,7 @@ defmodule Bitcoinex.PSBT.Utils do |> :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) @@ -198,11 +221,21 @@ defmodule Bitcoinex.PSBT.Utils do @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) - TxUtils.serialize_compact_size_unsigned_int(length(leaf_hashes)) <> leaf_hashes_bin + 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 @@ -211,6 +244,7 @@ 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.ExtendedKey @@ -239,11 +273,74 @@ defmodule Bitcoinex.PSBT.Global do @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} + 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 @@ -253,7 +350,8 @@ defmodule Bitcoinex.PSBT.Global 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} @@ -278,42 +376,39 @@ defmodule Bitcoinex.PSBT.Global do derivation: path } - global_xpubs = PsbtUtils.append(global.xpub, global_xpub) - - global = %Global{global | xpub: global_xpubs} - + global = add_field(global, :xpub, global_xpub) {global, psbt} end defp parse(<<@psbt_global_tx_version::big-size(8)>>, psbt, global) do {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) - global = %Global{global | tx_version: value} + 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 = %Global{global | fallback_locktime: value} + 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 = %Global{global | input_count: input_count} + 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 = %Global{global | output_count: output_count} + 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 = %Global{global | tx_modifiable: value} + global = add_field(global, :tx_modifiable, value) {global, psbt} end @@ -323,9 +418,15 @@ defmodule Bitcoinex.PSBT.Global do {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 @@ -356,14 +457,14 @@ defmodule Bitcoinex.PSBT.Global do defp serialize_kv(:input_count, value) when value != nil do PsbtUtils.serialize_kv( <<@psbt_global_input_count::big-size(8)>>, - TxUtils.serialize_compact_size_unsigned_int(value) + 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)>>, - TxUtils.serialize_compact_size_unsigned_int(value) + Utils.serialize_compact_size_unsigned_int(value) ) end @@ -378,10 +479,15 @@ defmodule Bitcoinex.PSBT.Global do ) end - defp serialize_kv(:proprietary, value) when value != nil do - PsbtUtils.serialize_kv(<<@psbt_global_proprietary::big-size(8)>>, value) + 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 serialized_global = Enum.reduce( @@ -429,7 +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, @@ -488,6 +596,168 @@ defmodule Bitcoinex.PSBT.In do @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) @@ -535,13 +805,13 @@ 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) @@ -554,14 +824,14 @@ defmodule Bitcoinex.PSBT.In do 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 @@ -580,7 +850,7 @@ 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 = PsbtUtils.serialize_fingerprint_path(value.pfp, value.derivation) @@ -591,35 +861,35 @@ defmodule Bitcoinex.PSBT.In do PsbtUtils.serialize_kv(<<@psbt_in_por_commitment::big-size(8)>>, value) end - defp serialize_kv(:in_ripemd160, value) when value != nil do + defp serialize_kv(:in_ripemd160, %{hash: hash, preimage: preimage}) do PsbtUtils.serialize_kv( - <<@psbt_in_ripemd160::big-size(8), value.hash::binary-size(20)>>, - value.preimage + <<@psbt_in_ripemd160::big-size(8), hash::binary-size(20)>>, + preimage ) end - defp serialize_kv(:in_sha256, value) when value != nil do + defp serialize_kv(:in_sha256, %{hash: hash, preimage: preimage}) do PsbtUtils.serialize_kv( - <<@psbt_in_sha256::big-size(8), value.hash::binary-size(32)>>, - value.preimage + <<@psbt_in_sha256::big-size(8), hash::binary-size(32)>>, + preimage ) end - defp serialize_kv(:in_hash160, value) when value != nil do + defp serialize_kv(:in_hash160, %{hash: hash, preimage: preimage}) do PsbtUtils.serialize_kv( - <<@psbt_in_hash160::big-size(8), value.hash::binary-size(20)>>, - value.preimage + <<@psbt_in_hash160::big-size(8), hash::binary-size(20)>>, + preimage ) end - defp serialize_kv(:in_hash256, value) when value != nil do + defp serialize_kv(:in_hash256, %{hash: hash, preimage: preimage}) do PsbtUtils.serialize_kv( - <<@psbt_in_hash256::big-size(8), value.hash::binary-size(32)>>, - value.preimage + <<@psbt_in_hash256::big-size(8), hash::binary-size(32)>>, + preimage ) end - defp serialize_kv(:previous_txid, value) when value != nil do + defp serialize_kv(:previous_txid, <>) do PsbtUtils.serialize_kv(<<@psbt_in_previous_txid::big-size(8)>>, value) end @@ -651,7 +921,8 @@ defmodule Bitcoinex.PSBT.In do defp serialize_kv(:tap_script_sig, value) when value != nil do PsbtUtils.serialize_kv( - <<@psbt_in_tap_script_sig::big-size(8), value.pubkey::binary, value.leaf_hash::binary>>, + <<@psbt_in_tap_script_sig::big-size(8), Point.x_bytes(value.public_key)::binary, + value.leaf_hash::binary>>, value.signature ) end @@ -671,21 +942,21 @@ defmodule Bitcoinex.PSBT.In do fingerprint_path = PsbtUtils.serialize_fingerprint_path(value.pfp, value.derivation) PsbtUtils.serialize_kv( - <<@psbt_in_tap_bip32_derivation::big-size(8), value.pubkey::binary>>, + <<@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)>>, value) + PsbtUtils.serialize_kv(<<@psbt_in_tap_internal_key::big-size(8)>>, Point.x_bytes(value)) end - defp serialize_kv(:tap_merkle_root, value) when value != nil do + defp serialize_kv(:tap_merkle_root, <>) do PsbtUtils.serialize_kv(<<@psbt_in_tap_merkle_root::big-size(8)>>, value) end - defp serialize_kv(:proprietary, value) when value != nil do - PsbtUtils.serialize_kv(<<@psbt_in_proprietary::big-size(8)>>, value) + 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 @@ -779,46 +1050,47 @@ defmodule Bitcoinex.PSBT.In do 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(value) - input = %In{input | non_witness_utxo: txn} + 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) + {:ok, pk} = Point.parse_public_key(public_key) + partial_sig = %{ - public_key: Base.encode16(public_key, case: :lower), + public_key: pk, signature: Base.encode16(value, case: :lower) } - partial_sigs = PsbtUtils.append(input.partial_sig, partial_sig) - input = %In{input | partial_sig: partial_sigs} + input = add_field(input, :partial_sig, partial_sig) {input, psbt} end defp parse(<<@psbt_in_sighash_type::big-size(8)>>, psbt, input) do {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{input | sighash_type: value} + 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 @@ -827,27 +1099,27 @@ defmodule Bitcoinex.PSBT.In do {pfp, path} = PsbtUtils.parse_fingerprint_path(value) + {:ok, pk} = Point.parse_public_key(public_key) + derivation = %{ - public_key: Base.encode16(public_key, case: :lower), + public_key: pk, pfp: pfp, derivation: path } - bip32_derivation = PsbtUtils.append(input.bip32_derivation, derivation) - - input = %In{input | bip32_derivation: bip32_derivation} + 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 @@ -860,9 +1132,7 @@ defmodule Bitcoinex.PSBT.In do preimage: preimage } - ripemd160s = PsbtUtils.append(input.ripemd160, data) - - input = %In{input | ripemd160: ripemd160s} + input = add_field(input, :ripemd160, data) {input, psbt} end @@ -875,8 +1145,7 @@ defmodule Bitcoinex.PSBT.In do preimage: preimage } - sha256s = PsbtUtils.append(input.sha256, data) - input = %In{input | sha256: sha256s} + input = add_field(input, :sha256, data) {input, psbt} end @@ -889,8 +1158,7 @@ defmodule Bitcoinex.PSBT.In do preimage: preimage } - hash160s = PsbtUtils.append(input.hash160, data) - input = %In{input | hash160: hash160s} + input = add_field(input, :hash160, data) {input, psbt} end @@ -903,47 +1171,43 @@ defmodule Bitcoinex.PSBT.In do preimage: preimage } - hash256s = PsbtUtils.append(input.hash256, data) - input = %In{input | hash256: hash256s} + input = add_field(input, :hash256, data) {input, psbt} end defp parse(<<@psbt_in_previous_txid::big-size(8)>>, psbt, input) do - {value = <<_::binary-size(32)>>, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{input | previous_txid: value} + {<>, 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 = %In{input | output_index: value} + 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 = %In{input | sequence: value} + input = add_field(input, :sequence, value) {input, psbt} end defp parse(<<@psbt_in_required_time_locktime::big-size(8)>>, psbt, input) do - # TODO:validation must be > 500_000_000 {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{input | required_time_locktime: value} + input = add_field(input, :required_time_locktime, value) {input, psbt} end defp parse(<<@psbt_in_required_height_locktime::big-size(8)>>, psbt, input) do - # TODO:validation must be < 500_000_000 {<>, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{input | required_height_locktime: value} + input = add_field(input, :required_height_locktime, value) {input, psbt} end defp parse(<<@psbt_in_tap_key_sig::big-size(8)>>, psbt, input) do - # TODO:validation validate script len (64|65) {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{input | tap_key_sig: value} + input = add_field(input, :tap_key_sig, value) {input, psbt} end @@ -956,15 +1220,15 @@ defmodule Bitcoinex.PSBT.In do # TODO:validation validate sig len (64|65) {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + {:ok, pk} = Point.lift_x(pubkey) + data = %{ - pubkey: pubkey, + public_key: pk, leaf_hash: leaf_hash, signature: value } - tap_script_sigs = PsbtUtils.append(input.tap_script_sig, data) - - input = %In{input | tap_script_sig: tap_script_sigs} + input = add_field(input, :tap_script_sig, data) {input, psbt} end @@ -987,53 +1251,51 @@ defmodule Bitcoinex.PSBT.In do control_block: control_block } - tap_leaf_scripts = PsbtUtils.append(input.tap_leaf_script, data) - input = %In{input | tap_leaf_script: tap_leaf_scripts} + 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 = %{ - pubkey: pubkey, + public_key: pk, leaf_hashes: leaf_hashes, pfp: pfp, derivation: path } - tap_bip32_derivation = PsbtUtils.append(input.tap_bip32_derivation, derivation) - - input = %In{input | tap_bip32_derivation: tap_bip32_derivation} + 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 = %In{input | tap_internal_key: value} + 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 = %In{input | tap_merkle_root: value} + input = add_field(input, :tap_merkle_root, value) {input, psbt} end - defp parse(<<@psbt_in_proprietary::big-size(8)>>, psbt, input) do + defp parse(<<@psbt_in_proprietary::big-size(8), key::binary>>, psbt, input) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) - input = %In{input | proprietary: value} + 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 @@ -1041,16 +1303,7 @@ defmodule Bitcoinex.PSBT.In do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) kv = %{key: key, value: value} - unknown = - case input.unknown do - nil -> - [kv] - - _ -> - input.unknown ++ [kv] - end - - input = %In{input | unknown: unknown} + input = add_field(input, :unknown, kv) {input, psbt} end end @@ -1060,9 +1313,12 @@ 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, @@ -1073,7 +1329,8 @@ defmodule Bitcoinex.PSBT.Out do :tap_internal_key, :tap_tree, :tap_bip32_derivation, - :proprietary + :proprietary, + :unknown ] @psbt_out_redeem_script 0x00 @@ -1086,6 +1343,65 @@ defmodule Bitcoinex.PSBT.Out do @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, <<>>) end @@ -1094,6 +1410,7 @@ 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() @@ -1102,7 +1419,7 @@ defmodule Bitcoinex.PSBT.Out do 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 @@ -1114,7 +1431,7 @@ 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) + 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) @@ -1129,7 +1446,7 @@ defmodule Bitcoinex.PSBT.Out do end defp serialize_kv(:tap_internal_key, value) when value != nil do - PsbtUtils.serialize_kv(<<@psbt_out_tap_internal_key::big-size(8)>>, value) + 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 @@ -1138,15 +1455,19 @@ defmodule Bitcoinex.PSBT.Out do end defp serialize_kv(:tap_bip32_derivation, value) when value != nil do - key = <<@psbt_out_tap_bip32_derivation::big-size(8), value.pubkey::binary>> + 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, value) when value != nil do - PsbtUtils.serialize_kv(<<@psbt_out_proprietary::big-size(8)>>, value) + 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 @@ -1208,13 +1529,13 @@ defmodule Bitcoinex.PSBT.Out do 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 @@ -1225,35 +1546,35 @@ defmodule Bitcoinex.PSBT.Out do ) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) + {:ok, pk} = Point.parse_public_key(public_key) + {pfp, path} = PsbtUtils.parse_fingerprint_path(value) derivation = %{ - public_key: Base.encode16(public_key, case: :lower), + public_key: pk, pfp: pfp, derivation: path } - bip32_derivation = PsbtUtils.append(output.bip32_derivation, derivation) - output = %Out{output | bip32_derivation: bip32_derivation} + 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 = %Out{output | amount: amount} + 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) - {:ok, script} = Script.parse_script(value) - output = %Out{output | script: script} + 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 = %Out{output | tap_internal_key: value} + output = add_field(output, :tap_internal_key, value) {output, psbt} end @@ -1261,14 +1582,14 @@ defmodule Bitcoinex.PSBT.Out 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 = %Out{output | tap_tree: %{leaves: leaves}} + 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 + output = %Out{} ) do {value, psbt} = PsbtUtils.parse_compact_size_value(psbt) @@ -1276,21 +1597,23 @@ defmodule Bitcoinex.PSBT.Out do {leaf_hashes, value} = PsbtUtils.parse_leaf_hashes(value, leaf_hash_ct) {pfp, path} = PsbtUtils.parse_fingerprint_path(value) + {:ok, pk} = Point.lift_x(pubkey) + derivation = %{ - pubkey: pubkey, + public_key: pk, leaf_hashes: leaf_hashes, pfp: pfp, derivation: path } - tap_bip32_derivation = PsbtUtils.append(output.tap_bip32_derivation, derivation) - output = %Out{output | tap_bip32_derivation: tap_bip32_derivation} + 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 = %Out{output | proprietary: value} + output = add_field(output, :proprietary, value) {output, psbt} end @@ -1320,7 +1643,7 @@ defmodule Bitcoinex.PSBT.Out do acc <> <> <> - TxUtils.serialize_compact_size_unsigned_int(byte_size(script_bytes)) <> + Utils.serialize_compact_size_unsigned_int(byte_size(script_bytes)) <> script_bytes end) end diff --git a/lib/script.ex b/lib/script.ex index 930ac24..e480bb2 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}"} @@ -204,6 +207,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 +339,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 +351,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 +361,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 +371,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 +381,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 +391,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 - number of signatures required, and the n authorized public keys. + 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 +453,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 +464,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 +510,44 @@ 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. """ - @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())) :: {:ok, t()} | {:error, String.t()} + def create_multisig(m, pubkeys) when is_valid_multisig(m, pubkeys) do 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) @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())) :: {: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) do + case create_multisig(m, pubkeys) do + {:ok, multisig} -> + h160 = hash160(multisig) {:ok, p2sh} = create_p2sh(h160) - {:ok, p2sh, multi} + {:ok, p2sh, multisig} {:error, msg} -> {:error, msg} @@ -546,17 +555,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())) :: {: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) do + case create_multisig(m, pubkeys) do + {:ok, multisig} -> + h256 = sha256(multisig) {:ok, p2wsh} = create_p2wsh(h256) - {:ok, p2wsh, multi} + {:ok, p2wsh, multisig} {:error, msg} -> {:error, msg} @@ -603,13 +612,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 7e72972..6408b31 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(), @@ -27,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. @@ -44,6 +70,280 @@ 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. """ @@ -119,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. @@ -154,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 = <> @@ -167,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 @@ -202,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()) @@ -211,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 @@ -242,12 +525,12 @@ defmodule Bitcoinex.Transaction.Witness 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) @@ -292,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 @@ -300,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(), @@ -325,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, @@ -348,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 @@ -382,6 +696,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(), @@ -395,20 +710,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 diff --git a/lib/utils.ex b/lib/utils.ex index 54d5dbf..6380ba9 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_sort(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/psbt_test.exs b/test/psbt_test.exs index 03c312d..c2ea453 100644 --- a/test/psbt_test.exs +++ b/test/psbt_test.exs @@ -121,11 +121,28 @@ defmodule Bitcoinex.PSBTTest do }, expected_in: [ %In{ - final_scriptsig: - "47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa88292" + 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{ - redeem_script: "001485d13537f2e265405a34dbafa9e3dda01fb82308", + 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 @@ -189,14 +206,24 @@ defmodule Bitcoinex.PSBTTest do derivation: %Bitcoinex.ExtendedKey.DerivationPath{ child_nums: [2_147_483_822, 2_147_483_648, 0] }, - public_key: "029da12cdb5b235692b91536afefe5c91c3ab9473d8e43b533836ab456299c8871", + 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: %Bitcoinex.ExtendedKey.DerivationPath{ child_nums: [2_147_483_822, 2_147_483_649, 0] }, - public_key: "03372b34234ed7cf9c1fea5d05d441557927be9542b162eb02e1ab2ce80224c00b", + 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>> } ], @@ -216,7 +243,12 @@ defmodule Bitcoinex.PSBTTest do derivation: %Bitcoinex.ExtendedKey.DerivationPath{ child_nums: [2_147_483_648, 2_147_483_648, 2_147_483_649] }, - public_key: "039eff1f547a1d5f92dfa2ba7af6ac971a4bd03ba4a734b03156a256b8ad3a1ef9", + 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>> } ] @@ -253,25 +285,47 @@ defmodule Bitcoinex.PSBTTest do derivation: %Bitcoinex.ExtendedKey.DerivationPath{ child_nums: [2_147_483_648, 2_147_483_648, 2_147_483_652] }, - public_key: "03b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46", + 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: %Bitcoinex.ExtendedKey.DerivationPath{ child_nums: [2_147_483_648, 2_147_483_648, 2_147_483_653] }, - public_key: "03de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd", + 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: "03b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46", + 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" } ], - redeem_script: "0020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681", + 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>> + ] + }, witness_script: "522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae", witness_utxo: %Bitcoinex.Transaction.Out{ @@ -358,21 +412,36 @@ defmodule Bitcoinex.PSBTTest do derivation: %Bitcoinex.ExtendedKey.DerivationPath{ child_nums: [2_147_483_648, 2_147_483_650, 2, 0, 0, 1, 13] }, - public_key: "023e1a92f74483a4c12a196d6c0253ac589cfadd5964ff3e6c63aef5d577051ed4", + 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: %Bitcoinex.ExtendedKey.DerivationPath{ child_nums: [2_147_483_648, 2_147_483_650, 2, 0, 0, 1, 13] }, - public_key: "024d0e39b33ab57782a98fd06a7d0653385e509ee3b74dc45559e6db6391d31378", + 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: %Bitcoinex.ExtendedKey.DerivationPath{ child_nums: [2_147_483_648, 2_147_483_650, 2, 0, 0, 1, 13] }, - public_key: "036c0bae6bcb02e47c857c480ed1d23888c8ad6a66b83ab111cb6b9242afb1c725", + 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>> } ], @@ -459,7 +528,14 @@ defmodule Bitcoinex.PSBTTest do value: 100_000_000, script_pub_key: "a9143545e6e33b832c47050f24d3eeb93c9c03948bc787" }, - redeem_script: "001485d13537f2e265405a34dbafa9e3dda01fb82308" + 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: [ @@ -470,7 +546,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_648, 2_147_483_648, 2_147_483_650] }, pfp: <<180, 166, 186, 103>>, - public_key: "02ead596687ca806043edc3de116cdf29d5e9257c196cd055cf698c8d02bf24e99" + 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 + } } ] }, @@ -481,7 +562,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_648, 2_147_483_649, 2_147_483_650] }, pfp: <<180, 166, 186, 103>>, - public_key: "0394f62be9df19952c5587768aeb7698061ad2c4a25c894f47d8c162b4d7213d05" + 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 + } } ] } @@ -660,7 +746,12 @@ defmodule Bitcoinex.PSBTTest do }, partial_sig: [ %{ - public_key: "03309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c", + 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" } @@ -671,7 +762,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_697, 2_147_483_648, 2_147_483_648, 0, 1] }, pfp: <<39, 86, 156, 80>>, - public_key: "03309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c" + 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 + } } ] }, @@ -682,7 +778,12 @@ defmodule Bitcoinex.PSBTTest do }, partial_sig: [ %{ - public_key: "02c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b110", + 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" } @@ -693,7 +794,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_697, 2_147_483_648, 2_147_483_648, 0, 0] }, pfp: <<39, 86, 156, 80>>, - public_key: "02c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b110" + 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 + } } ] } @@ -707,7 +813,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_697, 2_147_483_648, 2_147_483_648, 0, 4] }, pfp: <<39, 86, 156, 80>>, - public_key: "02d20ca502ee289686d21815bd43a80637b0698e1fbcdbe4caed445f6c1a0a90ef" + 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 + } } ] } @@ -842,7 +953,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -862,7 +978,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -925,7 +1046,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -945,7 +1071,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -1011,7 +1142,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -1031,7 +1167,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -1094,7 +1235,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -1114,7 +1260,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -1177,7 +1328,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -1197,7 +1353,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -1260,7 +1421,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -1280,7 +1446,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -1343,7 +1514,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -1363,7 +1539,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -1426,7 +1607,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -1446,7 +1632,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -1509,7 +1700,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -1529,7 +1725,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -1592,7 +1793,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -1612,7 +1818,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -1675,7 +1886,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -1695,7 +1911,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -1758,7 +1979,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -1778,7 +2004,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -1845,7 +2076,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 42] }, pfp: <<246, 157, 135, 62>>, - public_key: "02d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab792" + 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, @@ -1865,7 +2101,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 1, 100] }, pfp: <<246, 157, 135, 62>>, - public_key: "02e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b340" + 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, @@ -1917,14 +2158,20 @@ defmodule Bitcoinex.PSBTTest do }, leaf_hashes: [], pfp: <<119, 43, 45, 167>>, - pubkey: - <<254, 52, 144, 100, 201, 141, 110, 42, 133, 63, 163, 201, 177, 43, 216, 179, 4, - 161, 156, 25, 92, 96, 239, 167, 238, 35, 147, 4, 109, 63, 162, 50>> + 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: - <<254, 52, 144, 100, 201, 141, 110, 42, 133, 63, 163, 201, 177, 43, 216, 179, 4, 161, - 156, 25, 92, 96, 239, 167, 238, 35, 147, 4, 109, 63, 162, 50>> + 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: [ @@ -1935,7 +2182,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 0] }, pfp: <<119, 43, 45, 167>>, - public_key: "036b772a6db74d8753c98a827958de6c78ab3312109f37d3e0304484242ece73d8" + 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 + } } ] } @@ -1982,14 +2234,20 @@ defmodule Bitcoinex.PSBTTest do }, leaf_hashes: [], pfp: <<119, 43, 45, 167>>, - pubkey: - <<254, 52, 144, 100, 201, 141, 110, 42, 133, 63, 163, 201, 177, 43, 216, 179, 4, - 161, 156, 25, 92, 96, 239, 167, 238, 35, 147, 4, 109, 63, 162, 50>> + 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: - <<254, 52, 144, 100, 201, 141, 110, 42, 133, 63, 163, 201, 177, 43, 216, 179, 4, 161, - 156, 25, 92, 96, 239, 167, 238, 35, 147, 4, 109, 63, 162, 50>> + 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: [ @@ -2000,7 +2258,12 @@ defmodule Bitcoinex.PSBTTest do child_nums: [2_147_483_732, 2_147_483_649, 2_147_483_648, 0, 0] }, pfp: <<119, 43, 45, 167>>, - public_key: "036b772a6db74d8753c98a827958de6c78ab3312109f37d3e0304484242ece73d8" + 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 + } } ] } @@ -2043,21 +2306,30 @@ defmodule Bitcoinex.PSBTTest do }, leaf_hashes: [], pfp: <<119, 43, 45, 167>>, - pubkey: - <<254, 52, 144, 100, 201, 141, 110, 42, 133, 63, 163, 201, 177, 43, 216, 179, 4, - 161, 156, 25, 92, 96, 239, 167, 238, 35, 147, 4, 109, 63, 162, 50>> + 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: - <<254, 52, 144, 100, 201, 141, 110, 42, 133, 63, 163, 201, 177, 43, 216, 179, 4, 161, - 156, 25, 92, 96, 239, 167, 238, 35, 147, 4, 109, 63, 162, 50>> + 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: - <<17, 36, 218, 122, 236, 146, 204, 208, 108, 149, 69, 98, 100, 127, 67, 123, 19, 139, - 149, 114, 26, 132, 190, 43, 242, 39, 107, 189, 218, 179, 230, 113>>, + 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{ @@ -2065,9 +2337,12 @@ defmodule Bitcoinex.PSBTTest do }, leaf_hashes: [], pfp: <<119, 43, 45, 167>>, - pubkey: - <<17, 36, 218, 122, 236, 146, 204, 208, 108, 149, 69, 98, 100, 127, 67, 123, 19, - 139, 149, 114, 26, 132, 190, 43, 242, 39, 107, 189, 218, 179, 230, 113>> + 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 + } } ] } @@ -2167,9 +2442,12 @@ defmodule Bitcoinex.PSBTTest do 97, 114, 190, 1, 115, 104, 168, 153, 19, 175, 7, 79, 64, 11, 9>> ], pfp: <<119, 43, 45, 167>>, - pubkey: - <<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>> + 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{ @@ -2180,17 +2458,23 @@ defmodule Bitcoinex.PSBTTest do 92, 86, 50, 90, 41, 251, 68, 223, 194, 3, 243, 86, 225, 248>> ], pfp: <<119, 43, 45, 167>>, - pubkey: - <<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>> + 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>>, - pubkey: - <<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>> + 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{ @@ -2201,14 +2485,20 @@ defmodule Bitcoinex.PSBTTest do 16, 26, 255, 56, 227, 82, 155, 155, 147, 156, 231, 249, 26, 233, 112>> ], pfp: <<119, 43, 45, 167>>, - pubkey: - <<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>> + 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: - <<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>>, + 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>> @@ -2216,9 +2506,13 @@ defmodule Bitcoinex.PSBTTest do ], expected_out: [ %Bitcoinex.PSBT.Out{ - tap_internal_key: - <<17, 36, 218, 122, 236, 146, 204, 208, 108, 149, 69, 98, 100, 127, 67, 123, 19, 139, - 149, 114, 26, 132, 190, 43, 242, 39, 107, 189, 218, 179, 230, 113>>, + 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{ @@ -2226,9 +2520,13 @@ defmodule Bitcoinex.PSBTTest do }, leaf_hashes: [], pfp: <<119, 43, 45, 167>>, - pubkey: - <<17, 36, 218, 122, 236, 146, 204, 208, 108, 149, 69, 98, 100, 127, 67, 123, 19, - 139, 149, 114, 26, 132, 190, 43, 242, 39, 107, 189, 218, 179, 230, 113>> + 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 + } } ] } @@ -2271,21 +2569,30 @@ defmodule Bitcoinex.PSBTTest do }, leaf_hashes: [], pfp: <<119, 43, 45, 167>>, - pubkey: - <<254, 52, 144, 100, 201, 141, 110, 42, 133, 63, 163, 201, 177, 43, 216, 179, 4, - 161, 156, 25, 92, 96, 239, 167, 238, 35, 147, 4, 109, 63, 162, 50>> + 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: - <<254, 52, 144, 100, 201, 141, 110, 42, 133, 63, 163, 201, 177, 43, 216, 179, 4, 161, - 156, 25, 92, 96, 239, 167, 238, 35, 147, 4, 109, 63, 162, 50>> + 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: - <<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>>, + 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: [ %{ @@ -2336,17 +2643,24 @@ defmodule Bitcoinex.PSBTTest do 59, 22, 23, 218, 58, 65, 89, 202, 0, 130, 22, 205, 133, 107, 46, 14>> ], pfp: <<119, 43, 45, 167>>, - pubkey: - <<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>> + 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>>, - pubkey: - <<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>> + 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{ @@ -2357,9 +2671,13 @@ defmodule Bitcoinex.PSBTTest do 122, 120, 234, 237, 224, 242, 226, 150, 207, 67, 89, 97, 168, 244, 202>> ], pfp: <<119, 43, 45, 167>>, - pubkey: - <<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>> + 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{ @@ -2370,9 +2688,13 @@ defmodule Bitcoinex.PSBTTest do 96, 86, 208, 180, 8, 140, 185, 51, 202, 231, 130, 108, 184, 216, 44>> ], pfp: <<119, 43, 45, 167>>, - pubkey: - <<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>> + 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 + } } ] } @@ -2413,9 +2735,12 @@ defmodule Bitcoinex.PSBTTest do 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>>, - pubkey: - <<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>>, + 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, @@ -2426,9 +2751,12 @@ defmodule Bitcoinex.PSBTTest do 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>>, - pubkey: - <<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>>, + 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, @@ -2439,9 +2767,12 @@ defmodule Bitcoinex.PSBTTest do 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>>, - pubkey: - <<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>>, + 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, @@ -2513,9 +2844,12 @@ defmodule Bitcoinex.PSBTTest do 97, 114, 190, 1, 115, 104, 168, 153, 19, 175, 7, 79, 64, 11, 9>> ], pfp: <<119, 43, 45, 167>>, - pubkey: - <<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>> + 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{ @@ -2526,17 +2860,23 @@ defmodule Bitcoinex.PSBTTest do 92, 86, 50, 90, 41, 251, 68, 223, 194, 3, 243, 86, 225, 248>> ], pfp: <<119, 43, 45, 167>>, - pubkey: - <<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>> + 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>>, - pubkey: - <<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>> + 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{ @@ -2547,14 +2887,20 @@ defmodule Bitcoinex.PSBTTest do 16, 26, 255, 56, 227, 82, 155, 155, 147, 156, 231, 249, 26, 233, 112>> ], pfp: <<119, 43, 45, 167>>, - pubkey: - <<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>> + 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: - <<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>>, + 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>> @@ -2562,9 +2908,13 @@ defmodule Bitcoinex.PSBTTest do ], expected_out: [ %Bitcoinex.PSBT.Out{ - tap_internal_key: - <<17, 36, 218, 122, 236, 146, 204, 208, 108, 149, 69, 98, 100, 127, 67, 123, 19, 139, - 149, 114, 26, 132, 190, 43, 242, 39, 107, 189, 218, 179, 230, 113>>, + 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{ @@ -2572,9 +2922,13 @@ defmodule Bitcoinex.PSBTTest do }, leaf_hashes: [], pfp: <<119, 43, 45, 167>>, - pubkey: - <<17, 36, 218, 122, 236, 146, 204, 208, 108, 149, 69, 98, 100, 127, 67, 123, 19, - 139, 149, 114, 26, 132, 190, 43, 242, 39, 107, 189, 218, 179, 230, 113>> + 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..e361dfd 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", @@ -162,7 +163,253 @@ 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" + ] + } + } + ] + + describe "test basic functions" do test "test new/0 and empty?/1" do s = Script.new() assert Script.empty?(s) @@ -497,35 +744,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 +768,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 +786,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 +819,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 +838,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 +866,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 +885,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 +1236,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 @@ -1157,10 +1383,10 @@ defmodule Bitcoinex.ScriptTest do test "test raw 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_multi(m, pks) + {:ok, p2sh, multi_script} = Script.create_p2sh_multisig(m, pks) {:ok, addr} = Script.to_address(p2sh, multi.network) @@ -1169,7 +1395,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 +1533,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) 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) assert ms == ms2 end @@ -1365,4 +1591,15 @@ 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 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..e730f4d 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,331 @@ 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" + } + } + } + describe "decode/1" do test "decodes legacy bitcoin transaction" do txn_test = @txn_serialization_1 @@ -233,4 +562,167 @@ 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 end