diff --git a/lib/transaction/input.ex b/lib/transaction/input.ex index bd747fc..1e10b6b 100644 --- a/lib/transaction/input.ex +++ b/lib/transaction/input.ex @@ -154,7 +154,6 @@ defmodule Transaction.Input do outputs = case RpcClient.get_raw_transaction(rpc, prev_tx) do {:ok, tx} -> - IEx.pry() Map.get(tx, "result") |> Map.get("vout") reason -> @@ -164,6 +163,24 @@ defmodule Transaction.Input do Enum.at(outputs, prev_index) |> Map.get("scriptPubKey") |> Map.get("hex") end + def prev_out(%__MODULE__{prev_tx: prev_tx, prev_index: prev_index}) do + rpc = RpcClient.new() + prev_tx = if is_binary(prev_tx), do: Base.encode16(prev_tx), else: prev_tx + + outputs = + case RpcClient.get_raw_transaction(rpc, prev_tx) do + {:ok, tx} -> + IEx.pry() + Map.get(tx, "result") |> Map.get("vout") + + reason -> + {:error, reason} + end + + IEx.pry() + Enum.at(outputs, prev_index) + end + @doc """ Checks if a `TxIn` is part of a coinbase transaction. diff --git a/lib/transaction/psbt.ex b/lib/transaction/psbt.ex index 0193c53..dc83c36 100644 --- a/lib/transaction/psbt.ex +++ b/lib/transaction/psbt.ex @@ -1,2 +1,166 @@ defmodule Transaction.PSBT do + alias PrivateKey + alias Script + alias Helpers + alias Transaction.Input + alias Transaction.Output + alias Sdk.RpcClient + + defstruct [ + :unsigned_transaction, + :psbt_inputs, + :global_data + ] + + @enforce_keys [ + :unsigned_transaction, + :psbt_inputs + ] + + @type t :: %__MODULE__{unsigned_transaction: binary(), psbt_inputs: Transaction.PSBT.Input.t()} + + @doc """ + ## CREATOR ROLE + Signing info means adding specific pieces of data into the per-input fields + """ + def new(%Transaction{tx_ins: inputs} = tx) do + psbt_inputs = for _ <- inputs, do: %Transaction.PSBT.Input{} + %__MODULE__{unsigned_transaction: tx, psbt_inputs: psbt_inputs} + end + + @doc """ + ## UPDATER ROLE + Signing info means adding specific pieces of data into the per-input fields + """ + def add_sign_info(%__MODULE__{transaction: tx, psbt_inputs: psbt_inputs} = psbt) do + psbt_inputs + |> Enum.map(fn input -> + prev_out = Input.prev_out(input) + + script_pub_key = + prev_out |> Map.get("scriptPubKey") |> Map.get("hex") |> Base.decode16!(case: :lower) + + value = prev_out |> Map.get("value") |> Common.convert_to_satoshis() |> trunc() + + script_type = Script.identify_script_type(script_pub_key) + + case script_type do + :p2wpkh -> + witness_utxo_data = %{value: value} + + %Transaction.PSBT.Input{ + input + | witness_utxo: witness_utxo_data, + witness_script: :script_from_wallet + } + + type when type in [:p2wsh, :p2tr] -> + witness_utxo_data = %{value: value} + + %Transaction.PSBT.Input{ + input + | witness_utxo: witness_utxo_data, + witness_script: nil + } + + :p2pkh -> + # Entire transaction in hex + prev_output = prev_out |> Map.get("hex") + + %Transaction.PSBT.Input{ + input + | non_witness_utxo: prev_output, + redeem_script: nil + } + + type when type in [:p2pkh, :p2sh, :p2pk] -> + # Entire transaction in hex + prev_output = prev_out |> Map.get("hex") + + %Transaction.PSBT.Input{ + input + | non_witness_utxo: prev_output, + # The script must be given from out source + redeem_script: :script_from_wallet + } + end + end) + + %__MODULE__{psbt | psbt_inputs: psbt_inputs} + end + + # Replace with the keypair + def sign_input( + %__MODULE__{unsigned_transaction: tx, psbt_inputs: psbt_inputs}, + input_index, + %PrivateKey{}, + %BIP32.Xpub{ + public_key: public_key + } + ) do + psbt_input = Enum.at(psbt_inputs, input_index) + + script_type = + cond do + psbt_input.witness_utxo != nil and psbt_input.witness_script != nil -> :p2wsh + psbt_input.witness_utxo != nil -> :p2wpkh + psbt_input.non_witness_utxo != nil and psbt_input.redeem_script != nil -> :p2sh + psbt_input.non_witness_utxo != nil -> :p2pkh + true -> :unknown + end + + z = + case script_type do + # For segwit inputs, we use the BIP143 + :p2pwkh -> + # Script code is a reconstructed P2PKH script + pubkey_hash = public_key |> CryptoUtils.hash160() + script_code = Script.p2pkh_script(pubkey_hash) + + Transaction.Segwit.BIP143.sig_hash( + tx, + input_index, + script_code, + psbt_input.witness_utxo.value + ) + + :p2wsh -> + # For P2WSH the scriptCode is the witnessScript itself + script_code = psbt_input.witness_script + + Transaction.Segwit.BIP143.sig_hash( + tx, + input_index, + script_code, + psbt_input.witness_utxo.value + ) + + :p2pkh -> + script_pub_key = psbt_input.non_witness_utxo + Transaction.sig_hash(tx, input_index) + + :p2sh -> + script_code = psbt_input.redeem_script + Transaction.sig_hash(tx, input_index, script_code) + + _ -> + {:error, "Unsupported input type for signing"} + end + + der = PrivateKey.sign(private_key, z) |> Signature.der() + # The signature is a combination of the DER signature and the hash type + sig = der <> :binary.encode_unsigned(@sighash_all, :big) + sec = private_key.point |> Secp256Point.compressed_sec() + new_partial_sig = %{pubkey: sec, signature: sig} + end + + def combine(psbts) when is_list(psbts) do + {:ok} = Helpers.validate_list_of_structs(psbts, __MODULE__) + end + + def finalize(%__MODULE__{}) do + end + + def extract_transaction(%__MODULE__{}) do + end end diff --git a/lib/transaction/psbt/psbt_input.ex b/lib/transaction/psbt/psbt_input.ex new file mode 100644 index 0000000..3e3a518 --- /dev/null +++ b/lib/transaction/psbt/psbt_input.ex @@ -0,0 +1,19 @@ +defmodule Transaction.PSBT.Input do + @doc """ + 1. Utxo info + - `PSBT_IN_NON_WITNESS_UTXO`, for older, non-segwit inputs, the entire previous transaction + - `PSBT_IN_WITNESS_UTXO`, only the specific utxo from the prev tx is needed + 2. Script information, signer needs to know the rules for unlocking the funds it's being asked to spend. + - `PSBT_IN_REDEEM_SCRIPT` - spending P2SH you must provide this script + - `PSBT_IN_WITNESS_SCRIPT` - same concept but for P2WSH + 3. Key derivation, wallet needs to know which key to use + - `PSBT_IN_BIP32_DERIVATION` - path to the key + """ + defstruct [ + :non_witness_utxo, + :witness_utxo, + :redeem_script, + :witness_script, + :bip32_derivation + ] +end diff --git a/lib/transaction/segwit/bip143.ex b/lib/transaction/segwit/bip143.ex index e43ddb3..c886570 100644 --- a/lib/transaction/segwit/bip143.ex +++ b/lib/transaction/segwit/bip143.ex @@ -48,7 +48,7 @@ defmodule Transaction.Segwit.BIP143 do - A 32-byte binary representing the signature hash to be signed. - Or `{:error, reason}` if an unsupported `sighash_type` is provided (based on current implementation). """ - def sig_hash_bip143_p2wpkh( + def sig_hash( %Transaction{ version: version, tx_ins: inputs, @@ -56,9 +56,11 @@ defmodule Transaction.Segwit.BIP143 do locktime: locktime }, input_index, - public_key_hash, + script, + amount, sighash_type \\ @sighash_all - ) do + ) + when is_integer(input_index) and is_struct(script, Script) and is_integer(amount) do case sighash_type do @sighash_all -> version_le = MathUtils.int_to_little_endian(version, 4) @@ -72,10 +74,10 @@ defmodule Transaction.Segwit.BIP143 do prev_index_le = MathUtils.int_to_little_endian(prev_index, 4) outpoint = prev_tx_le <> prev_index_le - script_serialized = Script.p2pkh_script(public_key_hash) |> Script.serialize() - - amount = Input.value(input) |> MathUtils.int_to_little_endian(8) + script_serialized = script |> Script.serialize() + # amount = Input.value(input) |> MathUtils.int_to_little_endian(8) + amount = amount |> MathUtils.int_to_little_endian(8) n_sequence = sequence |> MathUtils.int_to_little_endian(4) hash_outputs = calculate_hash_outputs(outputs) locktime_le = locktime |> MathUtils.int_to_little_endian(4) diff --git a/lib/transaction/transaction.ex b/lib/transaction/transaction.ex index 2d07108..5f1d04b 100644 --- a/lib/transaction/transaction.ex +++ b/lib/transaction/transaction.ex @@ -217,7 +217,7 @@ defmodule Transaction do - integer() if valid. - {:error, reason} if invalid. """ - def fee(%{tx_ins: inputs, tx_outs: outputs}) do + def fee(%__MODULE__{tx_ins: inputs, tx_outs: outputs}) do input_sum = Enum.reduce(inputs, 0, fn input, acc -> acc + Input.value(input) end) output_sum = Enum.reduce(outputs, 0, fn output, acc -> acc + output.amount end) @@ -330,7 +330,7 @@ defmodule Transaction do z = case input.type do :segwit -> - BIP143.sig_hash_bip143_p2wpkh(tx, input_index, script_pubkey) + BIP143.sig_hash(tx, input_index, script_pubkey) :legacy -> sig_hash(tx, input_index) @@ -517,7 +517,7 @@ defmodule Transaction do z = case current_input.type do :segwit -> - BIP143.sig_hash_bip143_p2wpkh( + BIP143.sig_hash( tx, input_index, sender_pubkey |> CryptoUtils.hash160() diff --git a/lib/vm/script.ex b/lib/vm/script.ex index 729c9c1..b71ce0f 100644 --- a/lib/vm/script.ex +++ b/lib/vm/script.ex @@ -200,14 +200,14 @@ defmodule Script do # Pay to witness public key hash # Same as Pay to Public Key Hash - def p2wpkh(public_key_hash) when is_binary(public_key_hash) do + def p2wpkh(public_key_hash) when is_binary(public_key_hash) and byte_size(public_key_hash) do public_key_hash = case Helpers.is_hex_string?(public_key_hash) do true -> public_key_hash |> Base.decode16!(case: :mixed) false -> public_key_hash end - Script.new([0x76, 0xA9, public_key_hash, 0x88, 0xAC]) + Script.new([0x00, public_key_hash]) end # Takes a byte sequence hash160 and returns a p2pkh address string @@ -235,4 +235,45 @@ defmodule Script do Base58.encode_base58_checksum(prefix <> hash) end + + # Segwit v1 (Taproot/P2TR) 34 bytes length + def identify_script_type(<<0x51, 0x20, _::binary-size(32)>>) do + :p2tr + end + + # Segwit v0 P2WPKH + def identify_script_type(<<0x00, 0x14, _::binary-size(20)>>) do + :p2wpkh + end + + # Segwit v0 P2PWSH + def identify_script_type(<<0x00, 0x20, _::binary-size(32)>>) do + :p2wsh + end + + # P2SH + def identify_script_type(<<0xA9, _::binary-size(22)>> = decoded_script) do + # Last byte is `OP_EQUAL` + <<0x87, _::binary>> = Binary.Common.reverse_binary(decoded_script) + :p2sh + end + + # p2pkh + def identify_script_type(<<0x76, 0xA9, _::binary-size(22)>> = decoded_script) do + # Last bytes are `OP_CHECKSIG`, `OP_EQUALVERIFY` + <<0x76, 0x88, _::binary>> = Binary.Common.reverse_binary(decoded_script) + :p2pkh + end + + # p2pk + def identify_script_type(<<0x21, _::binary-size(34)>> = decoded_script) do + <<0xAC, _::binary>> = Binary.Common.reverse_binary(decoded_script) + :p2pk + end + + # p2pk + def identify_script_type(<<0x41, _::binary-size(66)>> = decoded_script) do + <<0xAC, _::binary>> = Binary.Common.reverse_binary(decoded_script) + :p2pk + end end diff --git a/roadmap.md b/roadmap.md index dd4ffc5..3b1de08 100644 --- a/roadmap.md +++ b/roadmap.md @@ -1,5 +1,7 @@ -## TODO +## Roadmap to-do 1. Psbt 2. Draw lifecycle of transactions -3. Keypair \ No newline at end of file +3. Keypair +4. Multi-sign transaction +5. schnorr_sig \ No newline at end of file diff --git a/test/bips/bip143/bip143_test.exs b/test/bips/bip143/bip143_test.exs index 8bcd586..15b85f9 100644 --- a/test/bips/bip143/bip143_test.exs +++ b/test/bips/bip143/bip143_test.exs @@ -35,7 +35,7 @@ defmodule Transaction.Segwit.BIP143Test do tx = Transaction.new(2, [tx_in], [change_output, target_output], 0) assert "362dbabf30ae67339d703ae801c389b6af64cfddb113e216b3f325fc2d9018a9" == - BIP143.sig_hash_bip143_p2wpkh(tx, 0, sender_pubkey_hash) + BIP143.sig_hash(tx, 0, sender_pubkey_hash) |> Base.encode16(case: :lower) end end diff --git a/test/transaction/psbt_test.exs b/test/transaction/psbt_test.exs new file mode 100644 index 0000000..c7ee0d6 --- /dev/null +++ b/test/transaction/psbt_test.exs @@ -0,0 +1,58 @@ +defmodule Transaction.PSBT.Test do + require Logger + use ExUnit.Case + require IEx + alias Sdk.Wallet + alias Transaction.Input + alias Transaction.Output + alias Transaction + alias Sdk.RpcClient + alias Transaction.PSBT + + @tag :in_progress + test "test psbt creation Segwit" do + seed_wallet = + Wallet.from_seed("4ac2c2d606a110b150ff849fef221cc71643a03517ca7fda185a8ca1d410c7d4") + + derive_path = "m/84'/1'/0'/0/0" + sender = Wallet.derive_private_key(seed_wallet, derive_path) + + {:ok, sender_address} = Wallet.generate_address(sender, type: :bech32, network: :testnet) + receiver_address = "tb1qlj64u6fqutr0xue85kl55fx0gt4m4urun25p7q" + + # ------------INPUT---------------- + prev_tx = + "3ac969f19bdf2d3bb2c216cecbe3756fa2ca36f126ca2df21cb2d772f2d5e4db" + |> Base.decode16!(case: :lower) + + change_amount = 2000 + target_amount = 1000 + + prev_index = 0 + tx_in = Input.new(prev_tx, prev_index, Script.new([]), 0xFFFFFFFF, :segwit) + + # ------------CHANGE---------------- + {:ok, {:bech32, _, changed_address_decoded}} = Bech32.decode(sender_address) + [witness_version | program_5bit] = changed_address_decoded + {:ok, witness_program} = Bech32.convert_bits(program_5bit, 5, 8, false) + change_script = Script.new([witness_version, witness_program |> :erlang.list_to_binary()]) + change_amount = trunc(change_amount) + change_output = Output.new(change_amount, change_script) + + # ------------TARGET---------------- + {:ok, {:bech32, _, received_address_decoded}} = Bech32.decode(receiver_address) + [witness_version | program_5bit] = changed_address_decoded + {:ok, witness_program} = Bech32.convert_bits(program_5bit, 5, 8, false) + target_amount = trunc(target_amount) + target_script = Script.new([witness_version, witness_program |> :erlang.list_to_binary()]) + target_output = Output.new(target_amount, target_script) + + # ------------PSBT---------------- + tx = + Transaction.new(2, [tx_in], [change_output, target_output], 0) + + psbt = Transaction.PSBT.new(tx, derive_path) + updated_psbt = Transaction.PSBT.add_sign_info(psbt) + Logger.info("Updated #{inspect(updated_psbt)}") + end +end