Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion lib/transaction/input.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand All @@ -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.

Expand Down
164 changes: 164 additions & 0 deletions lib/transaction/psbt.ex
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions lib/transaction/psbt/psbt_input.ex
Original file line number Diff line number Diff line change
@@ -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
14 changes: 8 additions & 6 deletions lib/transaction/segwit/bip143.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,19 @@ 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,
tx_outs: outputs,
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)
Expand All @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions lib/transaction/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
45 changes: 43 additions & 2 deletions lib/vm/script.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
6 changes: 4 additions & 2 deletions roadmap.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## TODO
## Roadmap to-do

1. Psbt
2. Draw lifecycle of transactions
3. Keypair
3. Keypair
4. Multi-sign transaction
5. schnorr_sig
2 changes: 1 addition & 1 deletion test/bips/bip143/bip143_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading