mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-05-12 19:20:46 +02:00
Move bitcoinex server dependency into repo
This commit is contained in:
parent
3a733e26b9
commit
f5e9d46910
2
server/bitcoinex/.dialyzer_ignore.exs
Normal file
2
server/bitcoinex/.dialyzer_ignore.exs
Normal file
@ -0,0 +1,2 @@
|
||||
[
|
||||
]
|
4
server/bitcoinex/.formatter.exs
Normal file
4
server/bitcoinex/.formatter.exs
Normal 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
30
server/bitcoinex/.gitignore
vendored
Normal 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
|
2
server/bitcoinex/.tool-versions
Normal file
2
server/bitcoinex/.tool-versions
Normal file
@ -0,0 +1,2 @@
|
||||
elixir 1.10.4-otp-22
|
||||
erlang 22.3.4.1
|
115
server/bitcoinex/README.md
Normal file
115
server/bitcoinex/README.md
Normal file
@ -0,0 +1,115 @@
|
||||
Forked from RiverFinancial/bitcoinex
|
||||
original README as follows:
|
||||
|
||||

|
||||
# 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.
|
24
server/bitcoinex/UNLICENSE
Normal file
24
server/bitcoinex/UNLICENSE
Normal 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/>
|
30
server/bitcoinex/config/config.exs
Normal file
30
server/bitcoinex/config/config.exs
Normal 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"
|
119
server/bitcoinex/lib/address.ex
Normal file
119
server/bitcoinex/lib/address.ex
Normal 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
|
132
server/bitcoinex/lib/base58.ex
Normal file
132
server/bitcoinex/lib/base58.ex
Normal 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
|
342
server/bitcoinex/lib/bech32.ex
Normal file
342
server/bitcoinex/lib/bech32.ex
Normal 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
|
7
server/bitcoinex/lib/bitcoinex.ex
Normal file
7
server/bitcoinex/lib/bitcoinex.ex
Normal file
@ -0,0 +1,7 @@
|
||||
defmodule Bitcoinex do
|
||||
@moduledoc """
|
||||
Documentation for Bitcoinex.
|
||||
|
||||
Bitcoinex is an Elixir library supporting basic Bitcoin functionality.
|
||||
"""
|
||||
end
|
92
server/bitcoinex/lib/block.ex
Normal file
92
server/bitcoinex/lib/block.ex
Normal 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
|
30
server/bitcoinex/lib/lightning_network/hop_hint.ex
Normal file
30
server/bitcoinex/lib/lightning_network/hop_hint.ex
Normal 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
|
646
server/bitcoinex/lib/lightning_network/invoice.ex
Normal file
646
server/bitcoinex/lib/lightning_network/invoice.ex
Normal 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
|
10
server/bitcoinex/lib/lightning_network/lightning_network.ex
Normal file
10
server/bitcoinex/lib/lightning_network/lightning_network.ex
Normal 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
|
80
server/bitcoinex/lib/network.ex
Normal file
80
server/bitcoinex/lib/network.ex
Normal 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
|
412
server/bitcoinex/lib/psbt.ex
Normal file
412
server/bitcoinex/lib/psbt.ex
Normal 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
|
285
server/bitcoinex/lib/secp256k1/math.ex
Normal file
285
server/bitcoinex/lib/secp256k1/math.ex
Normal 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
|
18
server/bitcoinex/lib/secp256k1/params.ex
Normal file
18
server/bitcoinex/lib/secp256k1/params.ex
Normal 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
|
40
server/bitcoinex/lib/secp256k1/point.ex
Normal file
40
server/bitcoinex/lib/secp256k1/point.ex
Normal 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
|
159
server/bitcoinex/lib/secp256k1/secp256k1.ex
Normal file
159
server/bitcoinex/lib/secp256k1/secp256k1.ex
Normal 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
|
159
server/bitcoinex/lib/segwit.ex
Normal file
159
server/bitcoinex/lib/segwit.ex
Normal 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
|
429
server/bitcoinex/lib/transaction.ex
Normal file
429
server/bitcoinex/lib/transaction.ex
Normal 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
|
50
server/bitcoinex/lib/utils.ex
Normal file
50
server/bitcoinex/lib/utils.ex
Normal 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
79
server/bitcoinex/mix.exs
Normal 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
31
server/bitcoinex/mix.lock
Normal 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"},
|
||||
}
|
216
server/bitcoinex/test/address_test.exs
Normal file
216
server/bitcoinex/test/address_test.exs
Normal 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
|
216
server/bitcoinex/test/base58_test.exs
Normal file
216
server/bitcoinex/test/base58_test.exs
Normal 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
|
298
server/bitcoinex/test/bech32_test.exs
Normal file
298
server/bitcoinex/test/bech32_test.exs
Normal 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
|
503
server/bitcoinex/test/lightning_network/invoice_test.exs
Normal file
503
server/bitcoinex/test/lightning_network/invoice_test.exs
Normal 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
|
58
server/bitcoinex/test/network.ex
Normal file
58
server/bitcoinex/test/network.ex
Normal 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
|
418
server/bitcoinex/test/psbt_test.exs
Normal file
418
server/bitcoinex/test/psbt_test.exs
Normal 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
|
18
server/bitcoinex/test/secp256k1/point_test.exs
Normal file
18
server/bitcoinex/test/secp256k1/point_test.exs
Normal 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
|
100
server/bitcoinex/test/secp256k1/secp256k1_test.exs
Normal file
100
server/bitcoinex/test/secp256k1/secp256k1_test.exs
Normal 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
|
139
server/bitcoinex/test/segwit_test.exs
Normal file
139
server/bitcoinex/test/segwit_test.exs
Normal 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
|
1
server/bitcoinex/test/test_helper.exs
Normal file
1
server/bitcoinex/test/test_helper.exs
Normal file
@ -0,0 +1 @@
|
||||
ExUnit.start()
|
236
server/bitcoinex/test/transaction_test.exs
Normal file
236
server/bitcoinex/test/transaction_test.exs
Normal 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
|
@ -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"},
|
||||
|
Loading…
Reference in New Issue
Block a user