diff --git a/ref/elixir/.gitignore b/ref/elixir/.gitignore new file mode 100644 index 0000000..6e1db0f --- /dev/null +++ b/ref/elixir/.gitignore @@ -0,0 +1,17 @@ +# 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 3rd-party dependencies like ExDoc output generated docs. +/doc + +# 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 diff --git a/ref/elixir/README.md b/ref/elixir/README.md new file mode 100644 index 0000000..7dc6234 --- /dev/null +++ b/ref/elixir/README.md @@ -0,0 +1,91 @@ +# BIP-0173 [![Build Status](https://travis-ci.org/stampery/elixir-bip0173.svg?branch=master)](https://travis-ci.org/stampery/elixir-bip0173) + +**Elixir implementation of Bitcoin's address format for native SegWit outputs.** + +Upstream GitHub repository: [stampery/elixir-bip0173](https://github.com/stampery/elixir-bip0173) + +## About BIP-0173 and Bech32 + +[BIP-0173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) proposes a checksummed base32 format, "Bech32", and a standard for native segregated witness output addresses using it. + +You can find more information in [the original proposal](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) by [@sipa](https://github.com/sipa) and [@gmaxwell](https://github.com/gmaxwell). + +## Installation + + 1. Add `bip0173` to your list of dependencies in `mix.exs`: + +```elixir + def deps do + [{:bip0173, "~> 0.1.2"}] + end +``` + +## How to use + +You can find the full API reference and examples in the [online documentation at Hexdocs](https://hexdocs.pm/bip0173/api-reference.html). + +### Bech32 + +#### Encoding data to Bech32 string +```elixir +iex> Bech32.encode("bech32", [0, 1, 2]) +"bech321qpz4nc4pe" +``` +```elixir +iex> Bech32.encode("bc", [0, 14, 20, 15, 7, 13, 26, 0, 25, 18, 6, 11, 13, +...> 8, 21, 4, 20, 3, 17, 2, 29, 3, 12, 29, 3, 4, 15, 24,20, 6, 14, 30, 22]) +"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" +``` + +#### Decoding data from Bech32 string +```elixir +iex> Bech32.decode("bech321qpz4nc4pe") +{:ok, {"bech32", [0, 1, 2]}} +``` +``` elixir +iex> Bech32.decode("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") +{:ok, {"bc", [0, 14, 20, 15, 7, 13, 26, 0, 25, 18, 6, 11, 13, 8, 21, + 4, 20, 3, 17, 2, 29, 3, 12, 29, 3, 4, 15, 24, 20, 6, 14, 30, 22]}} +``` + +### SegwitAddr + +#### Encoding a SegWit program into BIP-0173 format +```elixir +iex> SegwitAddr.encode("bc", "0014751e76e8199196d454941c45d1b3a323f1433bd6") +"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" +``` +```elixir +iex> SegwitAddr.encode("bc", 0, [117, 30, 118, 232, 25, 145, 150, 212, +...> 84, 148, 28, 69, 209, 179, 163, 35, 241, 67, 59, 214]) +"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" +``` + +#### Decoding a BIP-0173 address into a SegWit program and formatting it as an hexadecimal ScriptPubKey +```elixir +iex> SegwitAddr.decode("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") +{:ok, {"bc", 0, [117, 30, 118, 232, 25, 145, 150, 212, +84, 148, 28, 69, 209, 179, 163, 35, 241, 67, 59, 214]}} +``` +```elixir +iex> SegwitAddr.to_script_pub_key(0, [117, 30, 118, 232, 25, 145, 150, +...> 212, 84, 148, 28, 69, 209, 179, 163, 35, 241, 67, 59, 214]) +"0014751e76e8199196d454941c45d1b3a323f1433bd6" +``` + +## Development + +### Running tests +```bash +$ mix deps.get +$ mix test +``` + +### Running static analysis + +This package uses Erlang's [dialyzer](http://erlang.org/doc/man/dialyzer.html) to find software discrepancies such as definite type errors, code which has become dead or unreachable due to some programming error, unnecessary tests, etc. + +```bash +$ mix deps.get +$ mix dialyzer +``` diff --git a/ref/elixir/lib/bech32.ex b/ref/elixir/lib/bech32.ex new file mode 100644 index 0000000..9173551 --- /dev/null +++ b/ref/elixir/lib/bech32.ex @@ -0,0 +1,153 @@ +# Copyright (c) 2017 Adán Sánchez de Pedro Crespo +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# 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 OR COPYRIGHT HOLDERS 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. + +defmodule Bech32 do + use Bitwise + + @moduledoc ~S""" + Encode and decode the Bech32 format, with checksums. + """ + + # Encoding character set. Maps data value -> char + @charset 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' + + # Human-readable part and data part separator + @separator 0x31 + + # Generator coefficients + @generator [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + + @doc ~S""" + Encode a Bech32 string. + + ## Examples + + iex> Bech32.encode("bech32", [0, 1, 2]) + "bech321qpz4nc4pe" + + iex> Bech32.encode("bc", [0, 14, 20, 15, 7, 13, 26, 0, 25, 18, 6, 11, 13, + ...> 8, 21, 4, 20, 3, 17, 2, 29, 3, 12, 29, 3, 4, 15, 24,20, 6, 14, 30, 22]) + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" + """ + @spec encode(String.t, list(integer)) :: String.t + def encode(hrp, data) when is_list(data) do + checksummed = data ++ create_checksum(hrp, data) + dp = for (i <- checksummed), into: "", do: <> + <> + end + + @spec encode(String.t, String.t) :: String.t + def encode(hrp, data) when is_binary(data) do + encode(hrp, :binary.bin_to_list(data)) + end + + @doc ~S""" + Decode a Bech32 string. + + ## Examples + + iex> Bech32.decode("bech321qpz4nc4pe") + {:ok, {"bech32", [0, 1, 2]}} + + iex> Bech32.decode("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") + {:ok, {"bc", [0, 14, 20, 15, 7, 13, 26, 0, 25, 18, 6, 11, 13, 8, 21, + 4, 20, 3, 17, 2, 29, 3, 12, 29, 3, 4, 15, 24, 20, 6, 14, 30, 22]}} + """ + @spec decode(String.t) :: {:ok, {String.t, list(integer)}} | {:error, String.t} + def decode(bech) do + with {_, false} <- {:mixed, String.downcase(bech) != bech && + String.upcase(bech) != bech}, + bech_charlist = :binary.bin_to_list(bech), + {_, nil} <- {:oor, Enum.find( + bech_charlist, + fn (c) -> c < 33 || c > 126 end + )}, + bech = String.downcase(bech), + len = Enum.count(bech_charlist), + pos = Enum.find_index(Enum.reverse(bech_charlist), fn (c) -> + c == @separator + end), + {_, true} <- {:oor_sep, pos != nil}, + pos = len - pos - 1, + {_, false} <- {:empty_hrp, pos < 1}, + {_, false, _} <- {:short_cs, pos + 7 > len, len}, + {_, false, _} <- {:too_long, len > 90, len}, + <> = bech, + data_charlist = (for c <- :binary.bin_to_list(data) do + Enum.find_index(@charset, fn (d) -> c == d end) + end), + {_, nil} <- {:oor_data, Enum.find_index( + data_charlist, + fn (c) -> c < 0 || c > 31 end + )}, + {_, true} <- {:cs, verify_checksum(hrp, data_charlist)}, + data_len = Enum.count(data_charlist), + data = Enum.slice(data_charlist, 0, data_len - 6) + do + {:ok, {hrp, data}} + else + {:mixed, _} -> {:error, "Mixed case"} + {:oor, c} -> {:error, "Character #{inspect(<>)} out of range (#{c})"} + {:oor_sep, _} -> {:error, "No separator character"} + {:empty_hrp, _} -> {:error, "Empty HRP"} + {:short_cs, _, l} -> {:error, "Too short checksum (#{l})"} + {:too_long, _, l} -> {:error, "Overall max length exceeded (#{l})"} + {:oor_data, c} -> {:error, "Invalid data character #{inspect(<>)} (#{c})}"} + {:cs, _} -> {:error, "Invalid checksum"} + _ -> {:error, "Unknown error"} + end + end + + # Create a checksum. + defp create_checksum(hrp, data) do + values = expand_hrp(hrp) ++ data ++ [0, 0, 0, 0, 0, 0] + mod = polymod(values) ^^^ 1 + for p <- 0..5, do: (mod >>> 5 * (5 - p)) &&& 31 + end + + # Verify a checksum. + defp verify_checksum(hrp, data) do + polymod(expand_hrp(hrp) ++ data) == 1 + end + + # Expand a HRP for use in checksum computation. + defp expand_hrp(hrp) do + hrp_charlist = :binary.bin_to_list(hrp) + a_values = for c <- hrp_charlist, do: c >>> 5 + b_values = for c <- hrp_charlist, do: c &&& 31 + a_values ++ [0] ++ b_values + end + + # Find the polynomial with value coefficients mod the generator as 30-bit. + defp polymod(values) do + Enum.reduce(values, 1, fn (v, chk) -> + top = chk >>> 25 + chk = ((chk &&& 0x1ffffff) <<< 5) ^^^ v + Enum.reduce((for i <- 0..4, do: i), chk, fn(i, chk) -> + chk ^^^ if ((top >>> i) &&& 1) != 0 do + Enum.at(@generator, i) + else + 0 + end + end) + end) + end + +end diff --git a/ref/elixir/lib/segwit_addr.ex b/ref/elixir/lib/segwit_addr.ex new file mode 100644 index 0000000..f9b61c4 --- /dev/null +++ b/ref/elixir/lib/segwit_addr.ex @@ -0,0 +1,132 @@ +# Copyright (c) 2017 Adán Sánchez de Pedro Crespo +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# 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 OR COPYRIGHT HOLDERS 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. + +defmodule SegwitAddr do + use Bitwise + + @moduledoc ~S""" + Encode and decode BIP-0173 compliant SegWit addresses. + """ + + @doc ~S""" + Encode a SegWit address. + + ## Examples + + iex> SegwitAddr.encode("bc", "0014751e76e8199196d454941c45d1b3a323f1433bd6") + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" + + iex> SegwitAddr.encode("bc", 0, [117, 30, 118, 232, 25, 145, 150, 212, + ...> 84, 148, 28, 69, 209, 179, 163, 35, 241, 67, 59, 214]) + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" + """ + @spec encode(String.t, integer, list(integer)) :: String.t + def encode(hrp, version, program) when is_list(program) do + Bech32.encode(hrp, [version] ++ convert_bits(program, 8, 5)) + end + + @spec encode(String.t, String.t) :: String.t + def encode(hrp, program) when is_binary(program) do + <> = Base.decode16!(program, case: :mixed) + program + |> :binary.bin_to_list + |> (&(encode(hrp, version, &1))).() + end + + @doc ~S""" + Decode a SegWit address. + + ## Examples + + iex> SegwitAddr.decode("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") + {:ok, {"bc", 0, [117, 30, 118, 232, 25, 145, 150, 212, + 84, 148, 28, 69, 209, 179, 163, 35, 241, 67, 59, 214]}} + """ + @spec decode(String.t) + :: {:ok, {pos_integer, list(integer)}} | {:error, String.t} + def decode(addr) do + case Bech32.decode(addr) do + {:ok, {hrp, data}} -> + [version | encoded] = data + program = convert_bits(encoded, 5, 8, false) + {:ok, {hrp, version, program}} + error -> error + end + end + + @doc ~S""" + Encode a witness program into a hexadecimal ScriptPubKey. + + ## Examples + + iex> SegwitAddr.to_script_pub_key(0, [117, 30, 118, 232, 25, 145, 150, + ...> 212, 84, 148, 28, 69, 209, 179, 163, 35, 241, 67, 59, 214]) + "0014751e76e8199196d454941c45d1b3a323f1433bd6" + """ + @spec to_script_pub_key(pos_integer, list(integer)) :: String.t + def to_script_pub_key(version, program) do + [ + if version == 0 do 0 else version + 0x50 end, + Enum.count(program) | program + ] + |> :binary.list_to_bin() + |> Base.encode16(case: :lower) + end + + # General power-of-2 base conversion. + defp convert_bits(data, from, to, pad \\ true) do + max_v = (1 <<< to) - 1 + if (Enum.find(data, fn (c) -> c < 0 || (c >>> from) != 0 end)) do + nil + else + {acc, bits, ret} = Enum.reduce( + data, + {0, 0, []}, + fn (value, {acc, bits, ret}) -> + acc = ((acc <<< from) ||| value) + bits = bits + from + {bits, ret} = convert_bits_loop(to, max_v, acc, bits, ret) + {acc, bits, ret} + end + ) + if (pad && bits > 0) do + ret ++ [(acc <<< (to - bits)) &&& max_v] + else + if (bits > from || ((acc <<< (to - bits)) &&& max_v) > 0) do + nil + else + ret + end + end + end + end + + # Recursive version of the inner loop of the convert_bits function + 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 + +end diff --git a/ref/elixir/mix.exs b/ref/elixir/mix.exs new file mode 100644 index 0000000..a66a9e0 --- /dev/null +++ b/ref/elixir/mix.exs @@ -0,0 +1,39 @@ +defmodule BIP0173.Mixfile do + use Mix.Project + + def project do + [ app: :bip0173, + version: "0.1.2", + elixir: "~> 1.3", + build_embedded: Mix.env == :prod, + start_permanent: Mix.env == :prod, + description: description(), + package: package(), + deps: deps() + ] + end + + def description do + """ + Elixir implementation of Bitcoin's address format for native SegWit outputs. + """ + end + + def package do + [ + files: ["lib", "mix.exs", "README.md"], + maintainers: ["Adán Sánchez de Pedro Crespo"], + licenses: ["MIT"], + links: %{ + "GitHub" => "https://github.com/stampery/elixir-bip0173", + "BIP-0173" => "https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki", + "Other reference implementations" => "https://github.com/sipa/bech32/tree/master/ref" + } + ] + end + + defp deps do + [ {:ex_doc, "~> 0.16", only: :dev}, + {:dialyxir, "~> 0.5", only: [:dev]} ] + end +end diff --git a/ref/elixir/test/bip0173_test.exs b/ref/elixir/test/bip0173_test.exs new file mode 100644 index 0000000..9a0eee2 --- /dev/null +++ b/ref/elixir/test/bip0173_test.exs @@ -0,0 +1,77 @@ +defmodule BIP0173Test do + use ExUnit.Case + doctest Bech32 + doctest SegwitAddr + + @valid_checksum [ + "A12UEL5L", + "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", + "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", + "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", + "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", + ] + + @invalid_checksum [ + <<0x20, "1nwldj5">>, + <<0x7f, "1axkwrx">>, + "an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", + "pzry9x0s0muk", + "1pzry9x0s0muk", + "x1b4n0q5v", + "li1dgmt3", + <<"de1lg7wt", 0xff>>, + ] + + @valid_address [ + ["BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", "0014751e76e8199196d454941c45d1b3a323f1433bd6"], + ["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"], + ["bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", + "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"], + ["BC1SW50QA3JX3S", "6002751e"], + ["bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", "5210751e76e8199196d454941c45d1b3a323"], + ["tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"], + ] + + @invalid_address [ + "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", + "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", + "bc1rw5uspcuh", + "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", + "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", + "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", + "bc1gmk9yu" + ] + + test "valid_checksum" do + for bech <- @valid_checksum do + assert {:ok, {hrp, _program}} = Bech32.decode(bech) + assert hrp != nil + end + end + + test "invalid_checksum" do + for bech <- @invalid_checksum do + assert {:error, _msg} = Bech32.decode(bech) + end + end + + test "valid address" do + for [addr, hex] <- @valid_address do + assert {:ok, {hrp, version, program}} = SegwitAddr.decode(addr) + assert version != nil + assert SegwitAddr.encode(hrp, version, program) == String.downcase(addr) + assert SegwitAddr.to_script_pub_key(version, program) == hex + end + end + + test "invalid address" do + for [addr, _hex] <- @invalid_address do + assert {:error, _} = SegwitAddr.decode(addr) + end + end +end diff --git a/ref/elixir/test/test_helper.exs b/ref/elixir/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/ref/elixir/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()