lib/script/analyzer.ex

defmodule BitcoinLib.Script.Analyzer do
  @moduledoc """
  Based on that table https://i.stack.imgur.com/iXfVX.png

  useful links:
  - https://learnmeabitcoin.com/technical/scriptPubKey#standard-scripts
  """

  alias BitcoinLib.Script.Opcodes.{BitwiseLogic, Constants, Crypto, Data, Stack}

  @pub_key_hash_size 20
  @uncompressed_pub_key_size 65
  @key_hash_size 20
  @script_hash_size 32

  @zero Constants.Zero.v()
  @one Constants.One.v()
  @dup Stack.Dup.v()
  @equal BitwiseLogic.Equal.v()
  @equal_verify BitwiseLogic.EqualVerify.v()
  @hash160 Crypto.Hash160.v()
  @check_sig Crypto.CheckSig.v()

  @doc """
  Identify a script in either binary or opcode list forms

  ## Examples
      iex> <<0x4104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC::536>>
      ...> |> BitcoinLib.Script.Analyzer.identify()
      {:p2pk, <<0x04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f::520>>}
  """

  # 41 <<_pub_key::520>> ac
  @spec identify(binary() | list()) :: {:p2pk | :p2pkh | :p2wpkh | :p2sh | :p2wsh, bitstring()}
  def identify(<<@uncompressed_pub_key_size::8, pub_key::bitstring-520, @check_sig::8>>),
    do: {:p2pk, pub_key}

  def identify([%Data{value: <<pub_key::bitstring-520>>}, %Crypto.CheckSig{}]),
    do: {:p2pk, pub_key}

  # address example: 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2
  # 76 a9 14 <<_pub_key_hash::160>> 88 ac
  def identify(
        <<@dup::8, @hash160::8, @pub_key_hash_size::8, pub_key_hash::bitstring-160,
          @equal_verify::8, @check_sig::8>>
      ),
      do: {:p2pkh, pub_key_hash}

  def identify([
        %BitcoinLib.Script.Opcodes.Stack.Dup{},
        %BitcoinLib.Script.Opcodes.Crypto.Hash160{},
        %BitcoinLib.Script.Opcodes.Data{value: <<pub_key_hash::bitstring-160>>},
        %BitcoinLib.Script.Opcodes.BitwiseLogic.EqualVerify{},
        %BitcoinLib.Script.Opcodes.Crypto.CheckSig{}
      ]),
      do: {:p2pkh, pub_key_hash}

  # address example: 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy
  # a9 14 <<_script_hash::160>> 87
  def identify(<<@hash160::8, @pub_key_hash_size::8, script_hash::bitstring-160, @equal>>),
    do: {:p2sh, script_hash}

  def identify([
        %BitcoinLib.Script.Opcodes.Crypto.Hash160{},
        %BitcoinLib.Script.Opcodes.Data{value: <<script_hash::bitstring-160>>},
        %BitcoinLib.Script.Opcodes.BitwiseLogic.Equal{}
      ]),
      do: {:p2sh, script_hash}

  # see https://bitcoincore.org/en/segwit_wallet_dev/#native-pay-to-witness-public-key-hash-p2wpkh
  # address example: bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq
  # 00 14 <<_witness_script_hash::256>>
  def identify(<<@zero::8, @key_hash_size::8, key_hash::160>>), do: {:p2wpkh, key_hash}

  def identify([
        %BitcoinLib.Script.Opcodes.Constants.Zero{},
        %BitcoinLib.Script.Opcodes.Data{value: <<key_hash::bitstring-160>>}
      ]),
      do: {:p2wpkh, key_hash}

  # see https://bitcoincore.org/en/segwit_wallet_dev/#native-pay-to-witness-script-hash-p2wsh
  # address example: bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq
  # 00 20 <<_witness_script_hash::256>>
  def identify(<<@zero::8, @script_hash_size::8, script_hash::bitstring-256>>),
    do: {:p2wsh, script_hash}

  def identify([
        %BitcoinLib.Script.Opcodes.Constants.Zero{},
        %BitcoinLib.Script.Opcodes.Data{value: <<script_hash::bitstring-256>>}
      ]),
      do: {:p2wsh, script_hash}

  # see https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#script-validation-rules
  # address example: bc1pmzfrwwndsqmk5yh69yjr5lfgfg4ev8c0tsc06e
  # 01 20 <<_witness_script_hash::256>>
  def identify(<<@one::8, @script_hash_size::8, script_hash::bitstring-256>>),
    do: {:p2tr, script_hash}

  def identify([
        %BitcoinLib.Script.Opcodes.Constants.One{},
        %BitcoinLib.Script.Opcodes.Data{value: <<script_hash::bitstring-256>>}
      ]),
      do: {:p2tr, script_hash}

  def identify(script) when is_bitstring(script), do: :unknown
end