Skip to content

Commit

Permalink
segwit transaction addresses now leaving out witnesses
Browse files Browse the repository at this point in the history
  • Loading branch information
RooSoft committed Nov 13, 2022
1 parent 4d181cf commit 706af33
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 41 deletions.
2 changes: 1 addition & 1 deletion lib/key/private_key.ex
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ defmodule BitcoinLib.Key.PrivateKey do
"""
@spec from_derivation_path(%PrivateKey{}, %DerivationPath{}) :: {:ok, %PrivateKey{}}
def from_derivation_path(%PrivateKey{} = private_key, %DerivationPath{} = derivation_path) do
with {:ok, private_key} = ChildFromDerivationPath.get(private_key, derivation_path) do
with {:ok, private_key} <- ChildFromDerivationPath.get(private_key, derivation_path) do
{:ok, add_fingerprint(private_key)}
else
{:error, message} -> {:error, message}
Expand Down
8 changes: 8 additions & 0 deletions lib/signing/psbt/global/unsigned_tx.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule BitcoinLib.Signing.Psbt.Global.UnsignedTx do
keypair
|> validate_keypair()
|> decode_transaction()
|> validate_witness()
end

defp validate_keypair(keypair) do
Expand Down Expand Up @@ -34,4 +35,11 @@ defmodule BitcoinLib.Signing.Psbt.Global.UnsignedTx do
end
end
end

defp validate_witness({:error, message}), do: {:error, message}

defp validate_witness({:ok, %Transaction{segwit?: true, witness: []}}),
do: {:error, "unsigned tx serialized with witness serialization format"}

defp validate_witness({:ok, transaction}), do: {:ok, transaction}
end
108 changes: 92 additions & 16 deletions lib/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule BitcoinLib.Transaction do
@moduledoc """
Based on https://learnmeabitcoin.com/technical/transaction-data#fields
"""
defstruct [:version, :id, :inputs, :outputs, :locktime, witness: []]
defstruct [:version, :id, :inputs, :outputs, :locktime, :segwit?, witness: []]

alias BitcoinLib.Crypto
alias BitcoinLib.Key.PrivateKey
Expand Down Expand Up @@ -40,7 +40,8 @@ defmodule BitcoinLib.Transaction do
]
}
],
locktime: 0
locktime: 0,
segwit?: false
}
}
"""
Expand Down Expand Up @@ -82,14 +83,18 @@ defmodule BitcoinLib.Transaction do
]
}
],
locktime: 0
locktime: 0,
segwit?: false
}
}
"""
@spec decode(bitstring()) :: {:ok, %Transaction{}} | {:error, binary()}
def decode(encoded_transaction) do
encoded_transaction
|> Decoder.to_struct()
with {:ok, transaction} <- Decoder.to_struct(encoded_transaction) do
{:ok, add_id(transaction)}
else
{:error, message} -> {:error, message}
end
end

@doc """
Expand Down Expand Up @@ -186,31 +191,102 @@ defmodule BitcoinLib.Transaction do
"""
@spec add_id(%Transaction{}) :: %Transaction{}
def add_id(%Transaction{} = transaction) do
id =
transaction
|> encode()
|> id_from_encoded_transaction()
Map.put(transaction, :id, to_id(transaction))
end

@doc """
Hashes the transaction's id
## Examples
iex> %BitcoinLib.Transaction{
...> version: 2,
...> inputs: [
...> %BitcoinLib.Transaction.Input{
...> txid: "5a957f4bff6d23140eb7e9b6fcedd41d3febf1e145d37519c593c939789a49af",
...> vout: 0,
...> script_sig: [],
...> sequence: 4294967293
...> }
...> ],
...> outputs: [
...> %BitcoinLib.Transaction.Output{
...> value: 1303734,
...> script_pub_key: [%BitcoinLib.Script.Opcodes.Stack.Dup{},
...> %BitcoinLib.Script.Opcodes.Crypto.Hash160{},
...> %BitcoinLib.Script.Opcodes.Data{value: <<0x05e17c02fb238ca0779b3533271ebe916b01bcab::160>>},
...> %BitcoinLib.Script.Opcodes.BitwiseLogic.EqualVerify{},
...> %BitcoinLib.Script.Opcodes.Crypto.CheckSig{script: <<0x76a91405e17c02fb238ca0779b3533271ebe916b01bcab88ac::200>>}]
...> }
...> ],
...> locktime: 2378041,
...> witness: []
...> }
...> |> BitcoinLib.Transaction.to_id()
"b62e9d36389427d39e5d438a05045c23d1938e4242661c5fe2ad87c46337b091"
"""
@spec to_id(%Transaction{}) :: binary()
def to_id(%Transaction{} = transaction) do
transaction
|> Map.put(:id, id)
|> Encoder.for_txid()
|> Crypto.double_sha256()
|> Crypto.Bitstring.reverse()
|> Binary.to_hex()
end

@doc """
Computes a transaction id from an encoded transaction in bitstring format
## Examples
Based on https://bitcoin.stackexchange.com/a/2233/101041
A random transaction
iex> "010000000001016dc77969a38fcd7ade1524658b7cf04430eefcbed5ed18e8907f938932ee6b360100000000f5ffffff02404b4c000000000017a9140403fb8f4a93093430e338f346efe40774b45f95873c75360000000000160014a79e4076fca80a3742536d1d5cd364d5cfe0eeaf02483045022100ff621967f1a0c231dbdaf500a83079d4a5c32e64e2d6b3809ff47ee25c7750ca0220326d95db84811e391af38aded348c8f26cdf793be3ed19ae1185cc03aef42fa8012102b57b7b110b4c00ae0f2ed1594174ebcc2f4077667867ba2cb514bbf59ae8868500000000"
...> |> Binary.from_hex()
...> |> BitcoinLib.Transaction.id_from_encoded_transaction()
{:ok, "651560f701575fb0b15780b6ba771bf6096b1d6b6c6202b3d79915ebbac5ebff"}
The transaction in the genesis block
iex> "01000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000"
...> |> Binary.from_hex()
...> |> BitcoinLib.Transaction.id_from_encoded_transaction()
{:ok, "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"}
From real world ElectrumClient example
iex> "0200000000010135b0b1e0f2c3c5e2e4b96b501893b3b8f649701faf97e3b51ca60e672ae1b6970100000000fdffffff03881300000000000016001413bfff2d6dd02b8837f156c6f9fe0ea7363df79570170000000000002200206ff04018aff3bd320c89e2e8c9d4274e6b0e780975cd364810239ecc7bd8138a60220000000000001600147801fc793b86a3d81801588ffb4f3c4b11f704090247304402201530b7cf8beda625ad29280ef342c22518ea7df543060cb9966a81cde8b18c6f02205c23cf17627815805bbc5814ece5ea1f7f89e16ac373b19de9f40944588de1220121033c3fdfa1a29b984e83be8fc5b952558a0837734a0df0b663764e6c665c3e9e5b1ea40b00"
...> |> Binary.from_hex()
...> |> BitcoinLib.Transaction.id_from_encoded_transaction()
{:ok, "f318dd60cc36fb1238e39abf697b471881ed811f2f66b0bdeed15e29d3032c76"}
From https://learnmeabitcoin.com/technical/txid
iex> "01000000000101c6548bbe4e6a1bc65c2b6546b15c5bcb17682d0418d866632ec5328f0e6c46420000000000ffffffff02dcc5630000000000160014349a727852238d54d2d98662b72ba19d48dafea1c01d13000000000017a9140efd4916baef33b979989542f21a4e182a992bf6870247304402207b6a1d52a7bcfc87b776f3946a5c57fc686828131f87603f0e87b9c5cddfe4d90220405070bac780bf313df38c174313a48437e67eedf3234ef0770760d005f9182e012102b173048a5b94939f9486826d41b87add8a20910f7ff8acadabb5f89edbd0da9b00000000"
...> |> Binary.from_hex()
...> |> BitcoinLib.Transaction.id_from_encoded_transaction()
{:ok, "c7fbe3f35f6060555279ba455c1c4d419ceaaa1a5fe68940913a431296d37f5b"}
iex> <<0x0100000001c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd3704000000004847304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901ffffffff0200ca9a3b00000000434104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac00286bee0000000043410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac00000000::2200>>
...> |> BitcoinLib.Transaction.id_from_encoded_transaction()
{:ok, "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"}
iex> "02000000000101fabb6b29687f2047622f546cc97aedce4407da5d96b7f7d72e53c7fdbae9b5930100000000fdffffff02e63be7390000000017a9140e825b4005779df0808738f3beabcee7abe5b46687436b1200000000001976a9146b426bed96ace2804e99f29304a9eb1b1260ea6688ac0247304402207e39cfe660bc35ab24229a820d847aa97b3dfd96623cc5b6cc679d11215d2b3402201a7a2ddda43b8698968e38fc44a967a86d0e088103b24695e79c091c6139754b012103ee237db683c637ddf2bd51bc39555084dcc402599625bb1a2ec02fd2a1a79ddeaba40b00"
...> |> Binary.from_hex()
...> |> BitcoinLib.Transaction.id_from_encoded_transaction()
{:ok, "6991e107b306fa1629f3b41054ec856dbcc379aac28cc8c84102163d03239f88"}
Another one based on https://bitcoin.stackexchange.com/a/2233/101041
iex> <<0x01000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000::1632>>
...> |> BitcoinLib.Transaction.id_from_encoded_transaction()
"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
{:ok, "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"}
"""
@spec id_from_encoded_transaction(bitstring()) :: binary()
def id_from_encoded_transaction(encoded_transaction) do
encoded_transaction
|> Crypto.double_sha256()
|> Crypto.Bitstring.reverse()
|> Binary.to_hex()
with {:ok, transaction} <- Decoder.to_struct(encoded_transaction) do
{:ok, to_id(transaction)}
else
{:error, message} -> {:error, message}
end
end

@doc """
Expand Down
36 changes: 14 additions & 22 deletions lib/transaction/decoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ defmodule BitcoinLib.Transaction.Decoder do
:ok,
%BitcoinLib.Transaction{
version: 1,
id: "b1caa607a324ed9132c3d894d2cf7a22d59cdb4022bd8827c49e2f0d8ca018c6",
inputs: [
%BitcoinLib.Transaction.Input{
txid: "3f4fa19803dec4d6a84fae3821da7ac7577080ef75451294e71f9b20e0ab1e7b",
Expand All @@ -42,24 +41,23 @@ defmodule BitcoinLib.Transaction.Decoder do
]
}
],
locktime: 1170
locktime: 1170,
segwit?: false
}
}
"""
@spec to_struct(bitstring()) :: {:ok, %Transaction{}} | {:error, binary()}
def to_struct(encoded_transaction) do
# see https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki#hashes
Transaction.id_from_encoded_transaction(encoded_transaction)
|> version_specific_extract(encoded_transaction)
version_specific_extract(encoded_transaction)
end

# Extracts a witness transaction
defp version_specific_extract(
id,
<<version::little-32, @marker::8, @flag::8, remaining::bitstring>>
) do
result =
%{remaining: remaining, witness_signature?: true}
%{remaining: remaining}
|> extract_input_count
|> extract_inputs
|> extract_output_count
Expand All @@ -75,20 +73,20 @@ defmodule BitcoinLib.Transaction.Decoder do
%{inputs: inputs, outputs: outputs, witness: witness, locktime: locktime} ->
{:ok,
%Transaction{
id: id,
version: version,
inputs: inputs,
outputs: outputs,
witness: witness,
locktime: locktime
locktime: locktime,
segwit?: true
}}
end
end

# Extracts a non-witness transaction
defp version_specific_extract(id, remaining) do
defp version_specific_extract(remaining) do
result =
%{remaining: remaining, witness_signature?: false}
%{remaining: remaining}
|> extract_version
|> extract_input_count
|> extract_inputs
Expand All @@ -104,11 +102,11 @@ defmodule BitcoinLib.Transaction.Decoder do
%{version: version, inputs: inputs, outputs: outputs, locktime: locktime} ->
{:ok,
%Transaction{
id: id,
version: version,
inputs: inputs,
outputs: outputs,
locktime: locktime
locktime: locktime,
segwit?: false
}}
end
end
Expand Down Expand Up @@ -149,20 +147,14 @@ defmodule BitcoinLib.Transaction.Decoder do

defp extract_witness(%{error: message}), do: %{error: message}

defp extract_witness(%{remaining: remaining, witness_signature?: witness_signature?} = map) do
defp extract_witness(%{remaining: remaining} = map) do
%CompactInteger{value: witness_count, remaining: remaining} =
CompactInteger.extract_from(remaining, :big_endian)

case witness_count == 0 && witness_signature? do
true ->
%{error: "unsigned tx serialized with witness serialization format"}
{witness, remaining} = extract_witness_list([], remaining, witness_count)

false ->
{witness, remaining} = extract_witness_list([], remaining, witness_count)

%{map | remaining: remaining}
|> Map.put(:witness, witness)
end
%{map | remaining: remaining}
|> Map.put(:witness, witness)
end

defp extract_witness_list(witnesses, remaining, 0), do: {witnesses, remaining}
Expand Down
47 changes: 45 additions & 2 deletions lib/transaction/encoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule BitcoinLib.Transaction.Encoder do
Encodes a transaction in binary format from a structure
## Examples
iex> transaction = %BitcoinLib.Transaction{
iex> %BitcoinLib.Transaction{
...> inputs: [
...> %BitcoinLib.Transaction.Input{
...> script_sig: nil,
Expand All @@ -40,7 +40,7 @@ defmodule BitcoinLib.Transaction.Encoder do
...> locktime: 0,
...> version: 1
...> }
...> BitcoinLib.Transaction.Encoder.from_struct(transaction)
...> |> BitcoinLib.Transaction.Encoder.from_struct()
<<1, 0, 0, 0>> <> # version
<<1>> <> # input_count
<<0x449d45bbbfe7fc93bbe649bb7b6106b248a15da5dbd6fdc9bdfc7efede83235e0100000000ffffffff::328>> <> # inputs
Expand All @@ -62,6 +62,49 @@ defmodule BitcoinLib.Transaction.Encoder do
|> Map.get(:encoded)
end

@doc """
Specific encoding meant to generate Transaction ID's
## Examples
iex> %BitcoinLib.Transaction{
...> inputs: [
...> %BitcoinLib.Transaction.Input{
...> script_sig: nil,
...> sequence: 0xFFFFFFFF,
...> txid: "5e2383defe7efcbdc9fdd6dba55da148b206617bbb49e6bb93fce7bfbb459d44",
...> vout: 1
...> }
...> ],
...> outputs: [
...> %BitcoinLib.Transaction.Output{
...> script_pub_key: [
...> %BitcoinLib.Script.Opcodes.Stack.Dup{},
...> %BitcoinLib.Script.Opcodes.Crypto.Hash160{},
...> %BitcoinLib.Script.Opcodes.Data{value: <<0xf86f0bc0a2232970ccdf4569815db500f1268361::160>>},
...> %BitcoinLib.Script.Opcodes.BitwiseLogic.EqualVerify{},
...> %BitcoinLib.Script.Opcodes.Crypto.CheckSig{}
...> ],
...> value: 129000000
...> }
...> ],
...> locktime: 0,
...> version: 1
...> }
...> |> BitcoinLib.Transaction.Encoder.for_txid()
<<0x0100000001449d45bbbfe7fc93bbe649bb7b6106b248a15da5dbd6fdc9bdfc7efede83235e0100000000ffffffff014062b007000000001976a914f86f0bc0a2232970ccdf4569815db500f126836188ac00000000::680>>
"""
def for_txid(%Transaction{} = transaction) do
%{transaction: transaction, encoded: <<>>}
|> append_version
|> append_input_count
|> append_inputs
|> append_output_count
|> append_outputs
|> append_locktime
|> Map.get(:encoded)
end

defp append_version(%{transaction: %Transaction{version: version}, encoded: encoded} = map) do
%{map | encoded: <<encoded::binary, version::little-32>>}
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ defmodule BitcoinLib.Test.Integration.Transactions.DecodeRawTransactionTest do
}
],
locktime: 0,
segwit?: false,
witness: []
}
end
Expand Down

0 comments on commit 706af33

Please sign in to comment.