defmodule Bitcoinex.Transaction do @moduledoc """ Bitcoin on-chain transaction structure. Supports serialization of transactions. """ alias Bitcoinex.Transaction alias Bitcoinex.Transaction.In alias Bitcoinex.Transaction.Out alias Bitcoinex.Transaction.Witness alias Bitcoinex.Utils alias Bitcoinex.Transaction.Utils, as: TxUtils @type t() :: %__MODULE__{ version: non_neg_integer(), inputs: list(In.t()), outputs: list(Out.t()), witnesses: list(Witness.t()), lock_time: non_neg_integer() } defstruct [ :version, :inputs, :outputs, :witnesses, :lock_time ] @doc """ Returns the TxID of the given tranasction. TxID is sha256(sha256(nVersion | txins | txouts | nLockTime)) """ def transaction_id(txn) do legacy_txn = TxUtils.serialize(%{txn | witnesses: []}) Base.encode16( <<:binary.decode_unsigned( Utils.double_sha256(legacy_txn), :big )::little-size(256)>>, case: :lower ) end @doc """ Decodes a transaction in a hex encoded string into binary. """ def decode(tx_hex) when is_binary(tx_hex) do case Base.decode16(tx_hex, case: :lower) do {:ok, tx_bytes} -> case parse(tx_bytes) do {:ok, txn} -> {:ok, txn} :error -> {:error, :parse_error} end :error -> {:error, :decode_error} end end # returns transaction defp parse(<>) do {is_segwit, remaining} = case remaining do <<1::size(16), segwit_remaining::binary>> -> {:segwit, segwit_remaining} _ -> {:not_segwit, remaining} end # Inputs. {in_counter, remaining} = TxUtils.get_counter(remaining) {inputs, remaining} = In.parse_inputs(in_counter, remaining) # Outputs. {out_counter, remaining} = TxUtils.get_counter(remaining) {outputs, remaining} = Out.parse_outputs(out_counter, remaining) # If flag 0001 is present, this indicates an attached segregated witness structure. {witnesses, remaining} = if is_segwit == :segwit do Witness.parse_witness(in_counter, remaining) else {nil, remaining} end <> = remaining if byte_size(remaining) != 0 do :error else {:ok, %Transaction{ version: version, inputs: inputs, outputs: outputs, witnesses: witnesses, lock_time: lock_time }} end end end defmodule Bitcoinex.Transaction.Utils do @moduledoc """ Utilities for when dealing with transaction objects. """ alias Bitcoinex.Transaction alias Bitcoinex.Transaction.In alias Bitcoinex.Transaction.Out alias Bitcoinex.Transaction.Witness @doc """ Returns the Variable Length Integer used in serialization. Reference: https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer """ @spec get_counter(binary) :: {non_neg_integer(), binary()} def get_counter(<>) do case counter do # 0xFD followed by the length as uint16_t 0xFD -> <> = vec {len, vec} # 0xFE followed by the length as uint32_t 0xFE -> <> = vec {len, vec} # 0xFF followed by the length as uint64_t 0xFF -> <> = vec {len, vec} _ -> {counter, vec} end end @spec serialize(Transaction.t()) :: binary() def serialize(%Transaction{witnesses: witness} = txn) when is_list(witness) and length(witness) > 0 do version = <> marker = <<0x00::big-size(8)>> flag = <<0x01::big-size(8)>> tx_in_count = 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)) outputs = Out.serialize_outputs(txn.outputs) |> :erlang.list_to_binary() witness = Witness.serialize_witness(txn.witnesses) lock_time = <> version <> marker <> flag <> tx_in_count <> inputs <> tx_out_count <> outputs <> witness <> lock_time end def serialize(txn) do version = <> tx_in_count = 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)) 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 @moduledoc """ Witness structure part of an on-chain transaction. """ alias Bitcoinex.Transaction.Witness alias Bitcoinex.Transaction.Utils, as: TxUtils @type t :: %__MODULE__{ txinwitness: list(binary()) } defstruct [ :txinwitness ] @doc """ Wtiness accepts a binary and deserializes it. """ @spec witness(binary) :: t() def witness(witness_bytes) do {stack_size, witness_bytes} = TxUtils.get_counter(witness_bytes) {witness, _} = if stack_size == 0 do {%Witness{txinwitness: []}, witness_bytes} else {stack_items, witness_bytes} = parse_stack(witness_bytes, [], stack_size) {%Witness{txinwitness: stack_items}, witness_bytes} end witness end @spec serialize_witness(list(Witness.t())) :: binary def serialize_witness(witnesses) do serialize_witness(witnesses, <<>>) end defp serialize_witness([], serialized_witnesses), do: serialized_witnesses defp serialize_witness(witnesses, serialized_witnesses) do [witness | witnesses] = witnesses serialized_witness = if Enum.empty?(witness.txinwitness) do <<0x0::big-size(8)>> else stack_len = TxUtils.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)) acc <> item_len <> item end) stack_len <> field end serialize_witness(witnesses, serialized_witnesses <> serialized_witness) end def parse_witness(0, remaining), do: {nil, remaining} def parse_witness(counter, witnesses) do parse(witnesses, [], counter) end defp parse(remaining, witnesses, 0), do: {Enum.reverse(witnesses), remaining} defp parse(remaining, witnesses, count) do {stack_size, remaining} = TxUtils.get_counter(remaining) {witness, remaining} = if stack_size == 0 do {%Witness{txinwitness: 0}, remaining} else {stack_items, remaining} = parse_stack(remaining, [], stack_size) {%Witness{txinwitness: stack_items}, remaining} end parse(remaining, [witness | witnesses], count - 1) end defp parse_stack(remaining, stack_items, 0), do: {Enum.reverse(stack_items), remaining} defp parse_stack(remaining, stack_items, stack_size) do {item_size, remaining} = TxUtils.get_counter(remaining) <> = remaining parse_stack( remaining, [Base.encode16(stack_item, case: :lower) | stack_items], stack_size - 1 ) end end defmodule Bitcoinex.Transaction.In do @moduledoc """ Transaction Input part of an on-chain transaction. """ alias Bitcoinex.Transaction.In alias Bitcoinex.Transaction.Utils, as: TxUtils @type t :: %__MODULE__{ prev_txid: binary(), prev_vout: non_neg_integer(), script_sig: binary(), sequence_no: non_neg_integer() } defstruct [ :prev_txid, :prev_vout, :script_sig, :sequence_no ] @spec serialize_inputs(list(In.t())) :: iolist() def serialize_inputs(inputs) do serialize_input(inputs, []) end defp serialize_input([], serialized_inputs), do: serialized_inputs 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) {:ok, script_sig} = Base.decode16(input.script_sig, case: :lower) script_len = TxUtils.serialize_compact_size_unsigned_int(byte_size(script_sig)) serialized_input = [ prev_txid, <>, script_len, script_sig, <> ] serialize_input(inputs, [serialized_inputs, serialized_input]) end def parse_inputs(counter, inputs) do parse(inputs, [], counter) end defp parse(remaining, inputs, 0), do: {Enum.reverse(inputs), remaining} defp parse( <>, inputs, count ) do {script_len, remaining} = TxUtils.get_counter(remaining) <> = remaining input = %In{ prev_txid: Base.encode16(<<:binary.decode_unsigned(prev_txid, :big)::little-size(256)>>, case: :lower ), prev_vout: prev_vout, script_sig: Base.encode16(script_sig, case: :lower), sequence_no: sequence_no } parse(remaining, [input | inputs], count - 1) end end defmodule Bitcoinex.Transaction.Out do @moduledoc """ Transaction Output part of an on-chain transaction. """ alias Bitcoinex.Transaction.Out alias Bitcoinex.Transaction.Utils, as: TxUtils @type t :: %__MODULE__{ value: non_neg_integer(), script_pub_key: binary() } defstruct [ :value, :script_pub_key ] @spec serialize_outputs(list(Out.t())) :: iolist() def serialize_outputs(outputs) do serialize_output(outputs, []) end defp serialize_output([], serialized_outputs), do: serialized_outputs defp serialize_output(outputs, serialized_outputs) do [output | outputs] = outputs {: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)) serialized_output = [<>, script_len, script_pub_key] serialize_output(outputs, [serialized_outputs, serialized_output]) end def output(out_bytes) do <> = out_bytes {script_len, out_bytes} = TxUtils.get_counter(out_bytes) <> = out_bytes %Out{value: value, script_pub_key: Base.encode16(script_pub_key, case: :lower)} end def parse_outputs(counter, outputs) do parse(outputs, [], counter) end defp parse(remaining, outputs, 0), do: {Enum.reverse(outputs), remaining} defp parse(<>, outputs, count) do {script_len, remaining} = TxUtils.get_counter(remaining) <> = remaining output = %Out{ value: value, script_pub_key: Base.encode16(script_pub_key, case: :lower) } parse(remaining, [output | outputs], count - 1) end end