defmodule BSV.MerkleProof do
@moduledoc """
The MerkleProof module implements the [BSV TCS Merkle proof standard](https://tsc.bitcoinassociation.net/standards/merkle-proof-standardised-format/).
Merkle proofs are fundamental to the Simplified Payment Verification (SPV)
model that underpins bitcoin scaling. Assuming we have stored block headers
from the blockchain, given a transaction and `t:BSV.MerkleProof.t/0`, we can
verify the transaction is contained in a block without downloading the entire
block.
The TSC Merkle proof standard describes a way of serialising a Merkle proof
in a binary or json format, so network participants can share the proofs in
a standardised format.
"""
use Bitwise
alias BSV.{BlockHeader, Hash, Serializable, Tx, VarInt}
import BSV.Util, only: [decode: 2, encode: 2]
defstruct flags: 0, index: nil, subject: nil, target: nil, nodes: []
@typedoc "Merkle proof struct"
@type t() :: %__MODULE__{
flags: integer(),
index: non_neg_integer(),
subject: Tx.t() | Tx.hash(),
target: BlockHeader.t() | binary(),
nodes: list(Tx.hash())
}
defguard is_txid?(flags) when (flags &&& 0x01) == 0
defguard is_tx?(flags) when (flags &&& 0x01) == 1
defguard targets_block_hash?(flags) when (flags &&& (0x04 ||| 0x02)) == 0
defguard targets_block_header?(flags) when (flags &&& (0x04 ||| 0x02)) == 2
defguard targets_merkle_root?(flags) when (flags &&& (0x04 ||| 0x02)) == 4
@doc """
Parses the given binary into a `t:BSV.MerkleProof.t/0`.
Returns the result in an `:ok` / `:error` tuple pair.
## Options
The accepted options are:
* `:encoding` - Optionally decode the binary with either the `:base64` or `:hex` encoding scheme.
"""
@spec from_binary(binary(), keyword()) :: {:ok, t()} | {:error, term()}
def from_binary(data, opts \\ []) when is_binary(data) do
encoding = Keyword.get(opts, :encoding)
with {:ok, data} <- decode(data, encoding),
{:ok, merkle_proof, _rest} <- Serializable.parse(%__MODULE__{}, data)
do
{:ok, merkle_proof}
end
end
@doc """
Parses the given binary into a `t:BSV.MerkleProof.t/0`.
As `from_binary/2` but returns the result or raises an exception.
"""
@spec from_binary!(binary(), keyword()) :: t()
def from_binary!(data, opts \\ []) when is_binary(data) do
case from_binary(data, opts) do
{:ok, merkle_proof} ->
merkle_proof
{:error, error} ->
raise BSV.DecodeError, error
end
end
@doc """
Calculates and returns the result of hashing all of the transaction hashes
contained in the Merkle proof into a tree-like structure known as a Merkle tree.
"""
@spec calc_merkle_root(t()) :: binary()
def calc_merkle_root(%__MODULE__{index: index, subject: %Tx{} = tx, nodes: nodes}),
do: hash_nodes(Tx.get_hash(tx), index, nodes)
def calc_merkle_root(%__MODULE__{index: index, subject: tx_hash, nodes: nodes})
when is_binary(tx_hash),
do: hash_nodes(tx_hash, index, nodes)
# Iterates over and hashes the tx hashes
defp hash_nodes(hash, _index, []), do: hash
defp hash_nodes(hash, index, ["*" | rest]) when rem(index, 2) == 0,
do: hash_nodes(hash, index, [hash | rest])
defp hash_nodes(_hash, index, ["*" | _rest]) when rem(index, 2) == 1,
do: raise "invalid nodes"
defp hash_nodes(hash, index, [node | rest]) when rem(index, 2) == 0 do
Hash.sha256_sha256(hash <> node)
|> hash_nodes(floor(index / 2), rest)
end
defp hash_nodes(hash, index, [node | rest]) when rem(index, 2) == 1 do
Hash.sha256_sha256(node <> hash)
|> hash_nodes(floor(index / 2), rest)
end
@doc """
Serialises the given `t:BSV.MerkleProof.t/0` into a binary.
## Options
The accepted options are:
* `:encoding` - Optionally encode the binary with either the `:base64` or `:hex` encoding scheme.
"""
@spec to_binary(t()) :: binary()
def to_binary(%__MODULE__{} = merkle_proof, opts \\ []) do
encoding = Keyword.get(opts, :encoding)
merkle_proof
|> Serializable.serialize()
|> encode(encoding)
end
defimpl Serializable do
defguard is_txid?(flags) when (flags &&& 0x01) == 0
defguard is_tx?(flags) when (flags &&& 0x01) == 1
defguard targets_block_hash?(flags) when (flags &&& (0x04 ||| 0x02)) == 0
defguard targets_block_header?(flags) when (flags &&& (0x04 ||| 0x02)) == 2
defguard targets_merkle_root?(flags) when (flags &&& (0x04 ||| 0x02)) == 4
@impl true
def parse(merkle_proof, data) do
with <<flags::integer, data::binary>> <- data,
{:ok, index, data} <- VarInt.parse_int(data),
{:ok, subject, data} <- parse_subject(data, flags),
{:ok, target, data} <- parse_target(data, flags),
{:ok, nodes_num, data} <- VarInt.parse_int(data),
{:ok, nodes, rest} <- parse_nodes(data, nodes_num)
do
{:ok, struct(merkle_proof, [
flags: flags,
index: index,
subject: subject,
target: target,
nodes: nodes
]), rest}
else
{:error, error} ->
{:error, error}
_data ->
{:error, :invalid_merkle_proof}
end
end
@impl true
def serialize(%{flags: flags, nodes: nodes} = merkle_proof) do
index = VarInt.encode(merkle_proof.index)
tx_or_id = serialize_subject(merkle_proof.subject)
target = serialize_target(merkle_proof.target)
nodes_data = Enum.reduce(nodes, VarInt.encode(length(nodes)), &serialize_node/2)
<<
flags::integer,
index::binary,
tx_or_id::binary,
target::binary,
nodes_data::binary
>>
end
# Parses the tx or tx hash as per the given flags
defp parse_subject(data, flags) when is_tx?(flags) do
with {:ok, rawtx, data} <- VarInt.parse_data(data),
{:ok, tx} <- Tx.from_binary(rawtx)
do
{:ok, tx, data}
end
end
defp parse_subject(data, flags) when is_txid?(flags) do
with <<txid::binary-size(32), data::binary>> <- data do
{:ok, txid, data}
end
end
# # Parses the target as per the given flags
defp parse_target(data, flags) when targets_block_header?(flags) do
with {:ok, block_header, data} <- Serializable.parse(%BlockHeader{}, data) do
{:ok, block_header, data}
end
end
defp parse_target(data, _flags) do
<<hash::binary-size(32), data::binary>> = data
{:ok, hash, data}
end
# Parses the list of nodes
defp parse_nodes(data, num, nodes \\ [])
defp parse_nodes(data, num, nodes) when length(nodes) == num,
do: {:ok, Enum.reverse(nodes), data}
defp parse_nodes(<<0, hash::binary-size(32), data::binary>>, num, nodes),
do: parse_nodes(data, num, [hash | nodes])
defp parse_nodes(<<1, data::binary>>, num, nodes),
do: parse_nodes(data, num, ["*" | nodes])
# Serialised the tx or tx hash
defp serialize_subject(%Tx{} = tx) do
tx
|> Tx.to_binary()
|> VarInt.encode_binary()
end
defp serialize_subject(tx_hash), do: tx_hash
# Serialise the target header or hash
defp serialize_target(%BlockHeader{} = header),
do: BlockHeader.to_binary(header)
defp serialize_target(target), do: target
# Serialises the lists of nodes
defp serialize_node("*", data), do: data <> <<1>>
defp serialize_node(<<hash::binary-size(32)>>, data),
do: data <> <<0, hash::binary>>
end
end