Move bitcoinex server dependency into repo

This commit is contained in:
Mononaut 2021-12-02 17:13:13 -06:00
parent 3a733e26b9
commit f5e9d46910
38 changed files with 5531 additions and 1 deletions

View File

@ -0,0 +1,2 @@
[
]

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

30
server/bitcoinex/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
bitcoinex-*.tar
# Ignore linter directories
.elixir_ls/
# dialyzer plt
/_plts/*.plt
/_plts/*.plt.hash

View File

@ -0,0 +1,2 @@
elixir 1.10.4-otp-22
erlang 22.3.4.1

115
server/bitcoinex/README.md Normal file
View File

@ -0,0 +1,115 @@
Forked from RiverFinancial/bitcoinex
original README as follows:
![Bitcoinex](https://user-images.githubusercontent.com/8378656/102842648-4f671380-43bc-11eb-8e0d-72c2a107e5ed.png)
# Bitcoinex
Bitcoinex is striving to be the best and most up-to-date Bitcoin Library for Elixir.
## Documentation
Documentation is available on [hexdocs.pm](https://hexdocs.pm/bitcoinex/api-reference.html).
## Current Utilities
* Serialization and validation for Bech32 and Base58.
* Support for standard on-chain scripts (P2PKH..P2WPKH) and Bolt#11 Lightning Invoices.
* Transaction serialization.
* Basic PSBT (BIP174) parsing.
## Usage
With [Hex](https://hex.pm/packages/bitcoinex):
{:bitcoinex, "~> 0.1.0"}
Local:
$ mix deps.get
$ mix compile
## Examples
Decode a Lightning Network invoice:
```elixir
Bitcoinex.LightningNetwork.decode_invoice("lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp")
{:ok,
%Bitcoinex.LightningNetwork.Invoice{
amount_msat: 250000000,
description: "1 cup coffee",
description_hash: nil,
destination: "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad",
expiry: 60,
fallback_address: nil,
min_final_cltv_expiry: 18,
network: :mainnet,
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
route_hints: [],
timestamp: 1496314658
}}
```
Parse a BIP-174 Partially Signed Bitcoin Transaction:
```elixir
Bitcoinex.PSBT.decode("cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=")
{:ok,
%Bitcoinex.PSBT{
global: %Bitcoinex.PSBT.Global{
proprietary: nil,
unsigned_tx: %Bitcoinex.Transaction{...},
inputs: [
%Bitcoinex.PSBT.In{
bip32_derivation: [
%{
derivation: "b4a6ba67000000800000008004000080",
public_key: "03b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46"
},
%{
derivation: "b4a6ba67000000800000008005000080",
public_key: "03de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd"
}
],
final_scriptsig: nil,
final_scriptwitness: nil,
non_witness_utxo: nil,
partial_sig: %{
public_key: "03b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46",
signature: "304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a01"
},
por_commitment: nil,
proprietary: nil,
redeem_script: "0020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681",
sighash_type: nil,
witness_script: "522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae",
witness_utxo: %Bitcoinex.Transaction.Out{
script_pub_key: "a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87",
value: 199909013
}
}
],
outputs: []
}}
```
Handle bitcoin addresses:
```elixir
{:ok, {:mainnet, witness_version, witness_program}} = Bitcoinex.Segwit.decode_address("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
Bitcoinex.Segwit.encode_address(:mainnet, witness_version, witness_program)
{:ok, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"}
```
## Roadmap
Continued support for on-chain and off-chain functionality including:
* Full script support including validation.
* Block serialization.
* Transaction creation.
* Broader BIP support including BIP32.
## Contributing
We have big goals and this library is still in a very early stage. Contributions and comments are very much welcome.

View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# third-party users, it should be done in your "mix.exs" file.
# You can configure your application as:
#
# config :bitcoinex, key: :value
#
# and access this configuration in your application as:
#
# Application.get_env(:bitcoinex, :key)
#
# You can also configure a third-party app:
#
# config :logger, level: :info
#
# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env()}.exs"

View File

@ -0,0 +1,119 @@
defmodule Bitcoinex.Address do
@moduledoc """
Bitcoinex.Address supports Base58 and Bech32 address encoding and validation.
"""
alias Bitcoinex.{Segwit, Base58, Network}
@typedoc """
The address_type describes the address type to use.
Four address types are supported:
* p2pkh: Pay-to-Public-Key-Hash
* p2sh: Pay-to-Script-Hash
* p2wpkh: Pay-to-Witness-Public-Key-Hash
* p2wsh: Pay-To-Witness-Script-Hash
"""
@type address_type :: :p2pkh | :p2sh | :p2wpkh | :p2wsh
@address_types ~w(p2pkh p2sh p2wpkh p2wsh)a
@doc """
Accepts a public key hash, network, and address_type and returns its address.
"""
@spec encode(binary, Bitcoinex.Network.network_name(), address_type) :: String.t()
def encode(pubkey_hash, network_name, :p2pkh) do
network = Network.get_network(network_name)
decimal_prefix = network.p2pkh_version_decimal_prefix
Base58.encode(<<decimal_prefix>> <> pubkey_hash)
end
def encode(script_hash, network_name, :p2sh) do
network = Network.get_network(network_name)
decimal_prefix = network.p2sh_version_decimal_prefix
Base58.encode(<<decimal_prefix>> <> script_hash)
end
@doc """
Checks if the address is valid.
Both encoding and network is checked.
"""
@spec is_valid?(String.t(), Bitcoinex.Network.network_name()) :: boolean
def is_valid?(address, network_name) do
Enum.any?(@address_types, &is_valid?(address, network_name, &1))
end
@doc """
Checks if the address is valid and matches the given address_type.
Both encoding and network is checked.
"""
@spec is_valid?(String.t(), Bitcoinex.Network.network_name(), address_type) :: boolean
def is_valid?(address, network_name, :p2pkh) do
network = apply(Bitcoinex.Network, network_name, [])
is_valid_base58_check_address?(address, network.p2pkh_version_decimal_prefix)
end
def is_valid?(address, network_name, :p2sh) do
network = apply(Bitcoinex.Network, network_name, [])
is_valid_base58_check_address?(address, network.p2sh_version_decimal_prefix)
end
def is_valid?(address, network_name, :p2wpkh) do
case Segwit.decode_address(address) do
{:ok, {^network_name, witness_version, witness_program}}
when witness_version == 0 and length(witness_program) == 20 ->
true
# network is not same as network set in config
{:ok, {_network_name, _, _}} ->
false
{:error, _error} ->
false
end
end
def is_valid?(address, network_name, :p2wsh) do
case Segwit.decode_address(address) do
{:ok, {^network_name, witness_version, witness_program}}
when witness_version == 0 and length(witness_program) == 32 ->
true
# network is not same as network set in config
{:ok, {_network_name, _, _}} ->
false
{:error, _error} ->
false
end
end
@doc """
Returns a list of supported address types.
"""
def supported_address_types() do
@address_types
end
defp is_valid_base58_check_address?(address, valid_prefix) do
case Base58.decode(address) do
{:ok, <<^valid_prefix::8, _::binary>>} ->
true
_ ->
false
end
end
@doc """
Decodes an address and returns the address_type.
"""
@spec decode_type(String.t(), Bitcoinex.Network.network_name()) ::
{:ok, address_type} | {:error, :decode_error}
def decode_type(address, network_name) do
case Enum.find(@address_types, &is_valid?(address, network_name, &1)) do
nil -> {:error, :decode_error}
type -> {:ok, type}
end
end
end

View File

@ -0,0 +1,132 @@
defmodule Bitcoinex.Base58 do
@moduledoc """
Includes Base58 serialization and validation.
Some code is inspired by:
https://github.com/comboy/bitcoin-elixir/blob/develop/lib/bitcoin/base58_check.ex
"""
alias Bitcoinex.Utils
@typedoc """
Base58 encoding is only supported for p2sh and p2pkh address types.
"""
@type address_type :: :p2sh | :p2pkh
@base58_encode_list ~c(123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz)
@base58_decode_map @base58_encode_list |> Enum.with_index() |> Enum.into(%{})
@base58_0 <<?1>>
@type byte_list :: list(byte())
@doc """
Decodes a Base58 encoded string into a byte array and validates checksum.
"""
@spec decode(binary) :: {:ok, binary} | {:error, atom}
def decode(binary)
def decode(""), do: {:ok, ""}
def decode(<<body_and_checksum::binary>>) do
if valid_charset?(body_and_checksum) do
body_and_checksum
|> decode_base!()
|> validate_checksum()
else
{:error, :invalid_characters}
end
end
@doc """
Decodes a Base58 encoded string into a byte array.
"""
@spec decode_base!(binary) :: binary
def decode_base!(binary)
def decode_base!(@base58_0), do: <<0>>
def decode_base!(@base58_0 <> body) when byte_size(body) > 0 do
decode_base!(@base58_0) <> decode_base!(body)
end
def decode_base!(""), do: ""
def decode_base!(bin) do
bin
|> :binary.bin_to_list()
|> Enum.map(&Map.fetch!(@base58_decode_map, &1))
|> Integer.undigits(58)
|> :binary.encode_unsigned()
end
@doc """
Validates a Base58 checksum.
"""
@spec validate_checksum(binary) :: {:ok, binary} | {:error, atom}
def validate_checksum(data) do
[decoded_body, checksum] =
data
|> :binary.bin_to_list()
|> Enum.split(-4)
|> Tuple.to_list()
|> Enum.map(&:binary.list_to_bin(&1))
case checksum == binary_slice(Utils.double_sha256(decoded_body), 0..3) do
false -> {:error, :invalid_checksum}
true -> {:ok, decoded_body}
end
end
defp valid_charset?(""), do: true
defp valid_charset?(<<char>> <> string),
do: char in @base58_encode_list && valid_charset?(string)
@doc """
Encodes binary into a Base58 encoded string.
"""
@spec encode(binary) :: String.t()
def encode(bin) do
bin
|> append_checksum()
|> encode_base()
end
@spec encode_base(binary) :: String.t()
def encode_base(binary)
def encode_base(""), do: ""
def encode_base(<<0>> <> tail) do
@base58_0 <> encode_base(tail)
end
@doc """
Encodes a binary into a Base58 encoded string.
"""
def encode_base(bin) do
bin
|> :binary.decode_unsigned()
|> Integer.digits(58)
|> Enum.map(&Enum.fetch!(@base58_encode_list, &1))
|> List.to_string()
end
@spec append_checksum(binary) :: binary
defp append_checksum(body) do
body <> checksum(body)
end
@spec checksum(binary) :: binary
defp checksum(body) do
body
|> Utils.double_sha256()
|> binary_slice(0..3)
end
@spec binary_slice(binary, Range.t()) :: binary
defp binary_slice(data, range) do
data
|> :binary.bin_to_list()
|> Enum.slice(range)
|> :binary.list_to_bin()
end
end

View File

@ -0,0 +1,342 @@
defmodule Bitcoinex.Bech32 do
@moduledoc """
Includes Bech32 serialization and validation.
Reference: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32
"""
use Bitwise
@gen [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
@data_charset_list 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
@data_charset_map @data_charset_list
|> Enum.zip(0..Enum.count(@data_charset_list))
|> Enum.into(%{})
@hrp_char_code_point_upper_limit 126
@hrp_char_code_point_lower_limit 33
@max_overall_encoded_length 90
@separator "1"
@encoding_constant_map %{
bech32: 1,
bech32m: 0x2BC830A3
}
@type encoding_type :: :bech32 | :bech32m
@type hrp :: String.t()
@type data :: list(integer)
@type witness_version :: Range.t(0, 16)
@type witness_program :: list(integer)
@type max_encoded_length :: pos_integer() | :infinity
@type error :: atom()
# Inspired by Ecto.Changeset. more descriptive than result tuple
defmodule DecodeResult do
@type t() :: %__MODULE__{
encoded_str: String.t(),
encoding_type: Bitcoinex.Bech32.encoding_type() | nil,
hrp: String.t() | nil,
data: String.t() | nil,
error: atom() | nil
}
defstruct [:encoded_str, :encoding_type, :hrp, :data, :error]
@spec add_error(t(), atom()) :: t()
def add_error(%DecodeResult{} = decode_result, error) do
%{
decode_result
| error: error
}
end
@doc """
This naming is taken from Haskell. we will treat DecodeResult a bit like an Monad
And bind function will take a function that take DecodeResult that's only without error and return DecodeResult
And we can skip handling same error case for all function
"""
@spec bind(t(), (t -> t())) :: t()
def bind(%DecodeResult{error: error} = decode_result, _fun) when not is_nil(error) do
decode_result
end
def bind(%DecodeResult{} = decode_result, fun) do
fun.(decode_result)
end
end
@spec decode(String.t(), max_encoded_length()) ::
{:ok, {encoding_type, hrp, data}} | {:error, error}
def decode(bech32_str, max_encoded_length \\ @max_overall_encoded_length)
when is_binary(bech32_str) do
%DecodeResult{
encoded_str: bech32_str
}
|> DecodeResult.bind(&validate_bech32_length(&1, max_encoded_length))
|> DecodeResult.bind(&validate_bech32_case/1)
|> DecodeResult.bind(&split_bech32_str/1)
|> DecodeResult.bind(&validate_checksum_and_add_encoding_type/1)
|> format_bech32_decoding_result
end
@spec encode(hrp, data | String.t(), encoding_type, max_encoded_length()) ::
{:ok, String.t()} | {:error, error}
def encode(hrp, data, encoding_type, max_encoded_length \\ @max_overall_encoded_length)
def encode(hrp, data, encoding_type, max_encoded_length) when is_list(data) do
hrp_charlist = hrp |> String.to_charlist()
if is_valid_hrp?(hrp_charlist) do
checksummed = data ++ create_checksum(hrp_charlist, data, encoding_type)
dp = Enum.map(checksummed, &Enum.at(@data_charset_list, &1)) |> List.to_string()
encoded_result = <<hrp::binary, @separator, dp::binary>>
case validate_bech32_length(encoded_result, max_encoded_length) do
:ok ->
{:ok, String.downcase(encoded_result)}
{:error, error} ->
{:error, error}
end
else
{:error, :hrp_char_out_opf_range}
end
end
# Here we assume caller pass raw ASCII string
def encode(hrp, data, encoding_type, max_encoded_length) when is_binary(data) do
data_integers = data |> String.to_charlist() |> Enum.map(&Map.get(@data_charset_map, &1))
case check_data_charlist_validity(data_integers) do
:ok ->
encode(hrp, data_integers, encoding_type, max_encoded_length)
{:error, error} ->
{:error, error}
end
end
# Big endian conversion of a list of integer from base 2^frombits to base 2^tobits.
# ref https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py#L80
@spec convert_bits(list(integer), integer(), integer(), boolean()) ::
{:error, :invalid_data} | {:ok, list(integer)}
def convert_bits(data, from_bits, to_bits, padding \\ true) when is_list(data) do
max_v = (1 <<< to_bits) - 1
max_acc = (1 <<< (from_bits + to_bits - 1)) - 1
result =
Enum.reduce_while(data, {0, 0, []}, fn val, {acc, bits, ret} ->
if val < 0 or val >>> from_bits != 0 do
{:halt, {:error, :invalid_data}}
else
acc = (acc <<< from_bits ||| val) &&& max_acc
bits = bits + from_bits
{bits, ret} = convert_bits_loop(to_bits, max_v, acc, bits, ret)
{:cont, {acc, bits, ret}}
end
end)
case result do
{acc, bits, ret} ->
if padding && bits > 0 do
{:ok, ret ++ [acc <<< (to_bits - bits) &&& max_v]}
else
if bits >= from_bits || (acc <<< (to_bits - bits) &&& max_v) > 0 do
{:error, :invalid_data}
else
{:ok, ret}
end
end
{:error, :invalid_data} = e ->
e
end
end
defp convert_bits_loop(to, max_v, acc, bits, ret) do
if bits >= to do
bits = bits - to
ret = ret ++ [acc >>> bits &&& max_v]
convert_bits_loop(to, max_v, acc, bits, ret)
else
{bits, ret}
end
end
defp validate_checksum_and_add_encoding_type(
%DecodeResult{
data: data,
hrp: hrp
} = decode_result
) do
case bech32_polymod(bech32_hrp_expand(hrp) ++ data) do
unquote(@encoding_constant_map.bech32) ->
%DecodeResult{decode_result | encoding_type: :bech32}
unquote(@encoding_constant_map.bech32m) ->
%DecodeResult{decode_result | encoding_type: :bech32m}
_ ->
DecodeResult.add_error(decode_result, :invalid_checksum)
end
end
defp create_checksum(hrp, data, encoding_type) do
values = bech32_hrp_expand(hrp) ++ data ++ [0, 0, 0, 0, 0, 0]
mod = bech32_polymod(values) ^^^ @encoding_constant_map[encoding_type]
for p <- 0..5, do: mod >>> (5 * (5 - p)) &&& 31
end
defp bech32_polymod(values) do
Enum.reduce(
values,
1,
fn value, acc ->
b = acc >>> 25
acc = ((acc &&& 0x1FFFFFF) <<< 5) ^^^ value
Enum.reduce(0..length(@gen), acc, fn i, in_acc ->
in_acc ^^^
if (b >>> i &&& 1) != 0 do
Enum.at(@gen, i)
else
0
end
end)
end
)
end
defp bech32_hrp_expand(chars) when is_list(chars) do
Enum.map(chars, &(&1 >>> 5)) ++ [0 | Enum.map(chars, &(&1 &&& 31))]
end
defp format_bech32_decoding_result(%DecodeResult{
error: nil,
hrp: hrp,
data: data,
encoding_type: encoding_type
})
when not is_nil(hrp) and not is_nil(data) do
{:ok, {encoding_type, to_string(hrp), Enum.drop(data, -6)}}
end
defp format_bech32_decoding_result(%DecodeResult{
error: error
}) do
{:error, error}
end
defp split_bech32_str(
%DecodeResult{
encoded_str: encoded_str
} = decode_result
) do
# the bech 32 is at most 90 chars
# so it's ok to do 3 time reverse here
# otherwise we can use binary pattern matching with index for better performance
downcase_encoded_str = encoded_str |> String.downcase()
with {_, [data, hrp]} when hrp != "" and data != "" <-
{:split_by_separator,
downcase_encoded_str |> String.reverse() |> String.split(@separator, parts: 2)},
hrp = hrp |> String.reverse() |> String.to_charlist(),
{_, true} <- {:check_hrp_validity, is_valid_hrp?(hrp)},
data <-
data
|> String.reverse()
|> String.to_charlist()
|> Enum.map(&Map.get(@data_charset_map, &1)),
{_, :ok} <- {:check_data_validity, check_data_charlist_validity(data)} do
%DecodeResult{
decode_result
| hrp: hrp,
data: data
}
else
{:split_by_separator, [_]} ->
DecodeResult.add_error(decode_result, :no_separator_character)
{:split_by_separator, ["", _]} ->
DecodeResult.add_error(decode_result, :empty_data)
{:split_by_separator, [_, ""]} ->
DecodeResult.add_error(decode_result, :empty_hrp)
{:check_hrp_validity, false} ->
DecodeResult.add_error(decode_result, :hrp_char_out_opf_range)
{:check_data_validity, {:error, error}} ->
DecodeResult.add_error(decode_result, error)
end
end
defp validate_bech32_length(
%DecodeResult{
encoded_str: encoded_str
} = decode_result,
max_length
) do
case validate_bech32_length(encoded_str, max_length) do
:ok ->
decode_result
{:error, error} ->
DecodeResult.add_error(decode_result, error)
end
end
defp validate_bech32_length(encoded_str, :infinity) when is_binary(encoded_str) do
:ok
end
defp validate_bech32_length(
encoded_str,
max_length
)
when is_binary(encoded_str) and byte_size(encoded_str) > max_length do
{:error, :overall_max_length_exceeded}
end
defp validate_bech32_length(
encoded_str,
_max_length
)
when is_binary(encoded_str) do
:ok
end
defp validate_bech32_case(
%DecodeResult{
encoded_str: encoded_str
} = decode_result
) do
case String.upcase(encoded_str) == encoded_str or String.downcase(encoded_str) == encoded_str do
true ->
decode_result
false ->
DecodeResult.add_error(decode_result, :mixed_case)
end
end
defp check_data_charlist_validity(charlist) do
if length(charlist) >= 6 do
if Enum.all?(charlist, &(!is_nil(&1))) do
:ok
else
{:error, :contain_invalid_data_char}
end
else
{:error, :too_short_checksum}
end
end
defp is_valid_hrp?(hrp) when is_list(hrp), do: Enum.all?(hrp, &is_valid_hrp_char?/1)
defp is_valid_hrp_char?(char) do
char <= @hrp_char_code_point_upper_limit and char >= @hrp_char_code_point_lower_limit
end
end

View File

@ -0,0 +1,7 @@
defmodule Bitcoinex do
@moduledoc """
Documentation for Bitcoinex.
Bitcoinex is an Elixir library supporting basic Bitcoin functionality.
"""
end

View File

@ -0,0 +1,92 @@
defmodule Bitcoinex.Block do
@moduledoc """
Bitcoin on-chain transaction structure.
Supports serialization of transactions.
"""
alias Bitcoinex.Block
alias Bitcoinex.Transaction
alias Bitcoinex.Utils
alias Bitcoinex.Transaction.Utils, as: ProtocolUtils
defstruct [
:version,
:prev_block,
:merkle_root,
:timestamp,
:bits,
:nonce,
:txn_count,
:txns
]
@doc """
Returns the BlockID of the given block.
defined as the bitcoin hash of the block header (first 80 bytes):
BlockID is sha256(sha256(nVersion | prev_block | merkle_root | timestamp | bits | nonce))
"""
def block_id(raw_block) do
<<header::binary-size(80), _rest::binary>> = raw_block
Base.encode16(
<<:binary.decode_unsigned(
Utils.double_sha256(header),
:big
)::little-size(256)>>,
case: :lower
)
end
@doc """
Decodes a transaction in a hex encoded string into binary.
"""
def decode(block_hex) when is_binary(block_hex) do
case Base.decode16(block_hex, case: :lower) do
{:ok, block_bytes} ->
case parse(block_bytes) do
{:ok, block} ->
{:ok, block}
:error ->
{:error, :parse_error}
end
:error ->
{:error, :decode_error}
end
end
# returns block
defp parse(block_bytes) do
<<version::little-size(32), remaining::binary>> = block_bytes
# Previous block
<<prev_block::binary-size(32), remaining::binary>> = remaining
# Merkle root
<<merkle_root::binary-size(32), remaining::binary>> = remaining
# Timestamp, difficulty target bits, nonce
<<timestamp::little-size(32), bits::little-size(32), nonce::little-size(32), remaining::binary>> = remaining
# Transactions
{txn_count, remaining} = ProtocolUtils.get_counter(remaining)
{txns, remaining} = Transaction.parse_list(txn_count, remaining)
if byte_size(remaining) != 0 do
:error
else
{:ok,
%Block{
version: version,
prev_block: Base.encode16(<<:binary.decode_unsigned(prev_block, :big)::little-size(256)>>, case: :lower),
merkle_root: Base.encode16(<<:binary.decode_unsigned(merkle_root, :big)::little-size(256)>>, case: :lower),
timestamp: timestamp,
bits: bits,
nonce: nonce,
txn_count: length(txns),
txns: txns
}}
end
end
end

View File

@ -0,0 +1,30 @@
defmodule Bitcoinex.LightningNetwork.HopHint do
@moduledoc """
A hop hint is used to help the payer route a payment to the receiver.
A hint is included in BOLT#11 Invoices.
"""
@enforce_keys [
:node_id,
:channel_id,
:fee_base_m_sat,
:fee_proportional_millionths,
:cltv_expiry_delta
]
defstruct [
:node_id,
:channel_id,
:fee_base_m_sat,
:fee_proportional_millionths,
:cltv_expiry_delta
]
@type t() :: %__MODULE__{
node_id: String.t(),
channel_id: non_neg_integer,
fee_base_m_sat: non_neg_integer,
fee_proportional_millionths: non_neg_integer,
cltv_expiry_delta: non_neg_integer
}
end

View File

@ -0,0 +1,646 @@
defmodule Bitcoinex.LightningNetwork.Invoice do
@moduledoc """
Includes BOLT#11 Invoice serialization.
Reference: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md
"""
alias Bitcoinex.{Bech32, Network, Segwit}
alias Bitcoinex.LightningNetwork.HopHint
alias Decimal, as: D
use Bitwise
# consider using https://github.com/ejpcmac/typed_struct
@default_min_final_cltv_expiry 18
@default_expiry 3600
@enforce_keys [:network, :destination, :payment_hash, :timestamp]
defstruct [
:network,
:destination,
:payment_hash,
:amount_msat,
:timestamp,
:description,
:description_hash,
:fallback_address,
route_hints: [],
expiry: @default_expiry,
min_final_cltv_expiry: @default_min_final_cltv_expiry
]
@type t() :: %__MODULE__{
network: Network.network_name(),
destination: String.t(),
payment_hash: String.t(),
amount_msat: non_neg_integer | nil,
timestamp: integer(),
expiry: integer() | nil,
# description and description_hash are either both non-nil or nil
description: String.t() | nil,
description_hash: String.t() | nil,
fallback_address: String.t() | nil,
min_final_cltv_expiry: non_neg_integer,
route_hints: list(HopHint.t())
}
@prefix "ln"
@valid_multipliers ~w(m u n p)
# TODO move it to bitcoin asset?
@milli_satoshi_per_bitcoin 100_000_000_000
# the 512 bit signature + 8 bit recovery ID.
@signature_base32_length 104
@timestamp_base32_length 7
@sha256_hash_base32_length 52
@pubkey_base32_length 53
@hop_hint_length 51
@type error :: atom
@doc """
Decode accepts a Bech32 encoded string invoice and deserializes it.
"""
@spec decode(String.t()) :: {:ok, t} | {:error, error}
def decode(invoice) when is_binary(invoice) do
with {:ok, {_encoding_type, hrp, data}} <- Bech32.decode(invoice, :infinity),
{:ok, {network, amount_msat}} <- parse_hrp(hrp),
{invoice_data, signature_data} = split_at(data, -@signature_base32_length),
{:ok, parsed_data} <-
parse_invoice_data(invoice_data, network),
{:ok, destination} <-
validate_and_parse_signature_data(
Map.get(parsed_data, :destination),
hrp,
invoice_data,
signature_data
) do
__MODULE__
|> struct(
Map.merge(
parsed_data,
%{
network: network,
amount_msat: amount_msat,
destination: destination
}
)
)
|> validate_invoice()
end
end
def decode(invoice) when is_binary(invoice) do
{:error, :no_ln_prefix}
end
@doc """
Returns the expiry of the invoice.
"""
@spec expires_at(Bitcoinex.LightningNetwork.Invoice.t()) :: DateTime.t()
def expires_at(%__MODULE__{} = invoice) do
expiry = invoice.expiry
Timex.from_unix(invoice.timestamp + expiry, :second)
end
# checking some invariant for invoice
# TODO Could we use ecto(without SQL) for this?
defp validate_invoice(%__MODULE__{} = invoice) do
cond do
is_nil(invoice.network) ->
{:error, :network_missing}
!is_nil(invoice.amount_msat) && invoice.amount_msat < 0 ->
{:error, :negative_amount_msat}
is_nil(invoice.payment_hash) ->
{:error, :payment_hash_missing}
is_nil(invoice.description) && is_nil(invoice.description_hash) ->
{:error, :both_description_and_description_hash_present}
!is_nil(invoice.description) && !is_nil(invoice.description_hash) ->
{:error, :both_description_and_description_hash_missing}
# lnd have this but not in Bolt11. do we need to enforce this?
Enum.count(invoice.route_hints) > 20 ->
{:error, :too_many_private_routes}
String.length(invoice.payment_hash) != 64 ->
{:error, :invalid_payment_hash_length}
!is_nil(invoice.description_hash) && String.length(invoice.description_hash) != 64 ->
{:error, :invalid_payment_hash}
# String.length(invoice.destination) != 64 ->
# {:error, :invalid_destination_length}
true ->
{:ok, invoice}
end
end
defp validate_and_parse_signature_data(destination, hrp, invoice_data, signature_data)
when is_list(invoice_data) and is_list(signature_data) do
with {:ok, signature_data_in_byte} <- Bech32.convert_bits(signature_data, 5, 8),
{signature, [recoveryId]} = split_at(signature_data_in_byte, -1),
{:ok, invoice_data_in_byte} <- Bech32.convert_bits(invoice_data, 5, 8) do
to_sign = (hrp |> :erlang.binary_to_list()) ++ invoice_data_in_byte
signature = signature |> byte_list_to_binary
hash = to_sign |> Bitcoinex.Utils.sha256()
# TODO if destination exist from tagged field, we dun need to recover but to verify it with signature
# but that require convert lg sig before using secp256k1 to verify it
# TODO refactor too nested
case Bitcoinex.Secp256k1.ecdsa_recover_compact(hash, signature, recoveryId) do
{:ok, pubkey} ->
if is_nil(destination) or destination == pubkey do
{:ok, pubkey}
else
{:error, :invalid_invoice_signature}
end
{:error, error} ->
{:error, error}
end
end
end
defp parse_invoice_data(data, network) when is_list(data) do
{timstamp_data, tagged_fields_data} = split_at(data, @timestamp_base32_length)
with {:ok, timestamp} <- parse_timestamp(timstamp_data),
{:ok, parsed_data} <-
parse_tagged_fields(tagged_fields_data, network) do
{:ok, Map.put(parsed_data, :timestamp, timestamp)}
end
end
defp parse_tagged_fields(data, network) when is_list(data) do
do_parse_tagged_fields(data, %{}, network)
end
defp do_parse_tagged_fields([type, data_length1, data_length2 | rest], acc, network) do
data_length = data_length1 <<< 5 ||| data_length2
if Enum.count(rest) < data_length do
{:error, :invalid_field_length}
else
{data, new_rest} = split_at(rest, data_length)
case(parse_tagged_field(type, data, acc, network)) do
{:ok, acc} ->
do_parse_tagged_fields(new_rest, acc, network)
{:error, error} ->
{:error, error}
end
end
end
defp do_parse_tagged_fields(_, acc, _network) do
{:ok, acc}
end
defp parse_tagged_field(type, data, acc, network) do
case type do
1 ->
if Map.has_key?(acc, :payment_hash) do
{:ok, acc}
else
case parse_payment_hash(data) do
{:ok, payment_hash} ->
{:ok, Map.put(acc, :payment_hash, payment_hash)}
{:error, error} ->
{:error, error}
end
end
# r field HopHints
3 ->
if Map.has_key?(acc, :route_hints) do
{:ok, acc}
else
case parse_hop_hints(data) do
{:ok, hop_hints} ->
{:ok, Map.put(acc, :route_hints, hop_hints)}
{:error, error} ->
{:error, error}
end
end
# x field
6 ->
if Map.has_key?(acc, :expiry) do
{:ok, acc}
else
expiry = parse_expiry(data)
{:ok, Map.put(acc, :expiry, expiry)}
end
# f field fallback address
9 ->
if Map.has_key?(acc, :fallback_address) do
{:ok, acc}
else
case parse_fallback_address(data, network) do
{:ok, fallback_address} ->
{:ok, Map.put(acc, :fallback_address, fallback_address)}
{:error, error} ->
{:error, error}
end
end
# d field
13 ->
if Map.has_key?(acc, :description) do
{:ok, acc}
else
case parse_description(data) do
{:ok, description} ->
{:ok, Map.put(acc, :description, description)}
{:error, error} ->
{:error, error}
end
end
# n field destination
19 ->
case acc do
%{destination: destination} when destination != nil ->
{:ok, acc}
_ ->
case parse_destination(data) do
{:ok, destination} ->
{:ok, Map.put(acc, :destination, destination)}
{:error, error} ->
{:error, error}
end
end
# h field description hash
23 ->
if Map.has_key?(acc, :description_hash) do
{:ok, acc}
else
case parse_description_hash(data) do
{:ok, description_hash} ->
{:ok, Map.put(acc, :description_hash, description_hash)}
{:error, error} ->
{:error, error}
end
end
# c field MINIMUM Fianl CLTV Expiry
24 ->
if Map.has_key?(acc, :min_final_cltv_expiry) do
{:ok, acc}
else
min_final_cltv_expiry = parse_min_final_cltv_expiry(data)
{:ok, Map.put(acc, :min_final_cltv_expiry, min_final_cltv_expiry)}
end
_ ->
{:ok, acc}
end
end
defp parse_timestamp(data) do
{:ok, base32_to_integer(data)}
end
defp parse_payment_hash(data) when is_list(data) do
if Enum.count(data) == @sha256_hash_base32_length do
case Bech32.convert_bits(data, 5, 8, false) do
{:ok, converted_data} ->
{:ok, converted_data |> :binary.list_to_bin() |> Base.encode16(case: :lower)}
{:error, error} ->
{:error, error}
end
else
{:error, :invalid_payment_hash_length}
end
end
defp parse_description(data) do
case Bech32.convert_bits(data, 5, 8, false) do
{:ok, description} ->
{:ok, :binary.list_to_bin(description)}
{:error, error} ->
{:error, error}
end
end
defp parse_expiry(data) do
base32_to_integer(data)
end
@spec base32_to_integer(maybe_improper_list()) :: any()
def base32_to_integer(data) when is_list(data) do
Enum.reduce(data, 0, fn val, acc ->
acc <<< 5 ||| val
end)
end
defp parse_destination(data) when is_list(data) do
if Enum.count(data) == @pubkey_base32_length do
case Bech32.convert_bits(data, 5, 8, false) do
{:ok, data_in_bytes} ->
{:ok, bytes_to_hex_string(data_in_bytes)}
{:error, error} ->
{:error, error}
end
else
{:ok, nil}
end
end
defp parse_description_hash(data) when is_list(data) do
if Enum.count(data) == @sha256_hash_base32_length do
case Bech32.convert_bits(data, 5, 8, false) do
{:ok, data_in_bytes} ->
{:ok, data_in_bytes |> bytes_to_hex_string}
{:error, error} ->
{:error, error}
end
else
{:ok, nil}
end
end
defp parse_fallback_address([], _network) do
{:error, :empty_fallback_address}
end
defp parse_fallback_address([version | rest], network) do
case version do
0 ->
case Bech32.convert_bits(rest, 5, 8, false) do
{:ok, witness} ->
case Enum.count(witness) do
witness_program_lenghh when witness_program_lenghh in [20, 32] ->
Segwit.encode_address(network, 0, witness)
_ ->
{:error, :invalid_witness_program_length}
end
err ->
err
end
17 ->
case Bech32.convert_bits(rest, 5, 8, false) do
{:ok, pubKeyHash} ->
{:ok,
Bitcoinex.Address.encode(
pubKeyHash |> :binary.list_to_bin(),
network,
:p2pkh
)}
err ->
err
end
18 ->
case Bech32.convert_bits(rest, 5, 8, false) do
{:ok, scriptHash} ->
{:ok,
Bitcoinex.Address.encode(
scriptHash |> :binary.list_to_bin(),
network,
:p2sh
)}
err ->
err
end
# ignore unknown version
_ ->
{:ok, nil}
end
end
defp parse_hop_hints(data) when is_list(data) do
with {:ok, data_in_byte} <- Bech32.convert_bits(data, 5, 8, false),
{_, true} <-
{:validate_hop_hint_data_length, rem(Enum.count(data_in_byte), @hop_hint_length) == 0} do
hop_hints =
data_in_byte
|> Enum.chunk_every(@hop_hint_length)
|> Enum.map(&parse_hop_hint/1)
{:ok, hop_hints}
else
{:validate_hop_hint_data_length, false} ->
{:error, :invalid_hop_hint_data_length}
{:error, error} ->
{:error, error}
end
end
defp parse_integer_from_hex_str!(hex_str) do
{hex_str, ""} = Integer.parse(hex_str, 16)
hex_str
end
# exoect they are list of integer in byte
defp parse_hop_hint(data) when is_list(data) do
# 64 bits
{node_id_data, rest} = data |> split_at(33)
node_id = node_id_data |> bytes_to_hex_string
# 64 bits
{channel_id_data, rest} = rest |> split_at(8)
channel_id = channel_id_data |> bytes_to_hex_string |> parse_integer_from_hex_str!
# 32 bits
{fee_base_m_sat_data, rest} = rest |> split_at(4)
fee_base_m_sat = fee_base_m_sat_data |> bytes_to_hex_string |> parse_integer_from_hex_str!
# 32 bits
{fee_proportional_millionths_data, rest} = rest |> split_at(4)
fee_proportional_millionths =
fee_proportional_millionths_data |> bytes_to_hex_string |> parse_integer_from_hex_str!
cltv_expiry_delta = rest |> bytes_to_hex_string |> parse_integer_from_hex_str!
%HopHint{
node_id: node_id,
channel_id: channel_id,
fee_base_m_sat: fee_base_m_sat,
fee_proportional_millionths: fee_proportional_millionths,
cltv_expiry_delta: cltv_expiry_delta
}
end
defp parse_min_final_cltv_expiry(data) when is_list(data) do
base32_to_integer(data)
end
# defp get_pubkey_to_address_magicbyte(network, script_type) do
# case {network, script_type} do
# {:mainnet, :p2pkh} ->
# 0x00
# {:mainnet, :p2sh} ->
# 0x05
# {network, :p2pkh} when network in [:testnet, :regtest] ->
# 0x6F
# {network, :p2sh} when network in [:testnet, :regtest] ->
# 0xC4
# end
# end
defp parse_network(@prefix <> rest_hrp) do
case Enum.find(Network.supported_networks(), fn %{hrp_segwit_prefix: hrp_segwit_prefix} ->
if String.starts_with?(rest_hrp, hrp_segwit_prefix) do
size = bit_size(hrp_segwit_prefix)
case rest_hrp do
# without amount
^hrp_segwit_prefix ->
true
# with amount. a valid segwit_prefix must be following with base10 digit
# ?0..?9 means range of codepoint of 0 - 9
# it shoudln't include 0 but that's not responsiblity of passing network function here
<<_::size(size), i, _::binary>> when i in ?0..?9 ->
true
_ ->
false
end
end
end) do
nil ->
{:error, :invalid_network}
network ->
{:ok, network}
end
end
defp parse_hrp(hrp) do
with {_, @prefix <> rest_hrp} <- {:strip_prefix, hrp},
{_, {:ok, %{name: network_name, hrp_segwit_prefix: hrp_segwit_prefix}}} <-
{:parse_network, parse_network(hrp)} do
hrp_segwit_prefix_size = byte_size(hrp_segwit_prefix)
case rest_hrp do
^hrp_segwit_prefix ->
{:ok, {network_name, nil}}
_ ->
amount_str = String.slice(rest_hrp, hrp_segwit_prefix_size..-1)
case calculate_milli_satoshi(amount_str) do
{:ok, amount} ->
{:ok, {network_name, amount}}
{:error, error} ->
{:error, error}
end
end
else
{:strip_prefix, _} ->
{:error, :no_ln_prefix}
{:parse_network, error} ->
{:error, error}
end
end
defp calculate_milli_satoshi("0" <> _) do
{:error, :amount_with_leading_zero}
end
defp calculate_milli_satoshi(amount_str) do
result =
case Regex.run(~r/[munp]$/, amount_str) do
[multiplier] when multiplier in @valid_multipliers ->
case Integer.parse(String.slice(amount_str, 0..-2)) do
{amount, ""} ->
{:ok, to_bitcoin(amount, multiplier)}
_ ->
{:error, :invalid_amount}
end
_ ->
case Integer.parse(amount_str) do
{amount_in_bitcoin, ""} ->
{:ok, amount_in_bitcoin}
_ ->
{:error, :invalid_amount}
end
end
case result do
{:ok, amount_in_bitcoin} ->
amount_msat_dec = D.mult(amount_in_bitcoin, @milli_satoshi_per_bitcoin)
rounded_amount_msat_dec = D.round(amount_msat_dec)
case D.equal?(rounded_amount_msat_dec, amount_msat_dec) do
true ->
{:ok, D.to_integer(rounded_amount_msat_dec)}
false ->
{:error, :sub_msat_precision_amount}
end
{:error, error} ->
{:error, error}
end
end
defp to_bitcoin(amount, multiplier_str) when is_integer(amount) do
multiplier =
case multiplier_str do
"m" ->
0.001
"u" ->
0.000001
"n" ->
0.000000001
"p" ->
0.000000000001
end
D.mult(amount, D.from_float(multiplier))
end
defp bytes_to_hex_string(bytes) when is_list(bytes) do
bytes |> :binary.list_to_bin() |> Base.encode16(case: :lower)
end
defp byte_list_to_binary(bytes) when is_list(bytes) do
bytes |> :binary.list_to_bin()
end
@spec split_at(Enum.t(), integer()) :: {list(Enum.t()), list(Enum.t())}
defp split_at(xs, index) when index >= 0 do
{Enum.take(xs, index), Enum.drop(xs, index)}
end
defp split_at(xs, index) when index < 0 do
{Enum.drop(xs, index), Enum.take(xs, index)}
end
end

View File

@ -0,0 +1,10 @@
defmodule Bitcoinex.LightningNetwork do
@moduledoc """
Includes serialization and validation for Lightning Network BOLT#11 invoices.
"""
alias Bitcoinex.LightningNetwork.Invoice
# defdelegate encode_invoice(invoice), to: Invoice, as: :encode
defdelegate decode_invoice(invoice), to: Invoice, as: :decode
end

View File

@ -0,0 +1,80 @@
defmodule Bitcoinex.Network do
@moduledoc """
Includes network-specific paramater options.
Supported networks include mainnet, testnet3, and regtest.
"""
@enforce_keys [
:name,
:hrp_segwit_prefix,
:p2pkh_version_decimal_prefix,
:p2sh_version_decimal_prefix
]
defstruct [
:name,
:hrp_segwit_prefix,
:p2pkh_version_decimal_prefix,
:p2sh_version_decimal_prefix
]
@type t() :: %__MODULE__{
name: atom,
hrp_segwit_prefix: String.t(),
p2pkh_version_decimal_prefix: integer(),
p2sh_version_decimal_prefix: integer()
}
@type network_name :: :mainnet | :testnet | :regtest
@doc """
Returns a list of supported networks.
"""
def supported_networks() do
[
mainnet(),
testnet(),
regtest()
]
end
def mainnet do
%__MODULE__{
name: :mainnet,
hrp_segwit_prefix: "bc",
p2pkh_version_decimal_prefix: 0,
p2sh_version_decimal_prefix: 5
}
end
def testnet do
%__MODULE__{
name: :testnet,
hrp_segwit_prefix: "tb",
p2pkh_version_decimal_prefix: 111,
p2sh_version_decimal_prefix: 196
}
end
def regtest do
%__MODULE__{
name: :regtest,
hrp_segwit_prefix: "bcrt",
p2pkh_version_decimal_prefix: 111,
p2sh_version_decimal_prefix: 196
}
end
@spec get_network(network_name) :: t()
def get_network(:mainnet) do
mainnet()
end
def get_network(:testnet) do
testnet()
end
def get_network(:regtest) do
regtest()
end
end

View File

@ -0,0 +1,412 @@
defmodule Bitcoinex.PSBT do
@moduledoc """
Support for Partially Signed Bitcoin Transactions (PSBT).
The format consists of key-value maps.
Each map consists of a sequence of key-value records, terminated by a 0x00 byte.
Reference: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
"""
alias Bitcoinex.PSBT
alias Bitcoinex.PSBT.Global
alias Bitcoinex.PSBT.In
alias Bitcoinex.PSBT.Out
defstruct [
:global,
:inputs,
:outputs
]
@magic 0x70736274
@separator 0xFF
def separator, do: @separator
@doc """
Decodes a base64 encoded string into a PSBT.
"""
@spec decode(String.t()) :: {:ok, %Bitcoinex.PSBT{}} | {:error, term()}
def decode(psbt_b64) when is_binary(psbt_b64) do
case Base.decode64(psbt_b64, case: :lower) do
{:ok, psbt_b64} ->
case parse(psbt_b64) do
{:ok, txn} ->
{:ok, txn}
end
:error ->
{:error, :decode_error}
end
end
defp parse(<<@magic::big-size(32), @separator::big-size(8), psbt::binary>>) do
# key-value paris for all global data
{global, psbt} = Global.parse_global(psbt)
in_counter = length(global.unsigned_tx.inputs)
{inputs, psbt} = In.parse_inputs(psbt, in_counter)
out_counter = length(global.unsigned_tx.outputs)
{outputs, _} = Out.parse_outputs(psbt, out_counter)
{:ok,
%PSBT{
global: global,
inputs: inputs,
outputs: outputs
}}
end
end
defmodule Bitcoinex.PSBT.Utils do
@moduledoc """
Contains utility functions used throughout PSBT serialization.
"""
alias Bitcoinex.Transaction.Utils, as: TxUtils
def parse_compact_size_value(key_value) do
{len, key_value} = TxUtils.get_counter(key_value)
<<value::binary-size(len), remaining::binary>> = key_value
{value, remaining}
end
# parses key value pairs with a provided parse function
def parse_key_value(psbt, kv, parse_func) do
{kv, psbt} =
case psbt do
# separator
<<0x00::big-size(8), psbt::binary>> ->
{kv, psbt}
_ ->
case parse_compact_size_value(psbt) do
{key, psbt} ->
{kv, psbt} = parse_func.(key, psbt, kv)
parse_key_value(psbt, kv, parse_func)
end
end
{kv, psbt}
end
end
defmodule Bitcoinex.PSBT.Global do
@moduledoc """
Global properties of a partially signed bitcoin transaction.
"""
alias Bitcoinex.PSBT.Global
alias Bitcoinex.Transaction
alias Bitcoinex.Transaction.Utils, as: TxUtils
alias Bitcoinex.PSBT.Utils, as: PsbtUtils
alias Bitcoinex.Base58
defstruct [
:unsigned_tx,
:xpub,
:version,
:proprietary
]
@psbt_global_unsigned_tx 0x00
@psbt_global_xpub 0x01
@psbt_global_version 0xFB
@psbt_global_proprietary 0xFC
def parse_global(psbt) do
PsbtUtils.parse_key_value(psbt, %Global{}, &parse/3)
end
# unsigned transaction
defp parse(<<@psbt_global_unsigned_tx::big-size(8)>>, psbt, global) do
{txn_len, psbt} = TxUtils.get_counter(psbt)
<<txn_bytes::binary-size(txn_len), psbt::binary>> = psbt
# todo, different decode function for txn, directly in bytes
case Transaction.decode(Base.encode16(txn_bytes, case: :lower)) do
{:ok, txn} ->
{%Global{global | unsigned_tx: txn}, psbt}
{:error, error_msg} ->
{:error, error_msg}
end
end
defp parse(<<@psbt_global_xpub::big-size(8), xpub::binary-size(78)>>, psbt, global) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
global = %Global{
global
| xpub: %{xpub: Base58.encode(xpub), derivation: Base.encode16(value, case: :lower)}
}
{global, psbt}
end
defp parse(<<@psbt_global_version::big-size(8)>>, psbt, global) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
global = %Global{global | version: value}
{global, psbt}
end
defp parse(<<@psbt_global_proprietary::big-size(8)>>, psbt, global) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
global = %Global{global | proprietary: value}
{global, psbt}
end
end
defmodule Bitcoinex.PSBT.In do
@moduledoc """
Input properties of a partially signed bitcoin transaction.
"""
alias Bitcoinex.Transaction
alias Bitcoinex.Transaction.Witness
alias Bitcoinex.Transaction.Out
alias Bitcoinex.PSBT.In
alias Bitcoinex.PSBT.Utils, as: PsbtUtils
defstruct [
:non_witness_utxo,
:witness_utxo,
:partial_sig,
:sighash_type,
:redeem_script,
:witness_script,
:bip32_derivation,
:final_scriptsig,
:final_scriptwitness,
:por_commitment,
:proprietary
]
@psbt_in_non_witness_utxo 0x00
@psbt_in_witness_utxo 0x01
@psbt_in_partial_sig 0x02
@psbt_in_sighash_type 0x03
@psbt_in_redeem_script 0x04
@psbt_in_witness_script 0x05
@psbt_in_bip32_derivation 0x06
@psbt_in_final_scriptsig 0x07
@psbt_in_final_scriptwitness 0x08
@psbt_in_por_commitment 0x09
@psbt_in_proprietary 0xFC
def parse_inputs(psbt, num_inputs) do
psbt
|> parse_input([], num_inputs)
end
defp parse_input(psbt, inputs, 0), do: {Enum.reverse(inputs), psbt}
defp parse_input(psbt, inputs, num_inputs) do
case PsbtUtils.parse_key_value(psbt, %In{}, &parse/3) do
{nil, psbt} ->
parse_input(psbt, inputs, num_inputs - 1)
{input, psbt} ->
input =
case input do
%{bip32_derivation: bip32_derivation} when is_list(bip32_derivation) ->
%{input | bip32_derivation: Enum.reverse(bip32_derivation)}
_ ->
input
end
parse_input(psbt, [input | inputs], num_inputs - 1)
end
end
defp parse(<<@psbt_in_non_witness_utxo::big-size(8)>>, psbt, input) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
{:ok, txn} = Transaction.decode(Base.encode16(value, case: :lower))
input = %In{input | non_witness_utxo: txn}
{input, psbt}
end
defp parse(<<@psbt_in_witness_utxo::big-size(8)>>, psbt, input) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
out = Out.output(value)
input = %In{input | witness_utxo: out}
{input, psbt}
end
defp parse(<<@psbt_in_partial_sig::big-size(8), public_key::binary-size(33)>>, psbt, input) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
input = %In{
input
| partial_sig: %{
public_key: Base.encode16(public_key, case: :lower),
signature: Base.encode16(value, case: :lower)
}
}
{input, psbt}
end
defp parse(<<@psbt_in_sighash_type::big-size(8)>>, psbt, input) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
input = %In{input | sighash_type: value}
{input, psbt}
end
defp parse(<<@psbt_in_redeem_script::big-size(8)>>, psbt, input) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
input = %In{input | redeem_script: Base.encode16(value, case: :lower)}
{input, psbt}
end
defp parse(<<@psbt_in_witness_script::big-size(8)>>, psbt, input) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
input = %In{input | witness_script: Base.encode16(value, case: :lower)}
{input, psbt}
end
defp parse(<<@psbt_in_bip32_derivation::big-size(8), public_key::binary-size(33)>>, psbt, input) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
bip32_derivation =
case input.bip32_derivation do
nil ->
[
%{
public_key: Base.encode16(public_key, case: :lower),
derivation: Base.encode16(value, case: :lower)
}
]
_ ->
[
%{
public_key: Base.encode16(public_key, case: :lower),
derivation: Base.encode16(value, case: :lower)
}
| input.bip32_derivation
]
end
input = %In{input | bip32_derivation: bip32_derivation}
{input, psbt}
end
defp parse(<<@psbt_in_final_scriptsig::big-size(8)>>, psbt, input) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
input = %In{input | final_scriptsig: Base.encode16(value, case: :lower)}
{input, psbt}
end
defp parse(<<@psbt_in_por_commitment::big-size(8)>>, psbt, input) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
input = %In{input | por_commitment: value}
{input, psbt}
end
defp parse(<<@psbt_in_proprietary::big-size(8)>>, psbt, input) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
input = %In{input | proprietary: value}
{input, psbt}
end
defp parse(<<@psbt_in_final_scriptwitness::big-size(8)>>, psbt, input) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
value = Witness.witness(value)
input = %In{input | final_scriptwitness: value}
{input, psbt}
end
end
defmodule Bitcoinex.PSBT.Out do
@moduledoc """
Output properties of a partially signed bitcoin transaction.
"""
alias Bitcoinex.PSBT.Out
alias Bitcoinex.PSBT.Utils, as: PsbtUtils
defstruct [
:redeem_script,
:witness_script,
:bip32_derivation,
:proprietary
]
@psbt_out_redeem_script 0x00
@psbt_out_scriptwitness 0x01
@psbt_out_bip32_derivation 0x02
def parse_outputs(psbt, num_outputs) do
parse_output(psbt, [], num_outputs)
end
defp parse_output(psbt, outputs, 0), do: {Enum.reverse(outputs), psbt}
defp parse_output(psbt, outputs, num_outputs) do
case PsbtUtils.parse_key_value(psbt, %Out{}, &parse/3) do
{%Out{bip32_derivation: nil, proprietary: nil, redeem_script: nil, witness_script: nil},
psbt} ->
parse_output(psbt, outputs, num_outputs - 1)
{output, psbt} ->
output =
case output do
%{bip32_derivation: bip32_derivation} when is_list(bip32_derivation) ->
%{output | bip32_derivation: Enum.reverse(bip32_derivation)}
_ ->
output
end
parse_output(psbt, [output | outputs], num_outputs - 1)
end
end
defp parse(<<@psbt_out_redeem_script::big-size(8)>>, psbt, output) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
output = %Out{output | redeem_script: Base.encode16(value, case: :lower)}
{output, psbt}
end
defp parse(<<@psbt_out_scriptwitness::big-size(8)>>, psbt, output) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
output = %Out{output | witness_script: Base.encode16(value, case: :lower)}
{output, psbt}
end
defp parse(
<<@psbt_out_bip32_derivation::big-size(8), public_key::binary-size(33)>>,
psbt,
output
) do
{value, psbt} = PsbtUtils.parse_compact_size_value(psbt)
bip32_derivation =
case output.bip32_derivation do
nil ->
[
%{
public_key: Base.encode16(public_key, case: :lower),
derivation: Base.encode16(value, case: :lower)
}
]
_ ->
[
%{
public_key: Base.encode16(public_key, case: :lower),
derivation: Base.encode16(value, case: :lower)
}
| output.bip32_derivation
]
end
output = %Out{
output
| bip32_derivation: bip32_derivation
}
{output, psbt}
end
end
# defmodule Bitcoinex.PSBT.Bip32_Derivation do
# end

View File

@ -0,0 +1,285 @@
defmodule Bitcoinex.Secp256k1.Math do
@moduledoc """
Contains math utilities when dealing with secp256k1 curve points and scalars.
All of the addition and multiplication uses the secp256k1 curve paramaters.
Several of the jacobian multiplication and addition functions are borrowed heavily from https://github.com/starkbank/ecdsa-elixir/.
"""
alias Bitcoinex.Secp256k1.{Params, Point}
import Bitcoinex.Secp256k1.Point
use Bitwise, only_operators: true
@doc """
pow performs integer pow,
where x is raised to the power of y.
"""
# Integer.pow/2 was added since 1.12.0. This function_exported? can be removed when we decide
# to only support >= 1.12.0 in the future
if function_exported?(Integer, :pow, 2) do
defdelegate pow(base, exponent), to: Integer
else
# copy from https://github.com/elixir-lang/elixir/blob/master/lib/elixir/lib/integer.ex#L104
@spec pow(integer, non_neg_integer) :: integer
def pow(base, exponent) when is_integer(base) and is_integer(exponent) and exponent >= 0 do
guarded_pow(base, exponent)
end
# https://en.wikipedia.org/wiki/Exponentiation_by_squaring
defp guarded_pow(_, 0), do: 1
defp guarded_pow(b, 1), do: b
defp guarded_pow(b, e) when (e &&& 1) == 0, do: guarded_pow(b * b, e >>> 1)
defp guarded_pow(b, e), do: b * guarded_pow(b * b, e >>> 1)
end
@doc """
Inv performs the Extended Euclidean Algorithm to to find
the inverse of a number x mod n.
"""
@spec inv(integer, pos_integer) :: integer
def inv(x, n) when is_integer(x) and is_integer(n) and n >= 1 do
do_inv(x, n)
end
defp do_inv(x, _n) when x == 0, do: 0
defp do_inv(x, n), do: do_inv(1, 0, modulo(x, n), n) |> modulo(n)
defp do_inv(lm, hm, low, high) when low > 1 do
r = div(high, low)
do_inv(
hm - lm * r,
lm,
high - low * r,
low
)
end
defp do_inv(lm, _hm, _low, _high) do
lm
end
@spec modulo(integer, integer) :: integer
def modulo(x, n) when is_integer(x) and is_integer(n) do
r = rem(x, n)
if r < 0, do: r + n, else: r
end
@doc """
multiply accepts a point P and scalar n and,
does jacobian multiplication to return resulting point.
"""
def multiply(p, n) when is_point(p) and is_integer(n) do
p
|> toJacobian()
|> jacobianMultiply(n)
|> fromJacobian()
end
@doc """
add accepts points p and q and,
does jacobian addition to return resulting point.
"""
def add(p, q) when is_point(p) and is_point(q) do
jacobianAdd(toJacobian(p), toJacobian(q))
|> fromJacobian()
end
# Convert our point P to jacobian coordinates.
defp toJacobian(p) do
%Point{x: p.x, y: p.y, z: 1}
end
# Convert our jacobian coordinates to a point P on secp256k1 curve.
defp fromJacobian(p) do
z = inv(p.z, Params.curve().p)
%Point{
x:
modulo(
p.x * pow(z, 2),
Params.curve().p
),
y:
modulo(
p.y * pow(z, 3),
Params.curve().p
)
}
end
# double Point P to get point P + P
# We use the dbl-1998-cmo-2 doubling formula.
# For reference, http://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html.
defp jacobianDouble(p) do
if p.y == 0 do
%Point{x: 0, y: 0, z: 0}
else
# XX = X1^2
xsq =
pow(p.x, 2)
|> modulo(Params.curve().p)
# YY = Y1^2
ysq =
pow(p.y, 2)
|> modulo(Params.curve().p)
# S = 4 * X1 * YY
s =
(4 * p.x * ysq)
|> modulo(Params.curve().p)
# M = 3 * XX + a * Z1^4
m =
(3 * xsq + Params.curve().a * pow(p.z, 4))
|> modulo(Params.curve().p)
# T = M^2 - 2 * S
t =
(pow(m, 2) - 2 * s)
|> modulo(Params.curve().p)
# X3 = T
nx = t
# Y3 = M * (S - T) - 8 * YY^2
ny =
(m * (s - t) - 8 * pow(ysq, 2))
|> modulo(Params.curve().p)
# Z3 = 2 * Y1 * Z1
nz =
(2 * p.y * p.z)
|> modulo(Params.curve().p)
%Point{x: nx, y: ny, z: nz}
end
end
# add points P and Q to get P + Q
# We use the add-1998-cmo-2 addition formula.
# For reference, http://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html.
defp jacobianAdd(p, q) do
if p.y == 0 do
q
else
if q.y == 0 do
p
else
# U1 = X1 * Z2^2
u1 =
(p.x * pow(q.z, 2))
|> modulo(Params.curve().p)
# U2 = X2 * Z2^2
u2 =
(q.x * pow(p.z, 2))
|> modulo(Params.curve().p)
# S1 = Y1 * Z2^3
s1 =
(p.y * pow(q.z, 3))
|> modulo(Params.curve().p)
# S2 = y2 * Z1^3
s2 =
(q.y * pow(p.z, 3))
|> modulo(Params.curve().p)
if u1 == u2 do
if s1 != s2 do
%Point{x: 0, y: 0, z: 1}
else
jacobianDouble(p)
end
else
# H = U2 - U1
h = u2 - u1
# r = S2 - S1
r = s2 - s1
# HH = H^2
h2 =
(h * h)
|> modulo(Params.curve().p)
# HHH = H * HH
h3 =
(h * h2)
|> modulo(Params.curve().p)
# V = U1 * HH
v =
(u1 * h2)
|> modulo(Params.curve().p)
# X3 = 42 - HHH - 2 * V
nx =
(pow(r, 2) - h3 - 2 * v)
|> modulo(Params.curve().p)
# Y3 = r * (V - X3) - S1 * HHH
ny =
(r * (v - nx) - s1 * h3)
|> modulo(Params.curve().p)
# Z3 = Z1 * Z2 * H
nz =
(h * p.z * q.z)
|> modulo(Params.curve().p)
%Point{x: nx, y: ny, z: nz}
end
end
end
end
# multply point P with scalar n
defp jacobianMultiply(_p, n) when n == 0 do
%Point{x: 0, y: 0, z: 1}
end
defp jacobianMultiply(p, n) when n == 1 do
if p.y == 0 do
%Point{x: 0, y: 0, z: 1}
else
p
end
end
defp jacobianMultiply(p, n)
# This integer is n, the integer order of G for secp256k1.
# Unfortunately cannot call Params.curve.n to get the curve order integer,
# so instead, it is pasted it here.
# In the future we should move it back to Params.
when n < 0 or
n >
115_792_089_237_316_195_423_570_985_008_687_907_852_837_564_279_074_904_382_605_163_141_518_161_494_337 do
if p.y == 0 do
%Point{x: 0, y: 0, z: 1}
else
jacobianMultiply(p, modulo(n, Params.curve().n))
end
end
defp jacobianMultiply(p, n) when rem(n, 2) == 0 do
if p.y == 0 do
%Point{x: 0, y: 0, z: 1}
else
jacobianMultiply(p, div(n, 2))
|> jacobianDouble()
end
end
defp jacobianMultiply(p, n) do
if p.y == 0 do
%Point{x: 0, y: 0, z: 1}
else
jacobianMultiply(p, div(n, 2))
|> jacobianDouble()
|> jacobianAdd(p)
end
end
end

View File

@ -0,0 +1,18 @@
defmodule Bitcoinex.Secp256k1.Params do
@doc """
Secp256k1 parameters.
http://www.secg.org/sec2-v2.pdf
"""
@spec curve :: map
def curve do
%{
p: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_FFFFFC2F,
a: 0x00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
b: 0x00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000007,
g_x: 0x79BE667E_F9DCBBAC_55A06295_CE870B07_029BFCDB_2DCE28D9_59F2815B_16F81798,
g_y: 0x483ADA77_26A3C465_5DA4FBFC_0E1108A8_FD17B448_A6855419_9C47D08F_FB10D4B8,
n: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141,
h: 0x01
}
end
end

View File

@ -0,0 +1,40 @@
defmodule Bitcoinex.Secp256k1.Point do
@moduledoc """
Contains the x, y, and z of an elliptic curve point.
"""
@type t :: %__MODULE__{
x: integer(),
y: integer(),
z: integer()
}
@enforce_keys [
:x,
:y
]
defstruct [:x, :y, z: 0]
defguard is_point(term)
when is_map(term) and :erlang.map_get(:__struct__, term) == __MODULE__ and
:erlang.is_map_key(:x, term) and :erlang.is_map_key(:y, term) and
:erlang.is_map_key(:z, term)
@doc """
serialize_public_key serializes a compressed public key
"""
@spec serialize_public_key(t()) :: String.t()
def serialize_public_key(%__MODULE__{x: x, y: y}) do
case rem(y, 2) do
0 ->
Base.encode16(<<0x02>> <> Bitcoinex.Utils.pad(:binary.encode_unsigned(x), 32, :leading),
case: :lower
)
1 ->
Base.encode16(<<0x03>> <> Bitcoinex.Utils.pad(:binary.encode_unsigned(x), 32, :leading),
case: :lower
)
end
end
end

View File

@ -0,0 +1,159 @@
defmodule Bitcoinex.Secp256k1 do
@moduledoc """
ECDSA Secp256k1 curve operations.
libsecp256k1: https://github.com/bitcoin-core/secp256k1
Currently supports ECDSA public key recovery.
In the future, we will NIF for critical operations. However, it is more portable to have a native elixir version.
"""
use Bitwise, only_operators: true
alias Bitcoinex.Secp256k1.{Math, Params, Point}
@generator_point %Point{
x: Params.curve().g_x,
y: Params.curve().g_y
}
defmodule Signature do
@moduledoc """
Contains r,s in signature.
"""
@type t :: %__MODULE__{
r: pos_integer(),
s: pos_integer()
}
@enforce_keys [
:r,
:s
]
defstruct [:r, :s]
@spec parse_signature(binary) ::
{:ok, t()} | {:error, String.t()}
@doc """
accepts a compact signature and returns a Signature containing r,s
"""
def parse_signature(<<r::binary-size(32), s::binary-size(32)>>) do
# Get r,s from signature.
r = :binary.decode_unsigned(r)
s = :binary.decode_unsigned(s)
# Verify that r,s are integers in [1, n-1] where n is the integer order of G.
cond do
r < 1 ->
{:error, "invalid signature"}
r > Params.curve().n - 1 ->
{:error, "invalid signature"}
s < 1 ->
{:error, "invalid signature"}
s > Params.curve().n - 1 ->
{:error, "invalid signature"}
true ->
{:ok, %Signature{r: r, s: s}}
end
end
def parse_signature(compact_sig) when is_binary(compact_sig),
do: {:error, "invalid signature size"}
end
@doc """
ecdsa_recover_compact does ECDSA public key recovery.
"""
@spec ecdsa_recover_compact(binary, binary, integer) ::
{:ok, binary} | {:error, String.t()}
def ecdsa_recover_compact(msg, compact_sig, recoveryId) do
# Parse r and s from the signature.
case Signature.parse_signature(compact_sig) do
{:ok, sig} ->
# Find the iteration.
# R(x) = (n * i) + r
# where n is the order of the curve and R is from the signature.
r_x = Params.curve().n * Integer.floor_div(recoveryId, 2) + sig.r
# Check that R(x) is on the curve.
if r_x > Params.curve().p do
{:error, "R(x) is not on the curve"}
else
# Decompress to get R(y).
case get_y(r_x, rem(recoveryId, 2) == 1) do
{:ok, r_y} ->
# R(x,y)
point_r = %Point{x: r_x, y: r_y}
# Point Q is the recovered public key.
# We satisfy this equation: Q = r^-1(sR-eG)
inv_r = Math.inv(sig.r, Params.curve().n)
inv_r_s = (inv_r * sig.s) |> Math.modulo(Params.curve().n)
# R*s
point_sr = Math.multiply(point_r, inv_r_s)
# Find e using the message hash.
e =
:binary.decode_unsigned(msg)
|> Kernel.*(-1)
|> Math.modulo(Params.curve().n)
|> Kernel.*(inv_r |> Math.modulo(Params.curve().n))
# G*e
point_ge = Math.multiply(@generator_point, e)
# R*e * G*e
point_q = Math.add(point_sr, point_ge)
# Returns serialized compressed public key.
{:ok, Point.serialize_public_key(point_q)}
{:error, error} ->
{:error, error}
end
end
{:error, e} ->
{:error, e}
end
end
@doc """
Returns the y-coordinate of a secp256k1 curve point (P) using the x-coordinate.
To get P(y), we solve for y in this equation: y^2 = x^3 + 7.
"""
@spec get_y(integer, boolean) :: {:ok, integer} | {:error, String.t()}
def get_y(x, is_y_odd) do
# x^3 + 7
y_sq =
:crypto.mod_pow(x, 3, Params.curve().p)
|> :binary.decode_unsigned()
|> Kernel.+(7 |> Math.modulo(Params.curve().p))
# Solve for y.
y =
:crypto.mod_pow(y_sq, Integer.floor_div(Params.curve().p + 1, 4), Params.curve().p)
|> :binary.decode_unsigned()
y =
case rem(y, 2) == 1 do
^is_y_odd ->
y
_ ->
Params.curve().p - y
end
# Check.
if y_sq != :crypto.mod_pow(y, 2, Params.curve().p) |> :binary.decode_unsigned() do
{:error, "invalid sq root"}
else
{:ok, y}
end
end
end

View File

@ -0,0 +1,159 @@
defmodule Bitcoinex.Segwit do
@moduledoc """
SegWit address serialization.
"""
alias Bitcoinex.Bech32
use Bitwise
@valid_witness_program_length_range 2..40
@valid_witness_version 0..16
@supported_network [:mainnet, :testnet, :regtest]
@type hrp :: String.t()
@type data :: list(integer)
# seem no way to use list of atom module attribute in type spec
@type network :: :testnet | :mainnet | :regtest
@type witness_version :: 0..16
@type witness_program :: list(integer)
@type error :: atom()
@doc """
Decodes an address and returns its network, witness version, and witness program.
"""
@spec decode_address(String.t()) ::
{:ok, {network, witness_version, witness_program}} | {:error, error}
def decode_address(address) when is_binary(address) do
with {_, {:ok, {encoding_type, hrp, data}}} <- {:decode_bech32, Bech32.decode(address)},
{_, {:ok, network}} <- {:parse_network, parse_network(hrp |> String.to_charlist())},
{_, {:ok, {version, program}}} <- {:parse_segwit_data, parse_segwit_data(data)} do
case witness_version_to_bech_encoding(version) do
^encoding_type ->
{:ok, {network, version, program}}
_ ->
# encoding type derived from witness version (first byte of data) is different from the code derived from bech32 decoding
{:error, :invalid_checksum}
end
else
{_, {:error, error}} ->
{:error, error}
end
end
@doc """
Encodes an address string.
"""
@spec encode_address(network, witness_version, witness_program) ::
{:ok, String.t()} | {:error, error}
def encode_address(network, _, _) when not (network in @supported_network) do
{:error, :invalid_network}
end
def encode_address(_, witness_version, _)
when not (witness_version in @valid_witness_version) do
{:error, :invalid_witness_version}
end
def encode_address(network, version, program) do
with {:ok, converted_program} <- Bech32.convert_bits(program, 8, 5),
{:is_program_length_valid, true} <-
{:is_program_length_valid, is_program_length_valid?(version, program)} do
hrp =
case network do
:mainnet ->
"bc"
:testnet ->
"tb"
:regtest ->
"bcrt"
end
Bech32.encode(hrp, [version | converted_program], witness_version_to_bech_encoding(version))
else
{:is_program_length_valid, false} ->
{:error, :invalid_program_length}
error ->
error
end
end
@doc """
Simpler Interface to check if address is valid
"""
@spec is_valid_segswit_address?(String.t()) :: boolean
def is_valid_segswit_address?(address) when is_binary(address) do
case decode_address(address) do
{:ok, _} ->
true
_ ->
false
end
end
@spec get_segwit_script_pubkey(witness_version, witness_program) :: String.t()
def get_segwit_script_pubkey(version, program) do
# OP_0 is encoded as 0x00, but OP_1 through OP_16 are encoded as 0x51 though 0x60
wit_version_adjusted = if(version == 0, do: 0, else: version + 0x50)
[
wit_version_adjusted,
Enum.count(program) | program
]
|> :erlang.list_to_binary()
# to hex and all lower case for better readability
|> Base.encode16(case: :lower)
end
defp parse_segwit_data([]) do
{:error, :empty_segwit_data}
end
defp parse_segwit_data([version | encoded]) when version in @valid_witness_version do
case Bech32.convert_bits(encoded, 5, 8, false) do
{:ok, program} ->
if is_program_length_valid?(version, program) do
{:ok, {version, program}}
else
{:error, :invalid_program_length}
end
{:error, error} ->
{:error, error}
end
end
defp parse_segwit_data(_), do: {:error, :invalid_witness_version}
defp is_program_length_valid?(version, program)
when length(program) in @valid_witness_program_length_range do
case {version, length(program)} do
# BIP141 specifies If the version byte is 0, but the witness program is neither 20 nor 32 bytes, the script must fail.
{0, program_length} when program_length == 20 or program_length == 32 ->
true
{0, _} ->
false
_ ->
true
end
end
defp is_program_length_valid?(_, _), do: false
defp parse_network('bc'), do: {:ok, :mainnet}
defp parse_network('tb'), do: {:ok, :testnet}
defp parse_network('bcrt'), do: {:ok, :regtest}
defp parse_network(_), do: {:error, :invalid_network}
defp witness_version_to_bech_encoding(0), do: :bech32
defp witness_version_to_bech_encoding(witver) when witver in 1..16, do: :bech32m
end

View File

@ -0,0 +1,429 @@
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
defstruct [
:version,
:vbytes,
:inputs,
:outputs,
:witnesses,
:lock_time
]
@doc """
Returns the TxID of the given transaction.
TxID is sha256(sha256(nVersion | txins | txouts | nLockTime))
"""
def transaction_id(txn) do
legacy_txn = TxUtils.serialize_legacy(txn)
{:ok, legacy_txn} = Base.decode16(legacy_txn, case: :lower)
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
# Extracts and parses a transaction from the head of a binary
defp parse_one(tx_bytes) do
<<version::little-size(32), remaining::binary>> = tx_bytes
{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)
before_witness_bytes = byte_size(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
# discounted witness bytes = all of the witness segment
# plus segwit marker & segwit flag bytes
witness_byte_size = 2 + before_witness_bytes - byte_size(remaining)
<<lock_time::little-size(32), remaining::binary>> = remaining
initial_byte_size = byte_size(tx_bytes)
remaining_byte_size = byte_size(remaining)
total_byte_size = initial_byte_size - remaining_byte_size
# calculate size in vbytes
vbytes =
if is_segwit == :segwit do
non_witness_byte_size = total_byte_size - witness_byte_size
non_witness_byte_size + (witness_byte_size / 4)
else
total_byte_size
end
txn = %Transaction{
version: version,
vbytes: vbytes,
inputs: inputs,
outputs: outputs,
witnesses: witnesses,
lock_time: lock_time
}
cond do
byte_size(remaining) < 0 ->
:error
byte_size(remaining) > 0 ->
{:ok, txn, remaining}
true ->
{:ok, txn}
end
end
# returns transaction
defp parse(tx_bytes) do
case (parse_one(tx_bytes)) do
{:ok, txn} ->
{:ok, txn}
{:ok, _txn, _remaining} ->
:error
:error ->
:error
end
end
def parse_list(counter, txns), do: do_parse_list(txns, [], counter)
defp do_parse_list(remaining, txns, 0), do: {Enum.reverse(txns), remaining}
defp do_parse_list(remaining, txns, count) do
case parse_one(remaining) do
{:ok, txn} ->
do_parse_list(<<>>, [txn | txns], count - 1)
{:ok, txn, remaining} ->
do_parse_list(remaining, [txn | txns], count - 1)
end
end
end
defmodule Bitcoinex.Transaction.Utils do
@moduledoc """
Utilities for when dealing with transaction objects.
"""
alias Bitcoinex.Transaction.In
alias Bitcoinex.Transaction.Out
@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(<<counter::little-size(8), vec::binary>>) do
case counter do
# 0xFD followed by the length as uint16_t
0xFD ->
<<len::little-size(16), vec::binary>> = vec
{len, vec}
# 0xFE followed by the length as uint32_t
0xFE ->
<<len::little-size(32), vec::binary>> = vec
{len, vec}
# 0xFF followed by the length as uint64_t
0xFF ->
<<len::little-size(64), vec::binary>> = vec
{len, vec}
_ ->
{counter, vec}
end
end
@doc """
Serializes a transaction without the witness structure.
"""
def serialize_legacy(txn) do
version = <<txn.version::little-size(32)>>
tx_in_count = serialize_compact_size_unsigned_int(length(txn.inputs))
inputs = In.serialize_inputs(txn.inputs)
tx_out_count = serialize_compact_size_unsigned_int(length(txn.outputs))
outputs = Out.serialize_outputs(txn.outputs)
lock_time = <<txn.lock_time::little-size(32)>>
Base.encode16(
version <>
tx_in_count <>
inputs <>
tx_out_count <>
outputs <>
lock_time,
case: :lower
)
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::little-size(8)>>
compact_size <= 0xFFFF ->
<<0xFD>> <> <<compact_size::little-size(16)>>
compact_size <= 0xFFFFFFFF ->
<<0xFE>> <> <<compact_size::little-size(32)>>
compact_size <= 0xFF ->
<<0xFF>> <> <<compact_size::little-size(64)>>
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
defstruct [
:txinwitness
]
@doc """
Wtiness accepts a binary and deserializes it.
"""
@spec witness(binary) :: %Bitcoinex.Transaction.Witness{
:txinwitness => [any()] | 0
}
def witness(witness_bytes) do
{stack_size, witness_bytes} = TxUtils.get_counter(witness_bytes)
{witness, _} =
if stack_size == 0 do
{%Witness{txinwitness: 0}, witness_bytes}
else
{stack_items, witness_bytes} = parse_stack(witness_bytes, [], stack_size)
{%Witness{txinwitness: stack_items}, witness_bytes}
end
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)
<<stack_item::binary-size(item_size), remaining::binary>> = 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
defstruct [
:prev_txid,
:prev_vout,
:script_sig,
:sequence_no
]
def serialize_inputs(inputs) do
serialize_input(inputs, <<""::binary>>)
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 <>
<<input.prev_vout::little-size(32)>> <>
script_len <> script_sig <> <<input.sequence_no::little-size(32)>>
serialize_input(inputs, <<serialized_inputs::binary>> <> 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(
<<prev_txid::binary-size(32), prev_vout::little-size(32), remaining::binary>>,
inputs,
count
) do
{script_len, remaining} = TxUtils.get_counter(remaining)
<<script_sig::binary-size(script_len), sequence_no::little-size(32), remaining::binary>> =
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
defstruct [
:value,
:script_pub_key
]
def serialize_outputs(outputs) do
serialize_output(outputs, <<""::binary>>)
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 = <<output.value::little-size(64)>> <> script_len <> script_pub_key
serialize_output(outputs, <<serialized_outputs::binary>> <> serialized_output)
end
def output(out_bytes) do
<<value::little-size(64), out_bytes::binary>> = out_bytes
{script_len, out_bytes} = TxUtils.get_counter(out_bytes)
<<script_pub_key::binary-size(script_len), _::binary>> = 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(<<value::little-size(64), remaining::binary>>, outputs, count) do
{script_len, remaining} = TxUtils.get_counter(remaining)
<<script_pub_key::binary-size(script_len), remaining::binary>> = remaining
output = %Out{
value: value,
script_pub_key: Base.encode16(script_pub_key, case: :lower)
}
parse(remaining, [output | outputs], count - 1)
end
end

View File

@ -0,0 +1,50 @@
defmodule Bitcoinex.Utils do
@moduledoc """
Contains useful utility functions used in Bitcoinex.
"""
@spec sha256(iodata()) :: binary
def sha256(str) do
:crypto.hash(:sha256, str)
end
@spec replicate(term(), integer()) :: list(term())
def replicate(_num, 0) do
[]
end
def replicate(x, num) when x > 0 do
for _ <- 1..num, do: x
end
@spec double_sha256(iodata()) :: binary
def double_sha256(preimage) do
:crypto.hash(
:sha256,
:crypto.hash(:sha256, preimage)
)
end
@typedoc """
The pad_type describes the padding to use.
"""
@type pad_type :: :leading | :trailing
@doc """
pads binary according to the byte length and the padding type. A binary can be padded with leading or trailing zeros.
"""
@spec pad(bin :: binary, byte_len :: integer, pad_type :: pad_type) :: binary
def pad(bin, byte_len, _pad_type) when is_binary(bin) and byte_size(bin) == byte_len do
bin
end
def pad(bin, byte_len, pad_type) when is_binary(bin) and pad_type == :leading do
pad_len = 8 * byte_len - byte_size(bin) * 8
<<0::size(pad_len)>> <> bin
end
def pad(bin, byte_len, pad_type) when is_binary(bin) and pad_type == :trailing do
pad_len = 8 * byte_len - byte_size(bin) * 8
bin <> <<0::size(pad_len)>>
end
end

79
server/bitcoinex/mix.exs Normal file
View File

@ -0,0 +1,79 @@
defmodule Bitcoinex.MixProject do
use Mix.Project
def project do
[
app: :bitcoinex,
version: "0.1.1",
elixir: "~> 1.8",
package: package(),
start_permanent: Mix.env() == :prod,
dialyzer: dialyzer(),
deps: deps(),
aliases: aliases(),
description: description(),
source_url: "https://github.com/Mononaut/bitcoinex"
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:credo, "~> 1.0.0", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.0.0-rc.6", only: [:dev, :test], runtime: false},
{:excoveralls, "~> 0.10", only: :test},
{:mix_test_watch, "~> 0.8", only: :dev, runtime: false},
{:stream_data, "~> 0.1", only: :test},
{:timex, "~> 3.1"},
{:decimal, "~> 1.0"},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false}
]
end
defp aliases do
[
"lint.all": [
"format --check-formatted",
"credo --strict --only warning",
"dialyzer --halt-exit-status"
],
compile: ["compile --warnings-as-errors"]
]
end
# Dialyzer configuration
defp dialyzer do
[
plt_file: plt_file(),
flags: [
:error_handling,
:race_conditions
],
ignore_warnings: ".dialyzer_ignore.exs"
]
end
# Use a custom PLT directory for CI caching.
defp plt_file do
{:no_warn, "_plts/dialyzer.plt"}
end
defp package do
[
files: ~w(lib test .formatter.exs mix.exs README.md UNLICENSE),
licenses: ["Unlicense"],
links: %{"GitHub" => "https://github.com/RiverFinancial/bitcoinex"}
]
end
defp description() do
"Bitcoinex is a Bitcoin Library for Elixir."
end
end

31
server/bitcoinex/mix.lock Normal file
View File

@ -0,0 +1,31 @@
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"credo": {:hex, :credo, "1.0.5", "fdea745579f8845315fe6a3b43e2f9f8866839cfbc8562bb72778e9fdaa94214", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "16105fac37c5c4b3f6e1f70ba0784511fec4275cd8bb979386e3c739cf4e6455"},
"decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm", "771ea78576e5fa505ad58a834f57915c7f5f9df11c87a598a01fdf6065ccfb5d"},
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "49496d63267bc1a4614ffd5f67c45d9fc3ea62701a6797975bc98bc156d2763f"},
"earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"},
"erlex": {:hex, :erlex, "0.2.1", "cee02918660807cbba9a7229cae9b42d1c6143b768c781fa6cee1eaf03ad860b", [:mix], [], "hexpm", "df65aa8e1e926941982b208f5957158a52b21fbba06ba8141fff2b8c5ce87574"},
"ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"},
"excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "493daf5a2dd92d022a1c29e7edcc30f1bce1ffe10fb3690fac63889346d3af2f"},
"faker": {:hex, :faker, "0.12.0", "796cbac868c86c2df6f273ea4cdf2e271860863820e479e04a374b7ee6c376b6", [:mix], [], "hexpm"},
"file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm", "b4cfa2d69c7f0b18fd06db222b2398abeef743a72504e6bd7df9c52f171b047f"},
"gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm", "dd3a7ea5e3e87ee9df29452dd9560709b4c7cc8141537d0b070155038d92bdf1"},
"hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "c2790c9f0f7205f4a362512192dee8179097394400e745e4d20bab7226a8eaad"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"},
"libsecp256k1": {:git, "https://github.com/RiverFinancial/libsecp256k1.git", "c2e4363ff69008ed99f1fa3befd4d7e72919397b", [branch: "add-spec"]},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mix_test_watch": {:hex, :mix_test_watch, "0.9.0", "c72132a6071261893518fa08e121e911c9358713f62794a90c95db59042af375", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "817dec4a7f6edf260258002f99ac8ffaf7a8f395b27bf2d13ec24018beecec8a"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm", "603561dc0fd62f4f2ea9b890f4e20e1a0d388746d6e20557cafb1b16950de88c"},
"stream_data": {:hex, :stream_data, "0.4.3", "62aafd870caff0849a5057a7ec270fad0eb86889f4d433b937d996de99e3db25", [:mix], [], "hexpm", "7dafd5a801f0bc897f74fcd414651632b77ca367a7ae4568778191fc3bf3a19a"},
"timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "b8fd8c9fcfaef1fa9c415e0792e2e82783c7ec8a282dfceef7d48158d4cfb3e1"},
"tzdata": {:hex, :tzdata, "0.5.20", "304b9e98a02840fb32a43ec111ffbe517863c8566eb04a061f1c4dbb90b4d84c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "e91aa533aeeff51dcb0f279de494823389ade9eb5cee703fe5e3236f622adbf0"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
}

View File

@ -0,0 +1,216 @@
defmodule Bitcoinex.AddressTest do
use ExUnit.Case
doctest Bitcoinex.Address
alias Bitcoinex.Address
describe "is_valid?/1" do
setup do
valid_mainnet_p2pkh_addresses = [
"12KYrjTdVGjFMtaxERSk3gphreJ5US8aUP",
"12QeMLzSrB8XH8FvEzPMVoRxVAzTr5XM2y",
"17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem",
"1oNLrsHnBcR6dpaBpwz3LSwutbUNkNSjs"
]
valid_testnet_p2pkh_addresses = [
"mzBc4XEFSdzCDcTxAgf6EZXgsZWpztRhef",
"mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn"
]
valid_mainnet_p2sh_addresses = [
"3NJZLcZEEYBpxYEUGewU4knsQRn1WM5Fkt"
]
valid_testnet_p2sh_addresses = [
"2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc"
]
valid_mainnet_segwit_addresses = [
"BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"
]
valid_testnet_segwit_addresses = [
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
"tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"
]
valid_regtest_segwit_addresses = [
"bcrt1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qzf4jry",
"bcrt1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvseswlauz7"
]
valid_mainnet_p2wpkh_addresses = [
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
]
valid_testnet_p2wpkh_addresses = [
"tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx"
]
valid_mainnet_p2wsh_addresses = [
"bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"
]
valid_testnet_p2wsh_addresses = [
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"
]
valid_mainnet_addresses =
valid_mainnet_p2pkh_addresses ++
valid_mainnet_p2sh_addresses ++
valid_mainnet_segwit_addresses ++
valid_mainnet_p2wpkh_addresses ++
valid_mainnet_p2wsh_addresses
valid_testnet_addresses =
valid_testnet_p2pkh_addresses ++
valid_testnet_p2sh_addresses ++
valid_testnet_segwit_addresses ++
valid_testnet_p2wpkh_addresses ++
valid_testnet_p2wsh_addresses
valid_regtest_addresses =
valid_testnet_p2pkh_addresses ++
valid_testnet_p2sh_addresses ++ valid_regtest_segwit_addresses
invalid_addresses = [
# witness v1 address
"bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx",
"BC1SW50QA3JX3S",
"bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj",
"",
"rrRmhfXzGBKbV4YHtbpxfA1ftEcry8AJaX",
"LSxNsEQekEpXMS4B7tUYstMEdMyH321ZQ1",
"rrRmhfXzGBKbV4YHtbpxfA1ftEcry8AJaX",
"tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty",
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5",
"BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2",
"bc1rw5uspcuh",
"bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90",
"BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P",
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7",
"bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du",
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv",
"bc1gmk9yu"
]
{:ok,
valid_mainnet_addresses: valid_mainnet_addresses,
valid_testnet_addresses: valid_testnet_addresses,
valid_regtest_addresses: valid_regtest_addresses,
valid_mainnet_p2pkh_addresses: valid_mainnet_p2pkh_addresses,
valid_testnet_p2pkh_addresses: valid_testnet_p2pkh_addresses,
valid_mainnet_p2sh_addresses: valid_mainnet_p2sh_addresses,
valid_testnet_p2sh_addresses: valid_testnet_p2sh_addresses,
valid_mainnet_segwit_addresses: valid_mainnet_segwit_addresses,
valid_testnet_segwit_addresses: valid_testnet_segwit_addresses,
valid_regtest_segwit_addresses: valid_regtest_segwit_addresses,
valid_mainnet_p2wpkh_addresses: valid_mainnet_p2wpkh_addresses,
valid_testnet_p2wpkh_addresses: valid_testnet_p2wpkh_addresses,
valid_mainnet_p2wsh_addresses: valid_mainnet_p2wsh_addresses,
valid_testnet_p2wsh_addresses: valid_testnet_p2wsh_addresses,
invalid_addresses: invalid_addresses}
end
test "return true when the address is valid address either p2sh, p2pkh, pwsh, p2wpkh", %{
valid_mainnet_p2pkh_addresses: valid_mainnet_p2pkh_addresses,
valid_mainnet_p2sh_addresses: valid_mainnet_p2sh_addresses,
valid_mainnet_segwit_addresses: valid_mainnet_segwit_addresses
} do
all_valid_addresses =
valid_mainnet_p2sh_addresses ++
valid_mainnet_p2pkh_addresses ++ valid_mainnet_segwit_addresses
for valid_address <- all_valid_addresses do
assert Address.is_valid?(valid_address, :mainnet)
end
end
test "return false when the address is valid address either p2sh, p2pkh, pwsh, p2wpkh but not in correct network",
%{
valid_testnet_segwit_addresses: valid_testnet_segwit_addresses,
valid_testnet_p2pkh_addresses: valid_testnet_p2pkh_addresses,
valid_testnet_p2sh_addresses: valid_testnet_p2sh_addresses,
valid_regtest_segwit_addresses: valid_regtest_segwit_addresses
} do
all_valid_testnet_addresses =
valid_testnet_segwit_addresses ++
valid_testnet_p2pkh_addresses ++
valid_testnet_p2sh_addresses ++ valid_regtest_segwit_addresses
for valid_testnet_address <- all_valid_testnet_addresses do
refute Address.is_valid?(valid_testnet_address, :mainnet)
end
end
test "return false when the address is not valid address either p2sh, p2pkh, pwsh, p2wpkh", %{
invalid_addresses: invalid_addresses
} do
all_invalid_addresses = invalid_addresses
for invalid_address <- all_invalid_addresses do
for %{name: network_name} <- Bitcoinex.Network.supported_networks() do
for address_type <- Bitcoinex.Address.supported_address_types() do
refute Address.is_valid?(invalid_address, network_name, address_type)
end
end
end
end
test "check that the address decodes to the correct address type", %{
valid_mainnet_p2pkh_addresses: valid_mainnet_p2pkh_addresses,
valid_testnet_p2pkh_addresses: valid_testnet_p2pkh_addresses,
valid_mainnet_p2sh_addresses: valid_mainnet_p2sh_addresses,
valid_testnet_p2sh_addresses: valid_testnet_p2sh_addresses,
valid_mainnet_p2wpkh_addresses: valid_mainnet_p2wpkh_addresses,
valid_testnet_p2wpkh_addresses: valid_testnet_p2wpkh_addresses,
valid_mainnet_p2wsh_addresses: valid_mainnet_p2wsh_addresses,
valid_testnet_p2wsh_addresses: valid_testnet_p2wsh_addresses
} do
for mainnet_p2pkh <- valid_mainnet_p2pkh_addresses do
assert Address.decode_type(mainnet_p2pkh, :mainnet) == {:ok, :p2pkh}
end
for testnet_p2pkh <- valid_testnet_p2pkh_addresses do
assert Address.decode_type(testnet_p2pkh, :testnet) == {:ok, :p2pkh}
end
for mainnet_p2sh <- valid_mainnet_p2sh_addresses do
assert Address.decode_type(mainnet_p2sh, :mainnet) == {:ok, :p2sh}
end
for testnet_p2sh <- valid_testnet_p2sh_addresses do
assert Address.decode_type(testnet_p2sh, :testnet) == {:ok, :p2sh}
end
for mainnet_p2wpkh <- valid_mainnet_p2wpkh_addresses do
assert Address.decode_type(mainnet_p2wpkh, :mainnet) == {:ok, :p2wpkh}
end
for testnet_p2wpkh <- valid_testnet_p2wpkh_addresses do
assert Address.decode_type(testnet_p2wpkh, :testnet) == {:ok, :p2wpkh}
end
for mainnet_p2wsh <- valid_mainnet_p2wsh_addresses do
assert Address.decode_type(mainnet_p2wsh, :mainnet) == {:ok, :p2wsh}
end
for testnet_p2wsh <- valid_testnet_p2wsh_addresses do
assert Address.decode_type(testnet_p2wsh, :testnet) == {:ok, :p2wsh}
end
end
end
describe "encode/3" do
test "return true for encoding p2pkh" do
pubkey_hash = Base.decode16!("6dcd022b3c5e6439238eb333ec1d6ddd1973b5ba", case: :lower)
assert "1B1aF9aUzxqgEviiCSe9u339hpUWLVWfxu" == Address.encode(pubkey_hash, :mainnet, :p2pkh)
end
test "return true for encoding p2sh" do
script_hash = Base.decode16!("6d77fa9de297e9c536c6b23cfda1a8450bb5f765", case: :lower)
assert "3BfqJjn7H2jsbKd2NVHGP4sQWQ2bQWBRLv" == Address.encode(script_hash, :mainnet, :p2sh)
end
end
end

View File

@ -0,0 +1,216 @@
defmodule Bitcoinex.Base58Test do
use ExUnit.Case
use ExUnitProperties
doctest Bitcoinex.Base58
alias Bitcoinex.Base58
# From
@base58_encode_decode [
["", ""],
["61", "2g"],
["626262", "a3gV"],
["636363", "aPEr"],
["73696d706c792061206c6f6e6720737472696e67", "2cFupjhnEsSn59qHXstmK2ffpLv2"],
["00eb15231dfceb60925886b67d065299925915aeb172c06647", "1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L"],
["516b6fcd0f", "ABnLTmg"],
["bf4f89001e670274dd", "3SEo3LWLoPntC"],
["572e4794", "3EFU7m"],
["ecac89cad93923c02321", "EJDM8drfXA6uyA"],
["10c8511e", "Rt5zm"],
["00000000000000000000", "1111111111"],
[
"000111d38e5fc9071ffcd20b4a763cc9ae4f252bb4e48fd66a835e252ada93ff480d6dd43dc62a641155a5",
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
],
[
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
"1cWB5HCBdLjAuqGGReWE3R3CguuwSjw6RHn39s2yuDRTS5NsBgNiFpWgAnEx6VQi8csexkgYw3mdYrMHr8x9i7aEwP8kZ7vccXWqKDvGv3u1GxFKPuAkn8JCPPGDMf3vMMnbzm6Nh9zh1gcNsMvH3ZNLmP5fSG6DGbbi2tuwMWPthr4boWwCxf7ewSgNQeacyozhKDDQQ1qL5fQFUW52QKUZDZ5fw3KXNQJMcNTcaB723LchjeKun7MuGW5qyCBZYzA1KjofN1gYBV3NqyhQJ3Ns746GNuf9N2pQPmHz4xpnSrrfCvy6TVVz5d4PdrjeshsWQwpZsZGzvbdAdN8MKV5QsBDY"
]
]
# From https://github.com/bitcoinjs/bs58check/blob/master/test/fixtures.json
@valid_base58_strings [
["1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i", "0065a16059864a2fdbc7c99a4723a8395bc6f188eb"],
["3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou", "0574f209f6ea907e2ea48f74fae05782ae8a665257"],
["mo9ncXisMeAoXwqcV5EWuyncbmCcQN4rVs", "6f53c0307d6851aa0ce7825ba883c6bd9ad242b486"],
["2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br", "c46349a418fc4578d10a372b54b45c280cc8c4382f"],
[
"5Kd3NBUAdUnhyzenEwVLy9pBKxSwXvE9FMPyR4UKZvpe6E3AgLr",
"80eddbdc1168f1daeadbd3e44c1e3f8f5a284c2029f78ad26af98583a499de5b19"
],
[
"Kz6UJmQACJmLtaQj5A3JAge4kVTNQ8gbvXuwbmCj7bsaabudb3RD",
"8055c9bccb9ed68446d1b75273bbce89d7fe013a8acd1625514420fb2aca1a21c401"
],
[
"9213qJab2HNEpMpYNBa7wHGFKKbkDn24jpANDs2huN3yi4J11ko",
"ef36cb93b9ab1bdabf7fb9f2c04f1b9cc879933530ae7842398eef5a63a56800c2"
],
[
"cTpB4YiyKiBcPxnefsDpbnDxFDffjqJob8wGCEDXxgQ7zQoMXJdH",
"efb9f4892c9e8282028fea1d2667c4dc5213564d41fc5783896a0d843fc15089f301"
],
["1Ax4gZtb7gAit2TivwejZHYtNNLT18PUXJ", "006d23156cbbdcc82a5a47eee4c2c7c583c18b6bf4"],
["3QjYXhTkvuj8qPaXHTTWb5wjXhdsLAAWVy", "05fcc5460dd6e2487c7d75b1963625da0e8f4c5975"],
["n3ZddxzLvAY9o7184TB4c6FJasAybsw4HZ", "6ff1d470f9b02370fdec2e6b708b08ac431bf7a5f7"],
["2NBFNJTktNa7GZusGbDbGKRZTxdK9VVez3n", "c4c579342c2c4c9220205e2cdc285617040c924a0a"],
[
"5K494XZwps2bGyeL71pWid4noiSNA2cfCibrvRWqcHSptoFn7rc",
"80a326b95ebae30164217d7a7f57d72ab2b54e3be64928a19da0210b9568d4015e"
],
[
"L1RrrnXkcKut5DEMwtDthjwRcTTwED36thyL1DebVrKuwvohjMNi",
"807d998b45c219a1e38e99e7cbd312ef67f77a455a9b50c730c27f02c6f730dfb401"
],
[
"93DVKyFYwSN6wEo3E2fCrFPUp17FtrtNi2Lf7n4G3garFb16CRj",
"efd6bca256b5abc5602ec2e1c121a08b0da2556587430bcf7e1898af2224885203"
],
[
"cTDVKtMGVYWTHCb1AFjmVbEbWjvKpKqKgMaR3QJxToMSQAhmCeTN",
"efa81ca4e8f90181ec4b61b6a7eb998af17b2cb04de8a03b504b9e34c4c61db7d901"
],
["1C5bSj1iEGUgSTbziymG7Cn18ENQuT36vv", "007987ccaa53d02c8873487ef919677cd3db7a6912"],
["3AnNxabYGoTxYiTEZwFEnerUoeFXK2Zoks", "0563bcc565f9e68ee0189dd5cc67f1b0e5f02f45cb"],
["n3LnJXCqbPjghuVs8ph9CYsAe4Sh4j97wk", "6fef66444b5b17f14e8fae6e7e19b045a78c54fd79"],
["2NB72XtkjpnATMggui83aEtPawyyKvnbX2o", "c4c3e55fceceaa4391ed2a9677f4a4d34eacd021a0"],
[
"5KaBW9vNtWNhc3ZEDyNCiXLPdVPHCikRxSBWwV9NrpLLa4LsXi9",
"80e75d936d56377f432f404aabb406601f892fd49da90eb6ac558a733c93b47252"
],
[
"L1axzbSyynNYA8mCAhzxkipKkfHtAXYF4YQnhSKcLV8YXA874fgT",
"808248bd0375f2f75d7e274ae544fb920f51784480866b102384190b1addfbaa5c01"
],
[
"927CnUkUbasYtDwYwVn2j8GdTuACNnKkjZ1rpZd2yBB1CLcnXpo",
"ef44c4f6a096eac5238291a94cc24c01e3b19b8d8cef72874a079e00a242237a52"
],
[
"cUcfCMRjiQf85YMzzQEk9d1s5A4K7xL5SmBCLrezqXFuTVefyhY7",
"efd1de707020a9059d6d3abaf85e17967c6555151143db13dbb06db78df0f15c6901"
],
["1Gqk4Tv79P91Cc1STQtU3s1W6277M2CVWu", "00adc1cc2081a27206fae25792f28bbc55b831549d"],
["33vt8ViH5jsr115AGkW6cEmEz9MpvJSwDk", "05188f91a931947eddd7432d6e614387e32b244709"],
["mhaMcBxNh5cqXm4aTQ6EcVbKtfL6LGyK2H", "6f1694f5bc1a7295b600f40018a618a6ea48eeb498"],
["2MxgPqX1iThW3oZVk9KoFcE5M4JpiETssVN", "c43b9b3fd7a50d4f08d1a5b0f62f644fa7115ae2f3"],
[
"5HtH6GdcwCJA4ggWEL1B3jzBBUB8HPiBi9SBc5h9i4Wk4PSeApR",
"80091035445ef105fa1bb125eccfb1882f3fe69592265956ade751fd095033d8d0"
],
[
"L2xSYmMeVo3Zek3ZTsv9xUrXVAmrWxJ8Ua4cw8pkfbQhcEFhkXT8",
"80ab2b4bcdfc91d34dee0ae2a8c6b6668dadaeb3a88b9859743156f462325187af01"
],
[
"92xFEve1Z9N8Z641KQQS7ByCSb8kGjsDzw6fAmjHN1LZGKQXyMq",
"efb4204389cef18bbe2b353623cbf93e8678fbc92a475b664ae98ed594e6cf0856"
],
[
"cVM65tdYu1YK37tNoAyGoJTR13VBYFva1vg9FLuPAsJijGvG6NEA",
"efe7b230133f1b5489843260236b06edca25f66adb1be455fbd38d4010d48faeef01"
],
["1JwMWBVLtiqtscbaRHai4pqHokhFCbtoB4", "00c4c1b72491ede1eedaca00618407ee0b772cad0d"],
["3QCzvfL4ZRvmJFiWWBVwxfdaNBT8EtxB5y", "05f6fe69bcb548a829cce4c57bf6fff8af3a5981f9"],
["mizXiucXRCsEriQCHUkCqef9ph9qtPbZZ6", "6f261f83568a098a8638844bd7aeca039d5f2352c0"],
["2NEWDzHWwY5ZZp8CQWbB7ouNMLqCia6YRda", "c4e930e1834a4d234702773951d627cce82fbb5d2e"],
[
"5KQmDryMNDcisTzRp3zEq9e4awRmJrEVU1j5vFRTKpRNYPqYrMg",
"80d1fab7ab7385ad26872237f1eb9789aa25cc986bacc695e07ac571d6cdac8bc0"
],
[
"L39Fy7AC2Hhj95gh3Yb2AU5YHh1mQSAHgpNixvm27poizcJyLtUi",
"80b0bbede33ef254e8376aceb1510253fc3550efd0fcf84dcd0c9998b288f166b301"
],
[
"91cTVUcgydqyZLgaANpf1fvL55FH53QMm4BsnCADVNYuWuqdVys",
"ef037f4192c630f399d9271e26c575269b1d15be553ea1a7217f0cb8513cef41cb"
],
[
"cQspfSzsgLeiJGB2u8vrAiWpCU4MxUT6JseWo2SjXy4Qbzn2fwDw",
"ef6251e205e8ad508bab5596bee086ef16cd4b239e0cc0c5d7c4e6035441e7d5de01"
],
["19dcawoKcZdQz365WpXWMhX6QCUpR9SY4r", "005eadaf9bb7121f0f192561a5a62f5e5f54210292"],
["37Sp6Rv3y4kVd1nQ1JV5pfqXccHNyZm1x3", "053f210e7277c899c3a155cc1c90f4106cbddeec6e"],
["myoqcgYiehufrsnnkqdqbp69dddVDMopJu", "6fc8a3c2a09a298592c3e180f02487cd91ba3400b5"],
["2N7FuwuUuoTBrDFdrAZ9KxBmtqMLxce9i1C", "c499b31df7c9068d1481b596578ddbb4d3bd90baeb"],
[
"5KL6zEaMtPRXZKo1bbMq7JDjjo1bJuQcsgL33je3oY8uSJCR5b4",
"80c7666842503db6dc6ea061f092cfb9c388448629a6fe868d068c42a488b478ae"
],
[
"KwV9KAfwbwt51veZWNscRTeZs9CKpojyu1MsPnaKTF5kz69H1UN2",
"8007f0803fc5399e773555ab1e8939907e9badacc17ca129e67a2f5f2ff84351dd01"
],
[
"93N87D6uxSBzwXvpokpzg8FFmfQPmvX4xHoWQe3pLdYpbiwT5YV",
"efea577acfb5d1d14d3b7b195c321566f12f87d2b77ea3a53f68df7ebf8604a801"
],
[
"cMxXusSihaX58wpJ3tNuuUcZEQGt6DKJ1wEpxys88FFaQCYjku9h",
"ef0b3b34f0958d8a268193a9814da92c3e8b58b4a4378a542863e34ac289cd830c01"
],
["13p1ijLwsnrcuyqcTvJXkq2ASdXqcnEBLE", "001ed467017f043e91ed4c44b4e8dd674db211c4e6"],
["3ALJH9Y951VCGcVZYAdpA3KchoP9McEj1G", "055ece0cadddc415b1980f001785947120acdb36fc"]
]
@invalid_base58_strings [
["Z9inZq4e2HGQRZQezDjFMmqgUE8NwMRok", "Invalid checksum"],
["3HK7MezAm6qEZQUMPRf8jX7wDv6zig6Ky8", "Invalid checksum"],
["3AW8j12DUk8mgA7kkfZ1BrrzCVFuH1LsXS", "Invalid checksum"]
# ["#####", "Non-base58 character"] # TODO: handle gracefully
]
describe "decode_base!/1" do
test "decode_base! properly decodes base58 encoded strings" do
for pair <- @base58_encode_decode do
[base16_str, base58_str] = pair
base16_bin = Base.decode16!(base16_str, case: :lower)
assert base16_bin == Base58.decode_base!(base58_str)
end
end
end
describe "encode_base!/1" do
test "properly encodes to base58" do
for pair <- @base58_encode_decode do
[base16_str, base58_str] = pair
base16_bin = Base.decode16!(base16_str, case: :lower)
assert base58_str == Base58.encode_base(base16_bin)
end
end
end
describe "encode/1" do
test "properly encodes Base58" do
for pair <- @valid_base58_strings do
[base58_str, base16_str] = pair
base16_bin = Base.decode16!(base16_str, case: :lower)
assert base58_str == Base58.encode(base16_bin)
# double check
{:ok, _decoded} = Base58.decode(base58_str)
end
end
end
describe "decode/1" do
test "properly decodes Base58" do
for pair <- @valid_base58_strings do
[base58_str, base16_str] = pair
base16_bin = Base.decode16!(base16_str, case: :lower)
{:ok, decoded} = Base58.decode(base58_str)
assert base16_bin == decoded
end
end
test "catches invalid checksums" do
for pair <- @invalid_base58_strings do
[base58_str, _base16_str] = pair
assert {:error, :invalid_checksum} = Base58.decode(base58_str)
end
end
end
end

View File

@ -0,0 +1,298 @@
defmodule Bitcoinex.Bech32Test do
use ExUnit.Case
doctest Bitcoinex.Bech32
alias Bitcoinex.Bech32
# Bech32
@valid_bech32 [
"A12UEL5L",
"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs",
"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw",
"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j",
"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w"
]
@invalid_bech32_hrp_char_out_of_range [
<<0x20, "1nwldj5">>,
<<0x7F, "1axkwrx">>,
<<0x90::utf8, "1eym55h">>
]
@invalid_bech32_max_length_exceeded [
"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx"
]
@invalid_bech32_no_separator_character [
"pzry9x0s0muk"
]
@invalid_bech32_empty_hrp [
"1pzry9x0s0muk",
"10a06t8",
"1qzzfhee"
]
@invalid_bech32_checksum [
"A12UEL5A"
]
@invalid_bech32_invalid_data_character [
"x1b4n0q5v"
]
@invalid_bech32_too_short_checksum [
"li1dgmt3"
]
@invalid_bech32_invalid_character_in_checksum [
<<"de1lg7wt", 0xFF::utf8>>
]
# Bech32m
@valid_bech32m [
"A1LQFN3A",
"a1lqfn3a",
"an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6",
"abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx",
"11llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllludsr8",
"split1checkupstagehandshakeupstreamerranterredcaperredlc445v",
"?1v759aa"
]
@invalid_bech32m_hrp_char_out_of_range [
<<0x20, "1xj0phk">>,
<<0x7F, "1g6xzxy">>,
<<0x90::utf8, "1vctc34">>
]
@invalid_bech32m_max_length_exceeded [
"an84characterslonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11d6pts4"
]
@invalid_bech32m_no_separator_character [
"qyrz8wqd2c9m"
]
@invalid_bech32m_empty_hrp [
"1qyrz8wqd2c9m",
"16plkw9",
"1p2gdwpf"
]
@invalid_bech32m_checksum [
"M1VUXWEZ"
]
@invalid_bech32m_invalid_data_character [
"y1b0jsk6g",
"lt1igcx5c0"
]
@invalid_bech32m_too_short_checksum [
"in1muywd"
]
@invalid_bech32m_invalid_character_in_checksum [
"mm1crxm3i",
"au1s5cgom"
]
describe "decode/1 for bech32" do
test "successfully decode with valid bech32" do
for bech <- @valid_bech32 do
assert {:ok, {:bech32, hrp, data}} = Bech32.decode(bech)
assert hrp != nil
# encode after decode should be the same(after downcase) as before
{:ok, new_bech} = Bech32.encode(hrp, data, :bech32)
assert new_bech == String.downcase(bech)
end
end
test "fail to decode with invalid bech32 out of ranges" do
for bech <- @invalid_bech32_hrp_char_out_of_range do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :hrp_char_out_opf_range
end
end
test "fail to decode with invalid bech32 overall max length exceeded" do
for bech <- @invalid_bech32_max_length_exceeded do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :overall_max_length_exceeded
end
end
test "fail to decode with invalid bech32 no separator character" do
for bech <- @invalid_bech32_no_separator_character do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :no_separator_character
end
end
test "fail to decode with invalid bech32 empty hrp" do
for bech <- @invalid_bech32_empty_hrp do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :empty_hrp
end
end
test "fail to decode with invalid data character" do
for bech <- @invalid_bech32_invalid_data_character do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :contain_invalid_data_char
end
end
test "fail to decode with too short checksum" do
for bech <- @invalid_bech32_too_short_checksum do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :too_short_checksum
end
end
test "fail to decode with invalid character in checksum" do
for bech <- @invalid_bech32_invalid_character_in_checksum do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :contain_invalid_data_char
end
end
test "fail to decode with invalid checksum" do
for bech <- @invalid_bech32_checksum do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :invalid_checksum
end
end
end
describe "decode/1 for bech32m" do
test "successfully decode with valid bech32" do
for bech <- @valid_bech32m do
assert {:ok, {:bech32m, hrp, data}} = Bech32.decode(bech)
assert hrp != nil
# encode after decode should be the same(after downcase) as before
{:ok, new_bech} = Bech32.encode(hrp, data, :bech32m)
assert new_bech == String.downcase(bech)
end
end
test "fail to decode with invalid bech32m out of ranges" do
for bech <- @invalid_bech32m_hrp_char_out_of_range do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :hrp_char_out_opf_range
end
end
test "fail to decode with invalid bech32m overall max length exceeded" do
for bech <- @invalid_bech32m_max_length_exceeded do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :overall_max_length_exceeded
end
end
test "fail to decode with invalid bech32m no separator character" do
for bech <- @invalid_bech32m_no_separator_character do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :no_separator_character
end
end
test "fail to decode with invalid bech32m empty hrp" do
for bech <- @invalid_bech32m_empty_hrp do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :empty_hrp
end
end
test "fail to decode with invalid data character" do
for bech <- @invalid_bech32m_invalid_data_character do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :contain_invalid_data_char
end
end
test "fail to decode with too short checksum" do
for bech <- @invalid_bech32m_too_short_checksum do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :too_short_checksum
end
end
test "fail to decode with invalid character in checksum" do
for bech <- @invalid_bech32m_invalid_character_in_checksum do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :contain_invalid_data_char
end
end
test "fail to decode with invalid checksum" do
for bech <- @invalid_bech32m_checksum do
assert {:error, msg} = Bech32.decode(bech)
assert msg == :invalid_checksum
end
end
end
describe "encode/2 for bech32" do
test "successfully encode with valid hrp and empty data" do
assert {:ok, _bech} = Bech32.encode("bc", [], :bech32)
end
test "successfully encode with valid hrp and non empty valid data" do
assert {:ok, _bech} = Bech32.encode("bc", [1, 2], :bech32)
end
test "successfully encode with invalid string data" do
assert {:ok, _bech} = Bech32.encode("bc", "qpzry9x8gf2tvdw0s3jn54khce6mua7l", :bech32)
end
test "fail to encode with valid hrp and non empty valid string data" do
assert {:error, bech} = Bech32.encode("bc", "qpzry9x8gf2tvdw0s3jn54khce6mua7lo", :bech32)
end
test "fail to encode with overall encoded length is over 90" do
data = for _ <- 1..82, do: 1
assert {:error, :overall_max_length_exceeded} = Bech32.encode("bc", data, :bech32)
end
test "fail to encode with hrp contain invalid char(out of 1 to 83 US-ASCII)" do
data = [1]
assert {:error, :hrp_char_out_opf_range} = Bech32.encode(" ", data, :bech32)
assert {:error, :hrp_char_out_opf_range} = Bech32.encode("", data, :bech32)
assert {:error, :hrp_char_out_opf_range} = Bech32.encode("中文", data, :bech32)
end
end
describe "encode/2 for bech32m" do
test "successfully encode with valid hrp and empty data" do
assert {:ok, _bech} = Bech32.encode("bc", [], :bech32m)
end
test "successfully encode with valid hrp and non empty valid data" do
assert {:ok, _bech} = Bech32.encode("bc", [1, 2], :bech32m)
end
test "successfully encode with invalid string data" do
assert {:ok, _bech} = Bech32.encode("bc", "qpzry9x8gf2tvdw0s3jn54khce6mua7l", :bech32m)
end
test "fail to encode with valid hrp and non empty valid string data" do
assert {:error, bech} = Bech32.encode("bc", "qpzry9x8gf2tvdw0s3jn54khce6mua7lo", :bech32m)
end
test "fail to encode with overall encoded length is over 90" do
data = for _ <- 1..82, do: 1
assert {:error, :overall_max_length_exceeded} = Bech32.encode("bc", data, :bech32m)
end
test "fail to encode with hrp contain invalid char(out of 1 to 83 US-ASCII)" do
data = [1]
assert {:error, :hrp_char_out_opf_range} = Bech32.encode(" ", data, :bech32m)
assert {:error, :hrp_char_out_opf_range} = Bech32.encode("", data, :bech32m)
assert {:error, :hrp_char_out_opf_range} = Bech32.encode("中文", data, :bech32m)
end
end
end

View File

@ -0,0 +1,503 @@
defmodule Bitcoinex.LightningNetwork.InvoiceTest do
use ExUnit.Case
doctest Bitcoinex.Segwit
alias Bitcoinex.LightningNetwork.{Invoice, HopHint}
setup_all do
test_payment_hash = "0001020304050607080900010203040506070809000102030405060708090102"
test_description_hash_slice =
Bitcoinex.Utils.sha256(
"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"
)
|> Base.encode16(case: :lower)
test_description_coffee = "1 cup coffee"
test_description_coffee_japanese = "ナンセンス 1杯"
test_description_blockstream_ledger =
"Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items"
# testHopHintPubkeyBytes1 = Base.decode64!("")
testHopHintPubkey1 = "029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"
testHopHintPubkey2 = "039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"
testHopHintPubkey3 = "03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"
testSingleHop = [
%HopHint{
node_id: testHopHintPubkey1,
channel_id: 0x0102030405060708,
fee_base_m_sat: 0,
fee_proportional_millionths: 20,
cltv_expiry_delta: 3
}
]
testDoubleHop = [
%HopHint{
node_id: testHopHintPubkey1,
channel_id: 0x0102030405060708,
fee_base_m_sat: 1,
fee_proportional_millionths: 20,
cltv_expiry_delta: 3
},
%HopHint{
node_id: testHopHintPubkey2,
channel_id: 0x030405060708090A,
fee_base_m_sat: 2,
fee_proportional_millionths: 30,
cltv_expiry_delta: 4
}
]
test_address_testnet_P2PKH = "mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP"
test_address_mainnet_P2PKH = "1RustyRX2oai4EYYDpQGWvEL62BBGqN9T"
test_address_mainnet_P2SH = "3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX"
test_address_mainnet_P2WPKH = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
test_address_mainnet_P2WSH = "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"
test_pubkey = "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"
valid_encoded_invoices = [
# Please send $3 for a cup of coffee to the same peer, within one minute
{
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 250_000_000,
timestamp: 1_496_314_658,
description: test_description_coffee,
expiry: 60,
min_final_cltv_expiry: 18
}
},
# pubkey set in 'n' field.
{
"lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66jd3m5klcwhq68vdsmx2rjgxeay5v0tkt2v5sjaky4eqahe4fx3k9sqavvce3capfuwv8rvjng57jrtfajn5dkpqv8yelsewtljwmmycq62k443",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 2_400_000_000_000,
timestamp: 1_503_429_093,
description: "",
min_final_cltv_expiry: 18
}
},
# Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad
{
"lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w",
%Invoice{
network: :mainnet,
destination: test_pubkey,
description: "Please consider supporting this project",
payment_hash: test_payment_hash,
timestamp: 1_496_314_658,
min_final_cltv_expiry: 18
}
},
# Has a few unknown fields, should just be ignored.
{
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqtq2v93xxer9vczq8v93xxeqv72xr42ca60022jqu6fu73n453tmnr0ukc0pl0t23w7eavtensjz0j2wcu7nkxhfdgp9y37welajh5kw34mq7m4xuay0a72cwec8qwgqt5vqht",
%Invoice{
network: :mainnet,
destination: test_pubkey,
description: "Please consider supporting this project",
payment_hash: test_payment_hash,
amount_msat: 2_000_000_000,
timestamp: 1_496_314_658,
min_final_cltv_expiry: 18
}
},
# Please send 0.0025 BTC for a cup of nonsense (ナンセンス 1杯) to the same peer, within one minute
{
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 250_000_000,
timestamp: 1_496_314_658,
description: test_description_coffee_japanese,
expiry: 60,
min_final_cltv_expiry: 18
}
},
# Now send $24 for an entire list of things (hashed)
{
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7khhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 2_000_000_000,
timestamp: 1_496_314_658,
description_hash: test_description_hash_slice,
min_final_cltv_expiry: 18
}
},
# The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP
{
"lntb20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un98kmzzhznpurw9sgl2v0nklu2g4d0keph5t7tj9tcqd8rexnd07ux4uv2cjvcqwaxgj7v4uwn5wmypjd5n69z2xm3xgksg28nwht7f6zspwp3f9t",
%Invoice{
network: :testnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 2_000_000_000,
timestamp: 1_496_314_658,
description_hash: test_description_hash_slice,
fallback_address: test_address_testnet_P2PKH,
min_final_cltv_expiry: 18
}
},
# 1 sat with chinese description in testnet
{
"lntb10n1pwt8uswpp5r7j8v60vnevkhxls93x2zp3xyu7z65a4368wh0en8fl70vpypa2sdpzfehjqstdda6kuapqwa5hg6pquju2me5ksucqzys5qzh8dzpqjz7k7pdlal68ew2vx0y9rwaqth758mu0yu0v367kuc8typ08g7tnhh3a7v53svay2efvn7fwah8pesjsgvwrdpjjj795gqp0g4utq",
%Invoice{
network: :testnet,
destination: "0260d9119979caedc570ada883ff614c6efb93f7f7382e25d73ecbeba0b62df2d7",
description: "No Amount with 中文",
payment_hash: "1fa47669ec9e596b9bf02c4ca10626273c2d53b58e8eebbf333a7fe7b0240f55",
amount_msat: 1000,
timestamp: 1_555_296_782,
min_final_cltv_expiry: 144
}
},
# 1 hophint
{
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85frzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqqqqqqq9qqqvncsk57n4v9ehw86wq8fzvjejhv9z3w3q5zh6qkql005x9xl240ch23jk79ujzvr4hsmmafyxghpqe79psktnjl668ntaf4ne7ucs5csqh5mnnk",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 2_000_000_000,
timestamp: 1_496_314_658,
description_hash: test_description_hash_slice,
fallback_address: test_address_mainnet_P2PKH,
route_hints: testSingleHop,
min_final_cltv_expiry: 18
}
},
# two hophint
{
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 2_000_000_000,
timestamp: 1_496_314_658,
description_hash: test_description_hash_slice,
fallback_address: test_address_mainnet_P2PKH,
route_hints: testDoubleHop,
min_final_cltv_expiry: 18
}
},
# On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX
{
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppj3a24vwu6r8ejrss3axul8rxldph2q7z9kk822r8plup77n9yq5ep2dfpcydrjwzxs0la84v3tfw43t3vqhek7f05m6uf8lmfkjn7zv7enn76sq65d8u9lxav2pl6x3xnc2ww3lqpagnh0u",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 2_000_000_000,
timestamp: 1_496_314_658,
description_hash: test_description_hash_slice,
fallback_address: test_address_mainnet_P2SH,
min_final_cltv_expiry: 18
}
},
# # On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
{
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppqw508d6qejxtdg4y5r3zarvary0c5xw7kknt6zz5vxa8yh8jrnlkl63dah48yh6eupakk87fjdcnwqfcyt7snnpuz7vp83txauq4c60sys3xyucesxjf46yqnpplj0saq36a554cp9wt865",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 2_000_000_000,
timestamp: 1_496_314_658,
description_hash: test_description_hash_slice,
fallback_address: test_address_mainnet_P2WPKH,
min_final_cltv_expiry: 18
}
},
# On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3
{
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qvnjha2auylmwrltv2pkp2t22uy8ura2xsdwhq5nm7s574xva47djmnj2xeycsu7u5v8929mvuux43j0cqhhf32wfyn2th0sv4t9x55sppz5we8",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 2_000_000_000,
timestamp: 1_496_314_658,
description_hash: test_description_hash_slice,
fallback_address: test_address_mainnet_P2WSH,
min_final_cltv_expiry: 18
}
},
# Ignore unknown witness version in fallback address.
{
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpppw508d6qejxtdg4y5r3zarvary0c5xw7k8txqv6x0a75xuzp0zsdzk5hq6tmfgweltvs6jk5nhtyd9uqksvr48zga9mw08667w8264gkspluu66jhtcmct36nx363km6cquhhv2cpc6q43r",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 2_000_000_000,
timestamp: 1_496_314_658,
description_hash: test_description_hash_slice,
min_final_cltv_expiry: 18
}
},
# Ignore fields with unknown lengths.
{
"lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqpp3qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqshp38yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66np3q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfy8huflvs2zwkymx47cszugvzn5v64ahemzzlmm62rpn9l9rm05h35aceq00tkt296289wepws9jh4499wq2l0vk6xcxffd90dpuqchqqztyayq",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 2_400_000_000_000,
timestamp: 1_503_429_093,
description_hash: test_description_hash_slice,
min_final_cltv_expiry: 18
}
},
# # Send 2500uBTC for a cup of coffee with a custom CLTV expiry value.
{
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jscqzysnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66ysxkvnxhcvhz48sn72lp77h4fxcur27z0he48u5qvk3sxse9mr9jhkltt962s8arjnzk8rk59yj5nw4p495747gksj30gza0crhzwjcpgxzy00",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: test_payment_hash,
amount_msat: 250_000_000,
timestamp: 1_496_314_658,
description: test_description_coffee,
min_final_cltv_expiry: 144
}
},
{
"lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9qn07ytgrxxzad9hc4xt3mawjjt8znfv8xzscs7007v9gh9j569lencxa8xeujzkxs0uamak9aln6ez02uunw6rd2ht2sqe4hz8thcdagpleym0j",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: "462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f",
amount_msat: 967_878_534,
timestamp: 1_572_468_703,
description: test_description_blockstream_ledger,
min_final_cltv_expiry: 10,
expiry: 604_800,
route_hints: [
%HopHint{
node_id: testHopHintPubkey3,
channel_id: 0x08FE4E000CF00001,
fee_base_m_sat: 1000,
fee_proportional_millionths: 2500,
cltv_expiry_delta: 40
}
]
}
},
# TODO parsing payment secret
{
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
amount_msat: 2_500_000_000,
timestamp: 1_496_314_658,
description: "coffee beans",
min_final_cltv_expiry: 18
}
},
# Same, but all upper case
{
"LNBC25M1PVJLUEZPP5QQQSYQCYQ5RQWZQFQQQSYQCYQ5RQWZQFQQQSYQCYQ5RQWZQFQYPQDQ5VDHKVEN9V5SXYETPDEESSP5ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYGS9Q5SQQQQQQQQQQQQQQQPQSQ67GYE39HFG3ZD8RGC80K32TVY9XK2XUNWM5LZEXNVPX6FD77EN8QAQ424DXGT56CAG2DPT359K3SSYHETKTKPQH24JQNJYW6UQD08SGPTQ44QU",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
amount_msat: 2_500_000_000,
timestamp: 1_496_314_658,
description: "coffee beans",
min_final_cltv_expiry: 18
}
},
# Same, but including fields which must be ignored.
{
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq2qrqqqfppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqspnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnp5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnpkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2jxxfsnucm4jf4zwtznpaxphce606fvhvje5x7d4gw7n73994hgs7nteqvenq8a4ml8aqtchv5d9pf7l558889hp4yyrqv6a7zpq9fgpskqhza",
%Invoice{
network: :mainnet,
destination: test_pubkey,
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
amount_msat: 2_500_000_000,
timestamp: 1_496_314_658,
description: "coffee beans",
min_final_cltv_expiry: 18
}
},
{
"lnbcrt320u1pwt8mp3pp57xs8x6cs28zedru0r0hurkz6932e86dvlrzhwvm09azv57qcekxsdqlv9k8gmeqw3jhxarfdenjqumfd4cxcegcqzpgctyyv3qkvr6khzlnd7de95hrxkw8ewfhmyzuu9dh4sgauukpk5mryaex2qs39ksupm8sxj5jsh3hw3fa0gwdjchh7ga8cx7l652g5dgqzp2ddj",
%Invoice{
network: :regtest,
destination: "03f54387039a2932bca652e9fca1d0eb141a7f9c570979a2c469469a8083c73b47",
payment_hash: "f1a0736b1051c5968f8f1befc1d85a2c5593e9acf8c577336f2f44ca7818cd8d",
amount_msat: 32_000_000,
timestamp: 1_555_295_281,
description: "alto testing simple",
min_final_cltv_expiry: 40,
expiry: 3600
}
},
{
"lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z9kmrgvr7xlaqm47apw3d48zm203kzcq357a4ls9al2ea73r8jcceyjtya6fu5wzzpe50zrge6ulk4nvjcpxlekvmxl6qcs9j3tz0469gq5g658y",
%Invoice{
network: :mainnet,
destination: "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad",
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
amount_msat: 2_000_000_000,
timestamp: 1_496_314_658,
description_hash: "3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1",
min_final_cltv_expiry: 18,
fallback_address: "3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX"
}
},
{
"lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7kepvrhrm9s57hejg0p662ur5j5cr03890fa7k2pypgttmh4897d3raaq85a293e9jpuqwl0rnfuwzam7yr8e690nd2ypcq9hlkdwdvycqa0qza8",
%Invoice{
network: :mainnet,
destination: "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad",
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
amount_msat: 2_000_000_000,
timestamp: 1_496_314_658,
description_hash: "3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1",
min_final_cltv_expiry: 18,
fallback_address: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
}
},
{
"lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q28j0v3rwgy9pvjnd48ee2pl8xrpxysd5g44td63g6xcjcu003j3qe8878hluqlvl3km8rm92f5stamd3jw763n3hck0ct7p8wwj463cql26ava",
%Invoice{
network: :mainnet,
destination: "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad",
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
amount_msat: 2_000_000_000,
timestamp: 1_496_314_658,
description_hash: "3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1",
min_final_cltv_expiry: 18,
fallback_address: "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"
}
},
{
"lnbc1230p1pwpw4vhpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq8w3jhxaqxqrrsscqpfmmcvd29nucsnyapspmkpqqf65uedt4zvhqkstelrgyk4nfvwka38c3nlq06agjmazs9nr3uupxp6r0v0gzw4n26redc36urkqwxamqqqu7esys",
%Invoice{
network: :mainnet,
destination: "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad",
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
amount_msat: 123,
timestamp: 1_545_033_111,
description: "test",
min_final_cltv_expiry: 9,
expiry: 3600
}
},
{
"lnbcrt1230n1pwt8m44pp56zkej4pmwp273agvad0ljscuq4gsc072xlttlgrzrzpmlwzzhzksdq2wd5k6urvv5cqzpgym4nl5gt8xqy69t4cuwf025xnv968xpgvv30h387whfur5y9hq9h5sd8hkumauluj4dn9kqche8glswdpvc2lu4yua3atkyaefuzuqqp27dfsw",
%Invoice{
network: :regtest,
destination: "03f54387039a2932bca652e9fca1d0eb141a7f9c570979a2c469469a8083c73b47",
payment_hash: "d0ad99543b7055e8f50ceb5ff9431c05510c3fca37d6bfa0621883bfb842b8ad",
amount_msat: 123_000,
timestamp: 1_555_295_925,
description: "simple",
min_final_cltv_expiry: 40
}
}
]
invalid_encoded_invoices = [
# no hrp,
"asdsaddnasdnas",
# too short
"lnbc1abcde",
# empty hrp
"1asdsaddnv4wudz",
# hrp too short
"ln1asdsaddnv4wudz",
# no "ln" prefix
"llts1dasdajtkfl6",
# invalid segwit prefix
"lnts1dasdapukz0w",
# invalid amount
"lnbcm1aaamcu25m",
# invalid amount
"lnbc1000000000m1",
# empty fallback address field
"lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfqqepvrhrm9s57hejg0p662ur5j5cr03890fa7k2pypgttmh4897d3raaq85a293e9jpuqwl0rnfuwzam7yr8e690nd2ypcq9hlkdwdvycqjhlqg5",
# invalid routing info length: not a multiple of 51
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85frqg00000000j9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqsj5cgu",
# no payment hash set
"lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsjv38luh6p6s2xrv3mzvlmzaya43376h0twal5ax0k6p47498hp3hnaymzhsn424rxqjs0q7apn26yrhaxltq3vzwpqj9nc2r3kzwccsplnq470",
# Both Description and DescriptionHash set.
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs03vghs8y0kuj4ulrzls8ln7fnm9dk7sjsnqmghql6hd6jut36clkqpyuq0s5m6fhureyz0szx2qjc8hkgf4xc2hpw8jpu26jfeyvf4cpga36gt",
# Neither Description nor DescriptionHash set.
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqn2rne0kagfl4e0xag0w6hqeg2dwgc54hrm9m0auw52dhwhwcu559qav309h598pyzn69wh2nqauneyyesnpmaax0g6acr8lh9559jmcquyq5a9",
# mixed case
"lnbc2500u1PvJlUeZpP5QqQsYqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp",
# Lightning Payment Request signature pubkey does not match payee pubkey
"lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durllllll72gy6kphxuhh4a2ffwf9344ytfw98tyhvslsp9y5vt2uxdfhpucph83eqms28dyde9yxgu5ehln4zkwv04nvurxhst77vnng5s0ar9mqpm3cg0l",
# Bech32 checksum is invalid
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrnt",
# Malformed bech32 string (no 1)
"pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny",
# Malformed bech32 string (mixed case)
"LNBC2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny",
# Signature is not recoverable.
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaxtrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspk28uwq",
# String is too short.
"lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6na6hlh",
# Invalid multiplier
"lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpujr6jxr9gq9pv6g46y7d20jfkegkg4gljz2ea2a3m9lmvvr95tq2s0kvu70u3axgelz3kyvtp2ywwt0y8hkx2869zq5dll9nelr83zzqqpgl2zg",
# Invalid sub-millisatoshi precision.
"lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu7hqtk93pkf7sw55rdv4k9z2vj050rxdr6za9ekfs3nlt5lr89jqpdmxsmlj9urqumg0h9wzpqecw7th56tdms40p2ny9q4ddvjsedzcplva53s"
]
[
test_pubkey: test_pubkey,
valid_encoded_invoices: valid_encoded_invoices,
invalid_encoded_invoices: invalid_encoded_invoices
]
end
describe "decode/1" do
test "successfully decode with valid segwit addresses in mainnet", %{
valid_encoded_invoices: valid_encoded_invoices
} do
for {valid_encoded_invoice, invoice} <- valid_encoded_invoices do
assert {:ok, decoded_invoice} = Invoice.decode(valid_encoded_invoice)
assert decoded_invoice == invoice
end
end
test "fail to decode with invalid segwit addresses in mainnet", %{
invalid_encoded_invoices: invalid_encoded_invoices
} do
for invalid_encoded_invoice <- invalid_encoded_invoices do
assert {:error, error} = Invoice.decode(invalid_encoded_invoice)
end
end
end
describe "expires_at/1" do
test "calculates expires at time correctly for diff invoice types", %{
valid_encoded_invoices: invoices
} do
for {_valid_encoded_invoice, invoice} <- invoices do
expires_at = Invoice.expires_at(invoice)
assert Timex.to_unix(expires_at) - (invoice.expiry || 3600) == invoice.timestamp
end
end
end
end

View File

@ -0,0 +1,58 @@
defmodule Bitcoinex.Network do
@enforce_keys [
:name,
:hrp_segwit_prefix,
:p2pkh_version_decimal_prefix,
:p2sh_version_decimal_prefix
]
defstruct [
:name,
:hrp_segwit_prefix,
:p2pkh_version_decimal_prefix,
:p2sh_version_decimal_prefix
]
@type t() :: %__MODULE__{
name: atom,
hrp_segwit_prefix: String.t(),
p2pkh_version_decimal_prefix: integer(),
p2sh_version_decimal_prefix: integer()
}
@type network_name :: :mainnet | :testnet | :regtest
def supported_networks() do
[
mainnet(),
testnet(),
regtest()
]
end
def mainnet do
%__MODULE__{
name: :mainnet,
hrp_segwit_prefix: "bc",
p2pkh_version_decimal_prefix: 0,
p2sh_version_decimal_prefix: 5
}
end
def testnet do
%__MODULE__{
name: :testnet,
hrp_segwit_prefix: "tb",
p2pkh_version_decimal_prefix: 111,
p2sh_version_decimal_prefix: 196
}
end
def regtest do
%__MODULE__{
name: :regtest,
hrp_segwit_prefix: "bcrt",
p2pkh_version_decimal_prefix: 111,
p2sh_version_decimal_prefix: 196
}
end
end

View File

@ -0,0 +1,418 @@
defmodule Bitcoinex.PSBTTest do
use ExUnit.Case
doctest Bitcoinex.PSBT
alias Bitcoinex.PSBT
alias Bitcoinex.PSBT.In
alias Bitcoinex.PSBT.Out
alias Bitcoinex.PSBT.Global
@valid_psbts [
%{
psbt:
"cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA",
expected_global: %Global{
unsigned_tx: %Bitcoinex.Transaction{
inputs: [
%Bitcoinex.Transaction.In{
prev_txid: "f61b1742ca13176464adb3cb66050c00787bb3a4eead37e985f2df1e37718126",
prev_vout: 0,
script_sig: "",
sequence_no: 4_294_967_294
}
],
lock_time: 1_257_139,
outputs: [
%Bitcoinex.Transaction.Out{
script_pub_key: "76a914d0c59903c5bac2868760e90fd521a4665aa7652088ac",
value: 99_999_699
},
%Bitcoinex.Transaction.Out{
script_pub_key: "a9143545e6e33b832c47050f24d3eeb93c9c03948bc787",
value: 100_000_000
}
],
version: 2
}
},
expected_in: [
%In{
non_witness_utxo: %Bitcoinex.Transaction{
inputs: [
%Bitcoinex.Transaction.In{
prev_txid: "e567952fb6cc33857f392efa3a46c995a28f69cca4bb1b37e0204dab1ec7a389",
prev_vout: 1,
script_sig: "160014be18d152a9b012039daf3da7de4f53349eecb985",
sequence_no: 4_294_967_295
},
%Bitcoinex.Transaction.In{
prev_txid: "b490486aec3ae671012dddb2bb08466bef37720a533a894814ff1da743aaf886",
prev_vout: 1,
script_sig: "160014fe3e9ef1a745e974d902c4355943abcb34bd5353",
sequence_no: 4_294_967_295
}
],
lock_time: 0,
outputs: [
%Bitcoinex.Transaction.Out{
script_pub_key: "76a91485cff1097fd9e008bb34af709c62197b38978a4888ac",
value: 200_000_000
},
%Bitcoinex.Transaction.Out{
script_pub_key: "a914339725ba21efd62ac753a9bcd067d6c7a6a39d0587",
value: 190_303_501_938
}
],
version: 1,
witnesses: [
%Bitcoinex.Transaction.Witness{
txinwitness: [
"304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c01",
"03d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f2105"
]
},
%Bitcoinex.Transaction.Witness{
txinwitness: [
"3045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01",
"0223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab3"
]
}
]
}
}
],
expected_out: []
},
%{
psbt:
"cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA",
expected_global: %Global{
unsigned_tx: %Bitcoinex.Transaction{
inputs: [
%Bitcoinex.Transaction.In{
prev_txid: "e47b5b7a879f13a8213815cf3dc3f5b35af1e217f412829bc4f75a8ca04909ab",
prev_vout: 0,
script_sig: "",
sequence_no: 4_294_967_294
},
%Bitcoinex.Transaction.In{
prev_txid: "e47b5b7a879f13a8213815cf3dc3f5b35af1e217f412829bc4f75a8ca04909ab",
prev_vout: 1,
script_sig: "",
sequence_no: 4_294_967_294
}
],
lock_time: 0,
outputs: [
%Bitcoinex.Transaction.Out{
script_pub_key: "76a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac",
value: 199_900_000
},
%Bitcoinex.Transaction.Out{
script_pub_key: "76a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac",
value: 9358
}
],
version: 2,
witnesses: nil
}
},
expected_in: [
%In{
bip32_derivation: nil,
final_scriptsig:
"47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa88292",
final_scriptwitness: nil,
non_witness_utxo: nil,
partial_sig: nil,
por_commitment: nil,
proprietary: nil,
redeem_script: nil,
sighash_type: nil,
witness_script: nil,
witness_utxo: nil
},
%In{
bip32_derivation: nil,
final_scriptsig: nil,
final_scriptwitness: nil,
non_witness_utxo: nil,
partial_sig: nil,
por_commitment: nil,
proprietary: nil,
redeem_script: "001485d13537f2e265405a34dbafa9e3dda01fb82308",
sighash_type: nil,
witness_script: nil,
witness_utxo: %Bitcoinex.Transaction.Out{
script_pub_key: "a9143545e6e33b832c47050f24d3eeb93c9c03948bc787",
value: 100_000_000
}
}
],
expected_out: []
},
%{
psbt:
"cHNidP8BAFICAAAAAZ38ZijCbFiZ/hvT3DOGZb/VXXraEPYiCXPfLTht7BJ2AQAAAAD/////AfA9zR0AAAAAFgAUezoAv9wU0neVwrdJAdCdpu8TNXkAAAAATwEENYfPAto/0AiAAAAAlwSLGtBEWx7IJ1UXcnyHtOTrwYogP/oPlMAVZr046QADUbdDiH7h1A3DKmBDck8tZFmztaTXPa7I+64EcvO8Q+IM2QxqT64AAIAAAACATwEENYfPAto/0AiAAAABuQRSQnE5zXjCz/JES+NTzVhgXj5RMoXlKLQH+uP2FzUD0wpel8itvFV9rCrZp+OcFyLrrGnmaLbyZnzB1nHIPKsM2QxqT64AAIABAACAAAEBKwBlzR0AAAAAIgAgLFSGEmxJeAeagU4TcV1l82RZ5NbMre0mbQUIZFuvpjIBBUdSIQKdoSzbWyNWkrkVNq/v5ckcOrlHPY5DtTODarRWKZyIcSEDNys0I07Xz5wf6l0F1EFVeSe+lUKxYusC4ass6AIkwAtSriIGAp2hLNtbI1aSuRU2r+/lyRw6uUc9jkO1M4NqtFYpnIhxENkMak+uAACAAAAAgAAAAAAiBgM3KzQjTtfPnB/qXQXUQVV5J76VQrFi6wLhqyzoAiTACxDZDGpPrgAAgAEAAIAAAAAAACICA57/H1R6HV+S36K6evaslxpL0DukpzSwMVaiVritOh75EO3kXMUAAACAAAAAgAEAAIAA",
expected_global: %Global{
proprietary: nil,
unsigned_tx: %Bitcoinex.Transaction{
inputs: [
%Bitcoinex.Transaction.In{
prev_txid: "7612ec6d382ddf730922f610da7a5dd5bf658633dcd31bfe99586cc22866fc9d",
prev_vout: 1,
script_sig: "",
sequence_no: 4_294_967_295
}
],
lock_time: 0,
outputs: [
%Bitcoinex.Transaction.Out{
script_pub_key: "00147b3a00bfdc14d27795c2b74901d09da6ef133579",
value: 499_990_000
}
],
version: 2,
witnesses: nil
},
version: nil,
xpub: %{
derivation: "d90c6a4fae00008001000080",
xpub:
"tpubDBkJeJo2X94YsvtBEU1eKoibEWiNv51nW5iHhs6VZp59jsE6nen8KZMFyGHuGbCvqjRqirgeMcfpVBkttpUUT6brm4duzSGoZeTbhqCNUu6"
}
},
expected_in: [
%Bitcoinex.PSBT.In{
bip32_derivation: [
%{
derivation: "d90c6a4fae0000800000008000000000",
public_key: "029da12cdb5b235692b91536afefe5c91c3ab9473d8e43b533836ab456299c8871"
},
%{
derivation: "d90c6a4fae0000800100008000000000",
public_key: "03372b34234ed7cf9c1fea5d05d441557927be9542b162eb02e1ab2ce80224c00b"
}
],
final_scriptsig: nil,
final_scriptwitness: nil,
non_witness_utxo: nil,
partial_sig: nil,
por_commitment: nil,
proprietary: nil,
redeem_script: nil,
sighash_type: nil,
witness_script:
"5221029da12cdb5b235692b91536afefe5c91c3ab9473d8e43b533836ab456299c88712103372b34234ed7cf9c1fea5d05d441557927be9542b162eb02e1ab2ce80224c00b52ae",
witness_utxo: %Bitcoinex.Transaction.Out{
script_pub_key:
"00202c5486126c4978079a814e13715d65f36459e4d6ccaded266d0508645bafa632",
value: 500_000_000
}
}
],
expected_out: [
%Out{
bip32_derivation: [
%{
derivation: "ede45cc5000000800000008001000080",
public_key: "039eff1f547a1d5f92dfa2ba7af6ac971a4bd03ba4a734b03156a256b8ad3a1ef9"
}
],
proprietary: nil,
redeem_script: nil,
witness_script: nil
}
]
},
%{
psbt:
"cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=",
expected_global: %Global{
proprietary: nil,
unsigned_tx: %Bitcoinex.Transaction{
inputs: [
%Bitcoinex.Transaction.In{
prev_txid: "39bc5c3b33d66ce3d7852a7942331e3ec10f8ba50f225fc41fb5dfa523239a27",
prev_vout: 0,
script_sig: "",
sequence_no: 4_294_967_295
}
],
lock_time: 0,
outputs: [
%Bitcoinex.Transaction.Out{
script_pub_key: "76a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac",
value: 199_908_000
}
],
version: 2,
witnesses: nil
},
version: nil,
xpub: nil
},
expected_in: [
%Bitcoinex.PSBT.In{
bip32_derivation: [
%{
derivation: "b4a6ba67000000800000008004000080",
public_key: "03b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46"
},
%{
derivation: "b4a6ba67000000800000008005000080",
public_key: "03de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd"
}
],
final_scriptsig: nil,
final_scriptwitness: nil,
non_witness_utxo: nil,
partial_sig: %{
public_key: "03b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46",
signature:
"304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a01"
},
por_commitment: nil,
proprietary: nil,
redeem_script: "0020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681",
sighash_type: nil,
witness_script:
"522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae",
witness_utxo: %Bitcoinex.Transaction.Out{
script_pub_key: "a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87",
value: 199_909_013
}
}
],
expected_out: []
},
%{
# finalized psbt
psbt:
"cHNidP8BAKcBAAAAAjHC7gs4NF4rUOrlta+j+wB8UHTEuLn0XY6FDUcGybQMAAAAAAD+////NUUKTkDqBbL9oqrAIk9199/ZANXi/8XEgguqQY8iiewAAAAAAP7///8CdImYAAAAAAAiACCs+u6eefBEoqCFYVWhxscCwh/WJZ+286/E8zNH9gRhd4CWmAAAAAAAF6kUV3ZMSDpAgQZkllBPVNL5uRPlwOOHAAAAAAABASuAlpgAAAAAACIAIDH8Jza8S0T6nWkCcU5GqgwxJ2rGEgWFgDSGiJVFJ5W0AQj9/QAEAEgwRQIhALL4SZucnmwtsJ2BguTQkajOkbvRTRcIMF2B/c26pnZDAiAwNPAWsW3b3PxNXZouG43Z2HJ4WufvpjM0x+VlprgFUAFHMEQCIGV66oyrbw0b9HXA8EeGKrIi88YhTGuhpQKdDxX1VivPAiAcxSrameybDohX8yINx2t452PyyqP6qUiTUMNnoAv+twFpUiECZ3pcsDl1tPNTASW/gFEm/PlWLEnQJN5h32F5qmC2U6AhA1fyyfYB3ma7Vg6JKICdCsQFD7/IchNleJnjTaTGbCFgIQP8V/0ULlUTx5q8mJ6eJh6GaCHkHXDkTnmFbpZRGDsQVVOuAAEBK4CWmAAAAAAAIgAgi3WHXCAbeRTULI6EPlb3Z3+J153IX4zK5bHRsqnrSO4BCPwEAEcwRAIgelTwDK+TOYwP6luGb5htloRgijKLoLmNrjk9imXolaICIFQ9Rq0MrOGcrYHC6BZIyyz+tB0Lm8FhqnARl7R+TpyaAUcwRAIgfHNbxYLcTt1yWeADHyo5ye4jtApn+YTgFzK16IsOW0QCIDcOnv2QYaZlc0etz9kfIrkpoepeTndtvEREKROzqqlCAWlSIQIIPVGeoWYEHRGxyDhpzTqE0uBZIjBj5DDXgBX5QWwecCECL5C1pXxiQ5uiuhZASuHYEUq+gXmXqE+wxPnV590o+HAhA0odK6A98KAdcHcI5pcbNfwR1oq0PsofJzNfvSKkdqCMU64AAQFpUiECPhqS90SDpMEqGW1sAlOsWJz63Vlk/z5sY6711XcFHtQhAk0OObM6tXeCqY/Qan0GUzheUJ7jt03EVVnm22OR0xN4IQNsC65rywLkfIV8SA7R0jiIyK1qZrg6sRHLa5JCr7HHJVOuIgICPhqS90SDpMEqGW1sAlOsWJz63Vlk/z5sY6711XcFHtQgAAAAAAAAAIACAACAAgAAAAAAAAAAAAAAAQAAAA0AAAAiAgJNDjmzOrV3gqmP0Gp9BlM4XlCe47dNxFVZ5ttjkdMTeCAAAAAAAAAAgAIAAIACAAAAAAAAAAAAAAABAAAADQAAACICA2wLrmvLAuR8hXxIDtHSOIjIrWpmuDqxEctrkkKvscclIAAAAAAAAACAAgAAgAIAAAAAAAAAAAAAAAEAAAANAAAAAAA=",
expected_global: %Global{
proprietary: nil,
unsigned_tx: %Bitcoinex.Transaction{
inputs: [
%Bitcoinex.Transaction.In{
prev_txid: "0cb4c906470d858e5df4b9b8c474507c00fba3afb5e5ea502b5e34380beec231",
prev_vout: 0,
script_sig: "",
sequence_no: 4_294_967_294
},
%Bitcoinex.Transaction.In{
prev_txid: "ec89228f41aa0b82c4c5ffe2d500d9dff7754f22c0aaa2fdb205ea404e0a4535",
prev_vout: 0,
script_sig: "",
sequence_no: 4_294_967_294
}
],
lock_time: 0,
outputs: [
%Bitcoinex.Transaction.Out{
script_pub_key:
"0020acfaee9e79f044a2a0856155a1c6c702c21fd6259fb6f3afc4f33347f6046177",
value: 9_996_660
},
%Bitcoinex.Transaction.Out{
script_pub_key: "a91457764c483a4081066496504f54d2f9b913e5c0e387",
value: 10_000_000
}
],
version: 1,
witnesses: nil
},
version: nil,
xpub: nil
},
expected_in: [
%Bitcoinex.PSBT.In{
bip32_derivation: nil,
final_scriptsig: nil,
final_scriptwitness: %Bitcoinex.Transaction.Witness{
txinwitness: [
"",
"3045022100b2f8499b9c9e6c2db09d8182e4d091a8ce91bbd14d1708305d81fdcdbaa6764302203034f016b16ddbdcfc4d5d9a2e1b8dd9d872785ae7efa63334c7e565a6b8055001",
"30440220657aea8cab6f0d1bf475c0f047862ab222f3c6214c6ba1a5029d0f15f5562bcf02201cc52ada99ec9b0e8857f3220dc76b78e763f2caa3faa9489350c367a00bfeb701",
"522102677a5cb03975b4f3530125bf805126fcf9562c49d024de61df6179aa60b653a0210357f2c9f601de66bb560e8928809d0ac4050fbfc87213657899e34da4c66c21602103fc57fd142e5513c79abc989e9e261e866821e41d70e44e79856e9651183b105553ae"
]
},
non_witness_utxo: nil,
partial_sig: nil,
por_commitment: nil,
proprietary: nil,
redeem_script: nil,
sighash_type: nil,
witness_script: nil,
witness_utxo: %Bitcoinex.Transaction.Out{
script_pub_key:
"002031fc2736bc4b44fa9d6902714e46aa0c31276ac61205858034868895452795b4",
value: 10_000_000
}
},
%Bitcoinex.PSBT.In{
bip32_derivation: nil,
final_scriptsig: nil,
final_scriptwitness: %Bitcoinex.Transaction.Witness{
txinwitness: [
"",
"304402207a54f00caf93398c0fea5b866f986d9684608a328ba0b98dae393d8a65e895a20220543d46ad0cace19cad81c2e81648cb2cfeb41d0b9bc161aa701197b47e4e9c9a01",
"304402207c735bc582dc4edd7259e0031f2a39c9ee23b40a67f984e01732b5e88b0e5b440220370e9efd9061a6657347adcfd91f22b929a1ea5e4e776dbc44442913b3aaa94201",
"522102083d519ea166041d11b1c83869cd3a84d2e059223063e430d78015f9416c1e7021022f90b5a57c62439ba2ba16404ae1d8114abe817997a84fb0c4f9d5e7dd28f87021034a1d2ba03df0a01d707708e6971b35fc11d68ab43eca1f27335fbd22a476a08c53ae"
]
},
non_witness_utxo: nil,
partial_sig: nil,
por_commitment: nil,
proprietary: nil,
redeem_script: nil,
sighash_type: nil,
witness_script: nil,
witness_utxo: %Bitcoinex.Transaction.Out{
script_pub_key:
"00208b75875c201b7914d42c8e843e56f7677f89d79dc85f8ccae5b1d1b2a9eb48ee",
value: 10_000_000
}
}
],
expected_out: [
%Bitcoinex.PSBT.Out{
bip32_derivation: [
%{
derivation: "000000000000008002000080020000000000000000000000010000000d000000",
public_key: "023e1a92f74483a4c12a196d6c0253ac589cfadd5964ff3e6c63aef5d577051ed4"
},
%{
derivation: "000000000000008002000080020000000000000000000000010000000d000000",
public_key: "024d0e39b33ab57782a98fd06a7d0653385e509ee3b74dc45559e6db6391d31378"
},
%{
derivation: "000000000000008002000080020000000000000000000000010000000d000000",
public_key: "036c0bae6bcb02e47c857c480ed1d23888c8ad6a66b83ab111cb6b9242afb1c725"
}
],
proprietary: nil,
redeem_script: nil,
witness_script:
"5221023e1a92f74483a4c12a196d6c0253ac589cfadd5964ff3e6c63aef5d577051ed421024d0e39b33ab57782a98fd06a7d0653385e509ee3b74dc45559e6db6391d3137821036c0bae6bcb02e47c857c480ed1d23888c8ad6a66b83ab111cb6b9242afb1c72553ae"
}
]
}
]
describe "decode/1" do
test "valid psbts" do
for valid_psbt <- @valid_psbts do
case PSBT.decode(valid_psbt.psbt) do
{:ok, psbt} ->
assert valid_psbt.expected_global == psbt.global
assert valid_psbt.expected_in == psbt.inputs
assert valid_psbt.expected_out == psbt.outputs
{:error, _} ->
assert :error != :error
end
end
end
end
end

View File

@ -0,0 +1,18 @@
defmodule Bitcoinex.Secp256k1.PointTest do
use ExUnit.Case
doctest Bitcoinex.Secp256k1.Point
alias Bitcoinex.Secp256k1.Point
describe "serialize_public_key/1" do
test "successfully pad public key" do
assert "020003b94aecea4d0a57a6c87cf43c50c8b3736f33ab7fd34f02441b6e94477689" ==
Point.serialize_public_key(%Point{
x:
6_579_384_254_631_425_969_190_483_614_785_133_746_155_874_651_439_631_590_927_590_192_220_436_105,
y:
71_870_263_570_581_286_056_939_190_487_148_011_225_641_308_782_404_760_504_903_461_107_415_970_265_024
})
end
end
end

View File

@ -0,0 +1,100 @@
defmodule Bitcoinex.Secp256k1.Secp256k1Test do
use ExUnit.Case
doctest Bitcoinex.Secp256k1
alias Bitcoinex.Secp256k1
@valid_signatures_for_public_key_recovery [
%{
message_hash:
Base.decode16!(
"CE0677BB30BAA8CF067C88DB9811F4333D131BF8BCF12FE7065D211DCE971008",
case: :upper
),
signature:
Base.decode16!(
"90F27B8B488DB00B00606796D2987F6A5F59AE62EA05EFFE84FEF5B8B0E549984A691139AD57A3F0B906637673AA2F63D1F55CB1A69199D4009EEA23CEADDC93",
case: :upper
),
recovery_id: 1,
pubkey: "02e32df42865e97135acfb65f3bae71bdc86f4d49150ad6a440b6f15878109880a"
},
%{
message_hash:
Base.decode16!(
"5555555555555555555555555555555555555555555555555555555555555555",
case: :upper
),
signature:
Base.decode16!(
"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101",
case: :upper
),
recovery_id: 0,
pubkey: "02c1ab1d7b32c1adcdab9d378c2ae75ee27822541c6875beed3255f981f0dea378"
}
]
@invalid_signatures_for_public_key_recovery [
%{
# invalid curve point
message_hash:
Base.decode16!(
"00C547E4F7B0F325AD1E56F57E26C745B09A3E503D86E00E5255FF7F715D3D1C",
case: :upper
),
signature:
Base.decode16!(
"00B1693892219D736CABA55BDB67216E485557EA6B6AF75F37096C9AA6A5A75F00B940B1D03B21E36B0E47E79769F095FE2AB855BD91E3A38756B7D75A9C4549",
case: :upper
),
recovery_id: 0
},
%{
# Low r and s.
message_hash:
Base.decode16!(
"BA09EDC1275A285FB27BFE82C4EEA240A907A0DBAF9E55764B8F318C37D5974F",
case: :upper
),
signature:
Base.decode16!(
"00000000000000000000000000000000000000000000000000000000000000002C0000000000000000000000000000000000000000000000000000000000000004",
case: :upper
),
recovery_id: 1
},
%{
# invalid signature
message_hash:
Base.decode16!(
"5555555555555555555555555555555555555555555555555555555555555555",
case: :upper
),
signature:
Base.decode16!(
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
case: :upper
),
recovery_id: 0
}
]
describe "ecdsa_recover_compact/3" do
test "successfully recover a public key from a signature" do
for t <- @valid_signatures_for_public_key_recovery do
assert {:ok, recovered_pubkey} =
Secp256k1.ecdsa_recover_compact(t.message_hash, t.signature, t.recovery_id)
assert recovered_pubkey == t.pubkey
end
end
test "unsuccessfully recover a public key from a signature" do
for t <- @invalid_signatures_for_public_key_recovery do
assert {:error, _error} =
Secp256k1.ecdsa_recover_compact(t.message_hash, t.signature, t.recovery_id)
end
end
end
end

View File

@ -0,0 +1,139 @@
defmodule Bitcoinex.SegwitTest do
use ExUnit.Case
doctest Bitcoinex.Segwit
alias Bitcoinex.Segwit
import Bitcoinex.Utils, only: [replicate: 2]
@valid_segwit_address_hexscript_pairs_mainnet [
{"BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4",
"0014751e76e8199196d454941c45d1b3a323f1433bd6"},
{"bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y",
"5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"},
{"BC1SW50QGDZ25J", "6002751e"},
{"bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs", "5210751e76e8199196d454941c45d1b3a323"},
{"bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0",
"512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"}
]
@valid_segwit_address_hexscript_pairs_testnet [
{"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
"00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"},
{"tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c",
"5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"},
{"tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy",
"0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"}
]
@valid_segwit_address_hexscript_pairs_regtest [
{"bcrt1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qzf4jry",
"00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"},
{"bcrt1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvseswlauz7",
"0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"}
]
@invalid_segwit_addresses [
"tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut",
"bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd",
"tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf",
"BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL",
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh",
"tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47",
"bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4",
"BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R",
"bc1pw5dgrnzv",
"bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav",
"tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty",
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5",
"BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2",
"tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq",
"bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf",
"tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j",
"bc1rw5uspcuh",
"bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90",
"BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P",
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7",
"bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du",
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv",
"bc1gmk9yu"
]
describe "decode/1" do
test "successfully decode with valid segwit addresses in mainnet" do
for {address, hexscript} <- @valid_segwit_address_hexscript_pairs_mainnet do
assert_valid_segwit_address(address, hexscript, :mainnet)
end
end
test "successfully decode with valid segwit addresses in testnet" do
for {address, hexscript} <- @valid_segwit_address_hexscript_pairs_testnet do
assert_valid_segwit_address(address, hexscript, :testnet)
end
end
test "successfully decode with valid segwit addresses in regtest" do
for {address, hexscript} <- @valid_segwit_address_hexscript_pairs_regtest do
assert_valid_segwit_address(address, hexscript, :regtest)
end
end
test "fail to decode with invalid address" do
for address <- @invalid_segwit_addresses do
assert {:error, _error} = Segwit.decode_address(address)
end
end
end
describe "encode_address/1" do
test "successfully encode with valid netwrok, version and program " do
version = 1
program = replicate(1, 10)
assert {:ok, mainnet_address} = Segwit.encode_address(:mainnet, version, program)
assert {:ok, testnet_address} = Segwit.encode_address(:testnet, version, program)
assert {:ok, regtest_address} = Segwit.encode_address(:regtest, version, program)
all_addresses = [mainnet_address, testnet_address, regtest_address]
# make sure they are different
assert Enum.uniq(all_addresses) == all_addresses
end
test "fail to encode with program length > 40 " do
assert {:error, _error} = Segwit.encode_address(:mainnet, 1, replicate(1, 41))
end
test "fail to encode with version 0 but program length not equalt to 20 or 32 " do
assert {:ok, _address} = Segwit.encode_address(:mainnet, 0, replicate(1, 20))
assert {:ok, _address} = Segwit.encode_address(:mainnet, 0, replicate(1, 32))
assert {:error, _error} = Segwit.encode_address(:mainnet, 0, replicate(1, 21))
assert {:error, _error} = Segwit.encode_address(:mainnet, 0, replicate(1, 33))
end
end
describe "is_valid_segswit_address?/1" do
test "return true given valid address" do
for {address, _hexscript} <-
@valid_segwit_address_hexscript_pairs_mainnet ++
@valid_segwit_address_hexscript_pairs_testnet ++
@valid_segwit_address_hexscript_pairs_regtest do
assert Segwit.is_valid_segswit_address?(address)
end
end
test "return false given invalid address" do
for address <- @invalid_segwit_addresses do
refute Segwit.is_valid_segswit_address?(address)
end
end
end
# local private test helper
defp assert_valid_segwit_address(address, hexscript, network) do
assert {:ok, {hrp, version, program}} = Segwit.decode_address(address)
assert hrp == network
assert version in 0..16
assert Segwit.get_segwit_script_pubkey(version, program) == hexscript
# encode after decode should be the same(after downcase) as before
{:ok, new_address} = Segwit.encode_address(hrp, version, program)
assert new_address == String.downcase(address)
end
end

View File

@ -0,0 +1 @@
ExUnit.start()

View File

@ -0,0 +1,236 @@
defmodule Bitcoinex.TransactionTest do
use ExUnit.Case
doctest Bitcoinex.Transaction
alias Bitcoinex.Transaction
@txn_serialization_1 %{
tx_hex:
"01000000010470c3139dc0f0882f98d75ae5bf957e68dadd32c5f81261c0b13e85f592ff7b0000000000ffffffff02b286a61e000000001976a9140f39a0043cf7bdbe429c17e8b514599e9ec53dea88ac01000000000000001976a9148a8c9fd79173f90cf76410615d2a52d12d27d21288ac00000000"
}
@txn_segwit_serialization_1 %{
tx_hex:
"01000000000102fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f00000000494830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01eeffffffef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff02202cb206000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac9093510d000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac000247304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee0121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635711000000"
}
@txn_segwit_serialization_2 %{
tx_hex:
"01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000"
}
@txn_segwit_serialization_3 %{
tx_hex:
"01000000000101db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477010000001716001479091972186c449eb1ded22b78e40d009bdf0089feffffff02b8b4eb0b000000001976a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac0008af2f000000001976a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac02473044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb012103ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a2687392040000"
}
@txn_segwit_serialization_4 %{
tx_hex:
"0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000"
}
describe "decode/1" do
test "decodes legacy bitcoin transaction" do
txn_test = @txn_serialization_1
{:ok, txn} = Transaction.decode(txn_test.tx_hex)
assert 1 == length(txn.inputs)
assert 2 == length(txn.outputs)
assert 1 == txn.version
assert nil == txn.witnesses
assert 0 == txn.lock_time
assert "b020bdec4e92cb69db93557dcbbfcc73076fc01f6828e41eb3ef5f628414ee62" ==
Transaction.transaction_id(txn)
in_1 = Enum.at(txn.inputs, 0)
assert "7bff92f5853eb1c06112f8c532ddda687e95bfe55ad7982f88f0c09d13c37004" == in_1.prev_txid
assert 0 == in_1.prev_vout
assert "" == in_1.script_sig
assert 4_294_967_295 == in_1.sequence_no
out_0 = Enum.at(txn.outputs, 0)
assert 514_229_938 == out_0.value
assert "76a9140f39a0043cf7bdbe429c17e8b514599e9ec53dea88ac" == out_0.script_pub_key
out_1 = Enum.at(txn.outputs, 1)
assert 1 == out_1.value
assert "76a9148a8c9fd79173f90cf76410615d2a52d12d27d21288ac" == out_1.script_pub_key
end
test "decodes native segwit p2wpkh bitcoin transaction" do
txn_test = @txn_segwit_serialization_1
{:ok, txn} = Transaction.decode(txn_test.tx_hex)
assert 2 == length(txn.inputs)
assert 2 == length(txn.outputs)
assert 1 == txn.version
assert 2 == length(txn.witnesses)
assert 17 == txn.lock_time
assert "e8151a2af31c368a35053ddd4bdb285a8595c769a3ad83e0fa02314a602d4609" ==
Transaction.transaction_id(txn)
in_1 = Enum.at(txn.inputs, 0)
assert "9f96ade4b41d5433f4eda31e1738ec2b36f6e7d1420d94a6af99801a88f7f7ff" == in_1.prev_txid
assert 0 == in_1.prev_vout
assert "4830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01" ==
in_1.script_sig
assert 4_294_967_278 == in_1.sequence_no
in_2 = Enum.at(txn.inputs, 1)
assert "8ac60eb9575db5b2d987e29f301b5b819ea83a5c6579d282d189cc04b8e151ef" == in_2.prev_txid
assert 1 == in_2.prev_vout
assert "" == in_2.script_sig
assert 4_294_967_295 == in_2.sequence_no
witness_in_0 = Enum.at(txn.witnesses, 0)
assert 0 == witness_in_0.txinwitness
witness_in_1 = Enum.at(txn.witnesses, 1)
assert [
"304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee01",
"025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357"
] == witness_in_1.txinwitness
out_0 = Enum.at(txn.outputs, 0)
assert 112_340_000 == out_0.value
assert "76a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac" == out_0.script_pub_key
out_1 = Enum.at(txn.outputs, 1)
assert 223_450_000 == out_1.value
assert "76a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac" == out_1.script_pub_key
end
test "decodes native segwit p2wsh bitcoin transaction" do
txn_test = @txn_segwit_serialization_2
{:ok, txn} = Transaction.decode(txn_test.tx_hex)
assert 2 == length(txn.inputs)
assert 1 == length(txn.outputs)
assert 1 == txn.version
assert 2 == length(txn.witnesses)
assert 0 == txn.lock_time
assert "570e3730deeea7bd8bc92c836ccdeb4dd4556f2c33f2a1f7b889a4cb4e48d3ab" ==
Transaction.transaction_id(txn)
in_0 = Enum.at(txn.inputs, 0)
assert "6eb316926b1c5d567cd6f5e6a84fec606fc53d7b474526d1fff3948020c93dfe" == in_0.prev_txid
assert 0 == in_0.prev_vout
assert "47304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201" ==
in_0.script_sig
assert 4_294_967_295 == in_0.sequence_no
in_1 = Enum.at(txn.inputs, 1)
assert "f825690aee1b3dc247da796cacb12687a5e802429fd291cfd63e010f02cf1508" == in_1.prev_txid
assert 0 == in_1.prev_vout
assert "" == in_1.script_sig
assert 4_294_967_295 == in_1.sequence_no
witness_in_0 = Enum.at(txn.witnesses, 0)
assert 0 == witness_in_0.txinwitness
witness_in_1 = Enum.at(txn.witnesses, 1)
assert [
"304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503",
"3044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e2703",
"21026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac"
] == witness_in_1.txinwitness
out_1 = Enum.at(txn.outputs, 0)
assert 5_000_000_000 == out_1.value
assert "76a914a30741f8145e5acadf23f751864167f32e0963f788ac" == out_1.script_pub_key
end
test "decodes segwit p2sh-pw2pkh bitcoin transaction" do
txn_test = @txn_segwit_serialization_3
{:ok, txn} = Transaction.decode(txn_test.tx_hex)
assert 1 == length(txn.inputs)
assert 2 == length(txn.outputs)
assert 1 == txn.version
assert 1 == length(txn.witnesses)
assert 1170 == txn.lock_time
assert "ef48d9d0f595052e0f8cdcf825f7a5e50b6a388a81f206f3f4846e5ecd7a0c23" ==
Transaction.transaction_id(txn)
in_0 = Enum.at(txn.inputs, 0)
assert "77541aeb3c4dac9260b68f74f44c973081a9d4cb2ebe8038b2d70faa201b6bdb" == in_0.prev_txid
assert 1 == in_0.prev_vout
assert "16001479091972186c449eb1ded22b78e40d009bdf0089" ==
in_0.script_sig
assert 4_294_967_294 == in_0.sequence_no
witness_in_0 = Enum.at(txn.witnesses, 0)
assert [
"3044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb01",
"03ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a26873"
] == witness_in_0.txinwitness
out_0 = Enum.at(txn.outputs, 0)
assert 199_996_600 == out_0.value
assert "76a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac" == out_0.script_pub_key
out_1 = Enum.at(txn.outputs, 1)
assert 800_000_000 == out_1.value
assert "76a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac" == out_1.script_pub_key
end
test "decodes segwit p2sh-p2wsh bitcoin transaction" do
txn_test = @txn_segwit_serialization_4
{:ok, txn} = Transaction.decode(txn_test.tx_hex)
assert 1 == length(txn.inputs)
assert 2 == length(txn.outputs)
assert 1 == txn.version
assert 1 == length(txn.witnesses)
assert 0 == txn.lock_time
assert "27eae69aff1dd4388c0fa05cbbfe9a3983d1b0b5811ebcd4199b86f299370aac" ==
Transaction.transaction_id(txn)
in_0 = Enum.at(txn.inputs, 0)
assert "6eb98797a21c6c10aa74edf29d618be109f48a8e94c694f3701e08ca69186436" == in_0.prev_txid
assert 1 == in_0.prev_vout
assert "220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54" ==
in_0.script_sig
assert 4_294_967_295 == in_0.sequence_no
witness_in_0 = Enum.at(txn.witnesses, 0)
assert [
"",
"304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01",
"3044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502",
"3044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403",
"3045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381",
"3045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a0882",
"30440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783",
"56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae"
] == witness_in_0.txinwitness
out_0 = Enum.at(txn.outputs, 0)
assert 900_000_000 == out_0.value
assert "76a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688ac" == out_0.script_pub_key
out_1 = Enum.at(txn.outputs, 1)
assert 87_000_000 == out_1.value
assert "76a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac" == out_1.script_pub_key
end
end
end

View File

@ -35,7 +35,7 @@ defmodule BitcoinStream.MixProject do
{:chumak, github: "zeromq/chumak"},
# {:bitcoinex, "~> 0.1.0"},
# {:bitcoinex, git: "git@github.com:mononaut/bitcoinex.git", tag: "master"},
{:bitcoinex, path: "../bitcoinex", override: true},
{:bitcoinex, path: "./bitcoinex", override: true},
{:hackney, "~> 1.15"},
{:cowboy, "~> 2.4"},
{:plug, "~> 1.7"},