mirror of
https://github.com/erlang-punch/nostr
synced 2026-04-15 01:04:44 +00:00
Erlang Bech32 and Segwit Address Implementation
This commit implements bech32 format and segwit addressing like defined in Bitcoin implementation. A module called bech32 and another one called segwit have been created. These two modules offers the support for encoding and decoding of bech32 string in different formats. In final, 99% coverage for bech32 and 97% coverage for segwit. The document is enough for now. Added an article explaining how and why Bech32/Segwit were implemented in pure Erlang.
This commit is contained in:
@@ -69,6 +69,8 @@ Here the list of currently supported
|
||||
## Other Implementation (required by nostr)
|
||||
|
||||
- [x] [BIP-0340: Schnorr Signatures for secp256k1](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)
|
||||
- [x] [BIP-0173: Base32 address format for native v0-16 witness outputs](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki)
|
||||
- [x] [BIP-0350: Bech32m format for v1+ witness addresses](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki)
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
32
include/bech32.hrl
Normal file
32
include/bech32.hrl
Normal file
@@ -0,0 +1,32 @@
|
||||
%%%===================================================================
|
||||
%%% Copyright 2023 Mathieu Kerjouan
|
||||
%%%
|
||||
%%% 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.
|
||||
%%%
|
||||
%%% @author Mathieu Kerjouan aka Niamtokik
|
||||
%%% @author Maartz
|
||||
%%% @doc
|
||||
%%% This is a draft.
|
||||
%%% @end
|
||||
%%%===================================================================
|
||||
-type hrp() :: [pos_integer()] | iodata().
|
||||
-type data() :: [integer()] | iodata().
|
||||
-type format() :: bech32 | bech32m.
|
||||
@@ -0,0 +1,561 @@
|
||||
---
|
||||
date: 2023-08-11
|
||||
title: Implementing Bech32 and Segwit Address in Pure Erlang
|
||||
subtitle: From Pair Programming to Solo Programming
|
||||
author: Mathieu Kerjouan
|
||||
keywords: erlang,otp,nostr,bech32,segwit,address,bitcoin
|
||||
license: CC BY-NC-ND
|
||||
abstract: |
|
||||
|
||||
Bech32 encoding is used by Bitcoin and Nostr to encode binary data like
|
||||
public key or signature. On Bitcoin side, another format is also
|
||||
used, called Segwit using Bech32 to encoding its payload. This
|
||||
article is a brief overview of Bech32 and Segwit implementation
|
||||
in pure Erlang. The goal is to show how to use this new implementation,
|
||||
but also to explain how and why it as been implemented. A quick
|
||||
overview of other implementation is also available at the end of
|
||||
this publication.
|
||||
|
||||
toc: true
|
||||
hyperrefoptions:
|
||||
- linktoc=all
|
||||
---
|
||||
|
||||
# Implementing Bech32 and Segwit Address in Pure Erlang
|
||||
|
||||
This implementation was created to be easily shared with other Erlang
|
||||
project/application without importing external modules. A simple
|
||||
copy/paste of `bech32` and `segwit` modules should do the job for the
|
||||
moment or one can important this application in his own project by
|
||||
including nostr as dependency. In both case, it should work.
|
||||
|
||||
In fact, this application has not been deployed in production
|
||||
environment, and we can't really trust it for now, but it will be
|
||||
pretty soon integrated in `nostr` project. Both modules have a decent
|
||||
coverage (more than 90%) and should work pretty well everywhere.
|
||||
|
||||
## Bitcoin, Bech32 and Segwit Addresses
|
||||
|
||||
[Bech32](https://en.bitcoin.it/wiki/Bech32)[^bitcoin-bech32] data
|
||||
format was designed, created and specified in
|
||||
[BIP-0173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki)[^bitcoin-bip-00173]
|
||||
by the Bitcoin core team, in particular Pieter Wuille[^github-sipa]
|
||||
and Greg Maxwell[^github-maxwell]. The goal of this new format was to
|
||||
fix issues from the old
|
||||
[base58](https://en.bitcoin.it/wiki/BIP_0032)[^bitcoin-base58] format,
|
||||
in particular its complexity and its poor performance because of
|
||||
SHA256 checksum.
|
||||
|
||||
Bech32 format is divided in 3 parts:
|
||||
|
||||
- The human readable part or HRP is used to store
|
||||
- A static separator represented by the character `1` (one)
|
||||
- A data part containing a payload and a checksum.
|
||||
|
||||
In other hand, segwit[^wikipedia-segwit] (segregated witness) address
|
||||
is kind of subset of bech32, fixing limits on the length of the
|
||||
encoded data. Only segwit address standard is implemented there.
|
||||
|
||||
If you are looking for more information about how bech32 magic is
|
||||
working, other posts[^medium-bech32-maths] can easily be found using
|
||||
your favorite search engine.
|
||||
|
||||
[^bitcoin-bech32]: [https://en.bitcoin.it/wiki/Bech32](https://en.bitcoin.it/wiki/Bech32)
|
||||
[^bitcoin-bip-0173]: [https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki)
|
||||
[^bitcoin-base58]: [https://en.bitcoin.it/wiki/BIP_0032](https://en.bitcoin.it/wiki/BIP_0032)
|
||||
[^github-sipa]: [https://github.com/sipa](https://github.com/sipa)
|
||||
[^github-maxwell]: [https://github.com/gmaxwell](https://github.com/gmaxwell)
|
||||
[^wikipedia-segwit]: [https://en.wikipedia.org/wiki/SegWit](https://en.wikipedia.org/wiki/SegWit)
|
||||
[^medium-bech32-maths]: [https://medium.com/@meshcollider/some-of-the-math-behind-bech32-addresses-cf03c7496285](https://medium.com/@meshcollider/some-of-the-math-behind-bech32-addresses-cf03c7496285)
|
||||
|
||||
## Teaching and Learning with Pair Programming
|
||||
|
||||
This implementation started as an experiment with the following
|
||||
question in mind: "is it possible to do pair programming during live
|
||||
session recorded on multi-platform?" The idea was to deploy a small
|
||||
collaborative text editor like cloud9[^cloud9-ide] in an isolated
|
||||
place, share a link to this editor to the developers and start working
|
||||
on the project, without a lot of preparation.
|
||||
|
||||
The good part of this experiment was to see the different point of
|
||||
view during the implementation of the algorithm, based on the
|
||||
respective experiences of each participants. It was interesting to see
|
||||
what kind of solution was firstly implemented and how it evolved.
|
||||
|
||||
The bad part was quite important though. Because of the lack of
|
||||
preparation, the code was not correctly implemented. During the
|
||||
experiment, it was quite hard to think about the different features.
|
||||
|
||||
It's always hard to work with someone else, even more when you are
|
||||
recorded and without any kind of preparation. If you want to record
|
||||
your pair programming session, try, at least, to prepare it few weeks
|
||||
before. Try also to do a small implementation by yourself first, just
|
||||
to find which part will be more complex than another.
|
||||
|
||||
[^cloud9-ide]: https://en.wikipedia.org/wiki/Cloud9_IDE
|
||||
|
||||
## From Wild Implementation to Erlang/OTP
|
||||
|
||||
Creating implementation only from specification is hard, sometimes,
|
||||
the specifications are not even available, and one will need to read
|
||||
code example from already existing application. It could be harder at
|
||||
first to understand the code created by some anonymous developers but
|
||||
it can also help a lot.
|
||||
|
||||
The main reference used to implement it in Erlang was the Python
|
||||
version but few others like the one in Haskell, C and Ruby helped a
|
||||
lot as well. One big advantage with script languages like Python or
|
||||
Ruby is the way you can develop by just crafting something on a
|
||||
REPL. Recreating the module in this environment by just copying and
|
||||
pasting it or by importing it works pretty well.
|
||||
|
||||
The previous implementation of Schnorr scheme[^schnorr] was also using
|
||||
this model and it makes a lot of things easier. Firstly, having a code
|
||||
you can play with will help you to design the interfaces you will
|
||||
create on your own implementation. Everyone is designing them
|
||||
differently because of their experiences or their requirement. In the
|
||||
case of bech32 and segwit, the implementation was created with already
|
||||
some helpers in mind.
|
||||
|
||||
Secondly, having an example in another language will help you to
|
||||
design the state(s) you will need to deal with. One of the most
|
||||
complex task is to manage the different transition of the state,
|
||||
looking on the other implemention will give you an idea of the moving
|
||||
part of the application.
|
||||
|
||||
[^schnorr]: https://github.com/erlang-punch/nostr/tree/main/notes/0004-schnorr-signature-scheme-in-erlang
|
||||
|
||||
## Usage and Example
|
||||
|
||||
The reference implementation in Python looks a bit weird at first
|
||||
glance, in particular because if something goes wrong, nothing is
|
||||
returned. Any developers using this version in production environment
|
||||
will probably have troubles when understanding a crash because of
|
||||
that. As sysadmin and Erlang developer, I hate being in a situation
|
||||
where something is crashing without any explicit reason. This
|
||||
bech32/segwit implementation is fixing that by explicitely returning
|
||||
errors and their reason or by raising an error if no matching clause
|
||||
has been found.
|
||||
|
||||
```erlang
|
||||
1> bech32:encode("123", "test").
|
||||
{error,[{reason,"Invalid char"},
|
||||
{char,"t"},
|
||||
{position,0},
|
||||
{head,[]},
|
||||
{rest,"est"}]}
|
||||
```
|
||||
|
||||
Bech32 encoding works only with indexed values by default. The first
|
||||
argument is the HRP part as `string()` and can use any kind of
|
||||
printable characters. The second argument though is the indexed value,
|
||||
and should use a list of integers, each one greater or equal to 0 and
|
||||
lesser than 32.
|
||||
|
||||
```erlang
|
||||
2> bech32:encode("123", [ X || X <- lists:seq(0,31) ]).
|
||||
{ok,"1231qpzry9x8gf2tvdw0s3jn54khce6mua7l4lr8s5"}
|
||||
```
|
||||
|
||||
As you can see, this function used with these arguments is returning a
|
||||
correct bech32 encoded string without any errors. Using indexed values
|
||||
is not always what you want and what you need. In fact, I know lot of
|
||||
developers would probably use also unindexed data by default for some
|
||||
of their hacks. `bech32:encode/2` is only a wrapper around
|
||||
`bech32:encode/3` where the last argument contains the extra
|
||||
options. If someone want to use unindexed value, or switch to another
|
||||
bech32 format like `bech32m`, one can simply define it in this part of
|
||||
the function.
|
||||
|
||||
```erlang
|
||||
3> bech32:encode("123", "test", [{format, bech32}, {indexed, false}]).
|
||||
{ok,"1231w3jhxaq5p4x29"}
|
||||
|
||||
4> bech32:encode("123", "test", [{format, bech32m}, {indexed, false}]).
|
||||
{ok,"1231w3jhxaqpa9208"}
|
||||
```
|
||||
|
||||
This feature can be quite interesting to encode on the fly any kind of
|
||||
unindexed data and use bech32 as serializer mechanism. If you want to
|
||||
encode External Term Format (a format used to export terms in Erlang,
|
||||
also known as BERT) it is possible to do it easily.
|
||||
|
||||
```erlang
|
||||
5> bech32:encode("123", term_to_binary([{ok, value}])
|
||||
,[{format, bech32m}, {indexed, false}]).
|
||||
{ok,"1231sdkqqqqqq95qyeqqqfhkkeqqq4mxzmr4v44qh4dmfp"}
|
||||
```
|
||||
|
||||
The bech32 string produced contain now the value of the serialized
|
||||
term `[{ok, value}]`.
|
||||
|
||||
```erlang
|
||||
6> bech32:decode("1231sdkqqqqqq95qyeqqqfhkkeqqq4mxzmr4v44qh4dmfp").
|
||||
{ok,#{checksum => [23,21,13,27,9,1],
|
||||
data => [16,13,22,0,0,0,0,0,0,5,20,
|
||||
0,4,25,0,0,0,9,23,22,22,25,
|
||||
0,0,0,21,27,6,2,27,3,21,12,
|
||||
21,21,0],
|
||||
format => bech32m,hrp => "123",
|
||||
origin => "1231sdkqqqqqq95qyeqqqfhkkeqqq4mxzmr4v44qh4dmfp"}
|
||||
}
|
||||
```
|
||||
|
||||
When decoded, the data returned are using base32 and must be converted
|
||||
to another base if you want to use it correctly. Because developers
|
||||
are lazy, an option has been created to help them to automatically
|
||||
convert this data into any kind of base (or format).
|
||||
|
||||
|
||||
```erlang
|
||||
7> bech32:decode("1231sdkqqqqqq95qyeqqqfhkkeqqq4mxzmr4v44qh4dmfp"
|
||||
,[{converter, {base, 8}}]).
|
||||
{ok,#{checksum => [23,21,13,27,9,1],
|
||||
data => [131,108,0,0,0,1,104,2,100,0,
|
||||
2,111,107,100,0,5,118,97,108,
|
||||
117,101,106,0],
|
||||
format => bech32m,hrp => "123",
|
||||
origin => "1231sdkqqqqqq95qyeqqqfhkkeqqq4mxzmr4v44qh4dmfp"}
|
||||
}
|
||||
```
|
||||
|
||||
Using `{convert, {base, 8}}`, the function is now returning a list of
|
||||
integer using bytes. If you are familiar with ETF, you can see the
|
||||
first integer is `131`, it's the tag used to identify this kind of
|
||||
data. Does it mean our data was correctly decoded? Let try it.
|
||||
|
||||
```erlang
|
||||
8> Data =
|
||||
8> {ok, #{ data := Data}} =
|
||||
8> bech32:decode("1231sdkqqqqqq95qyeqqqfhkkeqqq4mxzmr4v44qh4dmfp"
|
||||
,[{converter, {base, 8}}]).
|
||||
|
||||
9> erlang:binary_to_term(list_to_binary(Data)).
|
||||
[{ok,value}]
|
||||
```
|
||||
|
||||
The data returned is the serialized one created previously but the
|
||||
conversion is done outside the converter. To make things easier (and
|
||||
lazyer), a function can be passed to the converter using `{converter,
|
||||
fun(D) -> {ok, D} end}`.
|
||||
|
||||
```erlang
|
||||
10> Converter = fun(Data) ->
|
||||
10> % convert base5 to base8
|
||||
10> {ok, Bytes} = bech32:convertbits(Data, 5, 8),
|
||||
10> % convert list to binary
|
||||
10> Binary = list_to_binary(Bytes),
|
||||
10> % convert binary to term
|
||||
10> {ok, binary_to_term(Binary)}
|
||||
10> end.
|
||||
#Fun<erl_eval.42.3316493>
|
||||
|
||||
11> bech32:decode("1231sdkqqqqqq95qyeqqqfhkkeqqq4mxzmr4v44qh4dmfp"
|
||||
11> ,[{converter, Converter}]).
|
||||
{ok,#{checksum => [23,21,13,27,9,1],
|
||||
data => [{ok,value}],
|
||||
format => bech32m,hrp => "123",
|
||||
origin => "1231sdkqqqqqq95qyeqqqfhkkeqqq4mxzmr4v44qh4dmfp"}
|
||||
}
|
||||
```
|
||||
|
||||
Now, the data returned is the good one, already deserialized and
|
||||
valid. The function `convertbits/3` has been used, it's the good time
|
||||
to talk a bit about this one. The bech32 model implementation offered
|
||||
by Bitcoin project and made in Python gives access to a function
|
||||
called `convertbits`. It is used to convert from one base to
|
||||
another. In fact, in Erlang we already have this feature with binary
|
||||
and bitstring term coupled with pattern matching, but in this
|
||||
particular scenario, I found this `convertbits` function easier to
|
||||
use.
|
||||
|
||||
```erlang
|
||||
12> bech32:convertbits("test", 8, 6).
|
||||
{ok,[29,6,21,51,29,0]}
|
||||
|
||||
13> bech32:convertbits("test", 8, 64).
|
||||
{ok,[8387236823100817408]}
|
||||
|
||||
14> bech32:convertbits("test", 8, 128).
|
||||
{ok,[154717211161333530444085113906767331328]}
|
||||
|
||||
15> bech32:convertbits("test", 8, 1)
|
||||
{ok,[0,1,1,1,0,1,0,0,0,1,1,0,0,1,0,1,0,1,
|
||||
1,1,0,0,1,1,0,1,1,1,0,1,0,0]}
|
||||
```
|
||||
|
||||
In the example below, an Erlang string (a list of printable integers)
|
||||
is converted to different base like base6, base64, base128 and in
|
||||
bits. `convertbits/3` is probably slower than doing a simple
|
||||
conversion with binary pattern matching, but it offers more
|
||||
flexibility. Perhaps small optimization can be done there.
|
||||
|
||||
The next part is segwit, this format is a bit more restrictive than
|
||||
raw bech32 encoding. Firstly, two versions of segwit are available,
|
||||
version 0 and version 1. Version 0 set a limit to the length of the
|
||||
data section to 20 elements.
|
||||
|
||||
```erlang
|
||||
15> segwit:encode("test", 0, [ X || X <- lists:seq(1,19) ]).
|
||||
{error,[{reason,"Invalid data length"}]}
|
||||
|
||||
16> segwit:encode("test", 0, [ X || X <- lists:seq(1,21) ]).
|
||||
{error,[{reason,"Invalid data length"}]}
|
||||
|
||||
16> segwit:encode("test", 0, [ X || X <- lists:seq(1,20) ]).
|
||||
{ok,"test1qqypqxpq9qcrsszg2pvxq6rs0zqg3yyc5uskwrt"}
|
||||
```
|
||||
|
||||
In other hands, this is not really the case with version 1 of the
|
||||
format, where it can encode a value from 2 to 40 elements.
|
||||
|
||||
```erlang
|
||||
17> segwit:encode("test", 1, [ X || X <- [1,2] ]).
|
||||
{ok,"test1pqypqe8v4yj"}
|
||||
|
||||
18> segwit:encode("test", 1, [ X || X <- lists:seq(1,40) ]).
|
||||
{ok,"test1pqypqxpq9qcrsszg2pvxq6rs0zqg3yyc5z5tpwxqergd3c8g7"
|
||||
"ruszzg3rysjjvfegauug3k"}
|
||||
```
|
||||
|
||||
To decode a segwit address, one can use `segwit:decode/1` and will
|
||||
have all required information about it retured as map.
|
||||
|
||||
```erlang
|
||||
19> segwit:decode("test1pqypqxpq9qcrsszg2pvxq6rs0zqg3yyc5z5t"
|
||||
19> "pwxqergd3c8g7ruszzg3rysjjvfegauug3k").
|
||||
{ok,#{hrp => "test",
|
||||
value => [1,2,3,4,5,6,7,8,9,10
|
||||
,11,12,13,14,15,16,17
|
||||
,18,19,20,21,22,23,24
|
||||
,25,26|...],
|
||||
version => 1}
|
||||
}
|
||||
```
|
||||
|
||||
Let try all these marvelous new functions on real world data. The
|
||||
first to test is with a nostr address, perhaps the most important
|
||||
feature for this nostr client/server project. [Edward
|
||||
Snowden](https://twitter.com/Snowden) nostr address is
|
||||
`npub1sn0wdenkukak0d9dfczzeacvhkrgz92ak56egt7vdgzn8pv2wfqqhrjdv9`.
|
||||
|
||||
```erlang
|
||||
20> bech32:decode("npub1sn0wdenkukak0d9dfczzeacvhkrgz92ak56egt7vd"
|
||||
20> "gzn8pv2wfqqhrjdv9").
|
||||
{ok,#{checksum => [23,3,18,13,12,5],
|
||||
data => [16,19,15,14,13,25,19,22,28,22,29,22,15,13,5,13,9,
|
||||
24,2,2,25,29,24,12,23,22,3,8,2,5,10,29,22,20,26,
|
||||
25,8,11,30,12,13,8,2,19,7,1,12,10,14,9,0,0],
|
||||
format => bech32,
|
||||
hrp => "npub",
|
||||
origin =>
|
||||
"npub1sn0wdenkukak0d9dfczzeacvhkrgz92ak56egt7vdgzn8pv2wfqqhrjdv9"}
|
||||
}
|
||||
```
|
||||
|
||||
It looks good, the function deserialize the address, print the
|
||||
checksum, return the data and gives the HRP. What about segwit address
|
||||
now. To have a raw segwit address, the best way is to go to
|
||||
[blockchain.com](https://www.blockchain.com/explorer/) and take the
|
||||
first [transaction
|
||||
displayed](https://www.blockchain.com/explorer/transactions/btc/9b53984bcde985a9b5bc7c729bc48ded84d5074b51633d6f8668a1ba74b72dba). The
|
||||
transaction was from `bc1q52265p57jwxph407dqp9r9cucm3qvrqwg9kqpu` to
|
||||
`bc1q0nwkgcvxhufess9keeszv7vw8xq8zmuy3upa7t`.
|
||||
|
||||
```erlang
|
||||
21> segwit:decode("bc1q52265p57jwxph407dqp9r9cucm3qvrqwg9kqpu")
|
||||
{ok,#{hrp => "bc",
|
||||
value => [162,149,170,6,158,147,140,27,213,254,104,2,81,
|
||||
151,28,198,226,6,12,14],
|
||||
version => 0}}
|
||||
|
||||
22> segwit:decode("bc1q0nwkgcvxhufess9keeszv7vw8xq8zmuy3upa7t").
|
||||
{ok,#{hrp => "bc",
|
||||
value => [124,221,100,97,134,191,19,152,64,182,206,96,
|
||||
38,121,142,57,128,113,111,132],
|
||||
version => 0}}
|
||||
```
|
||||
|
||||
It seems `bech32` and `segwit` modules can parse values from the wild.
|
||||
|
||||
## Under the Hood
|
||||
|
||||
The whole code is divided in small functions doing one thing, but
|
||||
trying to do it well.
|
||||
|
||||
Charset and indexing is done using `index_to_charset/1` and
|
||||
`charset_to_index/1` functions. Both are really simple one getting an
|
||||
integer and converting it to another one (ASCII for example). To be
|
||||
sure a character is available in the charset, the function
|
||||
`is_charset/1` has been created, same has been done for the indexed
|
||||
values with `is_index/1` function.
|
||||
|
||||
Strings or indexed strings must be tested, they are by using
|
||||
respectively `valid_indexed_string/1` and `valid_string/1`. Both of
|
||||
these functions are really important to ensure the data to be parsed
|
||||
and serialized or deserialized.
|
||||
|
||||
Bech32 format is including lot of interesting function, in particular
|
||||
a feature to checksum and reconstruct the message in case of
|
||||
problem. The algorithm used is still a bit obscure at my level of
|
||||
knowledge but seems to work perfectly. `polymod/1` function generates
|
||||
the checksum needed to reconstruct and ensure the data sent have been
|
||||
correctly received. A macro has been used here to simulate a loop as a
|
||||
pipeline.
|
||||
|
||||
```erlang
|
||||
% ...
|
||||
-define(POLYMOD_GENERATOR(INDEX, GENERATOR),
|
||||
polymod_generator(INDEX, Checksum, Top) ->
|
||||
case (Top bsr INDEX) band 1 of
|
||||
0 ->
|
||||
polymod_generator(INDEX+1, Checksum bxor 0, Top);
|
||||
_Value ->
|
||||
polymod_generator(INDEX+1, Checksum bxor GENERATOR, Top)
|
||||
end
|
||||
).
|
||||
% ...
|
||||
?POLYMOD_GENERATOR(0, 16#3B6A_57B2);
|
||||
?POLYMOD_GENERATOR(1, 16#2650_8E6D);
|
||||
?POLYMOD_GENERATOR(2, 16#1EA1_19FA);
|
||||
?POLYMOD_GENERATOR(3, 16#3D42_33DD);
|
||||
?POLYMOD_GENERATOR(4, 16#2A14_62B3);
|
||||
polymod_generator(5, Checksum, _Top) -> Checksum.
|
||||
```
|
||||
|
||||
The rest is mainly functions used to convert and validate the value,
|
||||
this previous one is probably the most completed created in this
|
||||
project. Decoding and encoding functions are executed as instruction
|
||||
flow, one after another one until the final result is being returned
|
||||
by `encode` or `decode` functions.
|
||||
|
||||
At this time, the coverage of these two modules, `segwit` and `bech32`
|
||||
are quite pretty good:
|
||||
|
||||
| module | coverage |
|
||||
|---------|------------|
|
||||
| [bech32](https://github.com/erlang-punch/nostr/blob/main/src/bech32.erl) | 99% |
|
||||
| [segwit](https://github.com/erlang-punch/nostr/blob/main/src/segwit.erl) | 97% |
|
||||
|
||||
Here a quick analysis of these modules with
|
||||
[`scc`](https://github.com/boyter/scc).
|
||||
|
||||
| Language | File | Lines | Blanks | Comments | Code | Complexity |
|
||||
| :------- | ----: | ----: | ----: | ----: | ----: | ----: |
|
||||
| Erlang | `bech32.erl` | 1316 | 75 | 413 | 828 | 10 |
|
||||
| Erlang | `segwip.erl` | 263 | 18 | 135 | 110 | 2 |
|
||||
|
||||
In total, the full bech32/segwit implementation with all the coverage
|
||||
was made in 1579 line of Erlang code (including comments and blank
|
||||
lines).
|
||||
|
||||
| Language | Files | Lines | Blanks | Comments | Code | Complexity |
|
||||
| :------- | ----: | ----: | -----: | -------: | ---: | ---------: |
|
||||
| Erlang | 2 | 1579 | 93 | 548 | 938 | 12 |
|
||||
| Total | 2 | 1579 | 93 | 548 | 938 | 12 |
|
||||
|
||||
The final result of estimated cost, schedule effort and people
|
||||
required:
|
||||
|
||||
- **Estimated Cost to Develop** (organic): $25,258
|
||||
- **Estimated Schedule Effort** (organic): 3.398828 months
|
||||
- **Estimated People Required** (organic): 0.660230
|
||||
|
||||
In fact, this implementation in Erlang takes around 3 days with one
|
||||
developer dedicated full time but that's an estimation, other analysis
|
||||
tools could have produced more accurate results.
|
||||
|
||||
## Conclusion, Optimizations and Future Modifications
|
||||
|
||||
This is a first implementation, made from scratch based on Python
|
||||
model implementation, and lot of features are missing, even if this
|
||||
implementation seems flexible, a bit of cleanup will be required.
|
||||
|
||||
This code is not optimized, it has been created quickly but it was
|
||||
correctly tested. Unfortunately, if this code is used in production
|
||||
environment, a clear performance problem will happen. This is why it
|
||||
is required to have an idea how to improve this implementation.
|
||||
|
||||
Proper (property based testing) and Dialyzer (static-analysis) needs
|
||||
to be added as well as profiling References.
|
||||
|
||||
Anyway, it was fun to create bech32 and segwit format in pure
|
||||
Erlang. I hope I will find more time to improve it in a near future,
|
||||
and perhaps use it outside of nostr.
|
||||
|
||||
## Alternative Implementations
|
||||
|
||||
Erlang is perhaps not the best language to deal with bech32 and
|
||||
segwit. Firstly because Erlang is not the best language for doing
|
||||
mathematics, but also because bech32 and segwit have been designed for
|
||||
more classical language. You can, though, find small implementation,
|
||||
more tested and optimized in other languages. Here a list of other
|
||||
bech32 and segwit implementation you can easily find on Github.
|
||||
|
||||
| Project | Language | File | Lines | Blanks | Comments | Code | Complexity |
|
||||
| :------ | :---- | ----------: | --: | --: | ----: | --: | --: |
|
||||
| [bech32](https://github.com/sipa/bech32) | C | `segwit_addr.c` | 209 | 11 | 20 | 178 | 90 |
|
||||
| [bech32](https://github.com/sipa/bech32) | C++ | `bech32.cpp` | 226 | 27 | 81 | 118 | 43 |
|
||||
| [bech32](https://github.com/sipa/bech32) | C++ | `segwit_addr.cpp` | 86 | 10 | 23 | 53 | 27 |
|
||||
| [bech32](https://github.com/sipa/bech32) | Go | `bech32.go` | 217 | 12 | 25 | 180 | 78 |
|
||||
| [bech32](https://github.com/sipa/bech32) | Haskell | `Bech32.hs` | 159 | 25 | 10 | 124 | 18 |
|
||||
| [bech32](https://github.com/sipa/bech32) | JavaScript | `bech32.js` | 131 | 10 | 19 | 102| 28 |
|
||||
| [bech32](https://github.com/sipa/bech32) | JavaScript | `segwit_addr.js` | 91 | 5 | 19 | 67 | 26 |
|
||||
| [bech32](https://github.com/sipa/bech32) | Python | `segwit_addr.py` | 137 | 13 | 26 | 98 | 11 |
|
||||
| [bech32](https://github.com/sipa/bech32) | Ruby | `bech32.rb` | 99 | 14 | 30 | 55 | 12 |
|
||||
| [bech32](https://github.com/sipa/bech32) | Ruby | `segwit_addr.rb` | 86 | 11 | 19 | 56 | 36 |
|
||||
| [bech32](https://github.com/sipa/bech32) | Rust | `bech32.rs` | 230 | 23 | 60 | 147 | 45 |
|
||||
| [bech32](https://github.com/sipa/bech32) | Rust | `wit_prog.rs` | 219 | 11 | 73 | 135 | 36 |
|
||||
| [bitcoinj](https://github.com/bitcoinj/bitcoinj) | Java | `Bech32.java` | 188 | 17 | 35 | 136 | 38 |
|
||||
| [bitcoinj](https://github.com/bitcoinj/bitcoinj) | Java | `SegwitAddress.java` | 464 | 42 | 196 | 226 | 58 |
|
||||
| [bitcoinaddress](https://github.com/fortesp/bitcoinaddress) | Python | `segwit_addr.py` | 123 | 13 | 25 | 85 | 7 |
|
||||
| [btclib](https://github.com/btclib-org/btclib) | Python | `bech32.py` | 146 | 28 | 56 | 62 | 28 |
|
||||
| [ocaml-bech32](https://github.com/vbmithr/ocaml-bech32) | OCaml | `bech32.ml` | 468 | 43 | 22 | 403 | 54 |
|
||||
| [bitcoin-address-validator](https://github.com/kielabokkie/bitcoin-address-validator) | PHP | `Bech32.php` | 342 | 61 | 80 | 201 | 43 |
|
||||
| [bech32](https://github.com/akovalenko/bech32) | Lisp | `bech32.lisp` | 248 | 25 | 0 | 223 | 19 |
|
||||
| [bech32](https://github.com/input-output-hk/bech32) | Haskell | `Internal.hs` | 919 | 78 | 293 | 548 | 49 |
|
||||
| [bech32-elixir](https://github.com/f2pool/bech32-elixir) | Elixir | `bech32.ex` | 356 | 47 | 14 | 299 | 12 |
|
||||
|
||||
Based on `scc`, the less complex is using Python and the most complex
|
||||
is the one in C (headers were not included).
|
||||
|
||||
## Resources
|
||||
|
||||
- [Bitcoin Wiki - BIP 0350](https://en.bitcoin.it/wiki/BIP_0350),
|
||||
[https://en.bitcoin.it/wiki/BIP_0350](https://en.bitcoin.it/wiki/BIP_0350)
|
||||
|
||||
- [Bitcoin Wiki - Bech32](https://en.bitcoin.it/wiki/Bech32),
|
||||
[https://en.bitcoin.it/wiki/Bech32](https://en.bitcoin.it/wiki/Bech32)
|
||||
|
||||
- [Bitcoin Wiki - Bech32
|
||||
Adoption](https://en.bitcoin.it/wiki/Bech32_adoption),
|
||||
[https://en.bitcoin.it/wiki/Bech32_adoption](https://en.bitcoin.it/wiki/Bech32_adoption)
|
||||
|
||||
- [Nostr Protocol -
|
||||
NIP/19](https://github.com/nostr-protocol/nips/blob/master/19.md),
|
||||
[https://github.com/nostr-protocol/nips/blob/master/19.md](https://github.com/nostr-protocol/nips/blob/master/19.md)
|
||||
|
||||
- [bech32-buffer](https://slowli.github.io/bech32-buffer/),
|
||||
[https://slowli.github.io/bech32-buffer/](https://slowli.github.io/bech32-buffer/)
|
||||
|
||||
- [Bech32 reference implementation source
|
||||
code](https://github.com/sipa/bech32/tree/master/ref),
|
||||
[https://github.com/sipa/bech32/tree/master/ref](https://github.com/sipa/bech32/tree/master/ref)
|
||||
|
||||
- [Base58 Check
|
||||
Encoding](https://en.bitcoin.it/wiki/Base58Check_encoding),
|
||||
[https://en.bitcoin.it/wiki/Base58Check_encoding](https://en.bitcoin.it/wiki/Base58Check_encoding)
|
||||
|
||||
- [(Some of) the math behind Bech32
|
||||
addresses](https://medium.com/@meshcollider/some-of-the-math-behind-bech32-addresses-cf03c7496285)
|
||||
by [Samuel Dobson](https://medium.com/@meshcollider) on medium,
|
||||
[https://medium.com/@meshcollider/some-of-the-math-behind-bech32-addresses-cf03c7496285](https://medium.com/@meshcollider/some-of-the-math-behind-bech32-addresses-cf03c7496285)
|
||||
|
||||
- [Understanding Base58 Encoding - It is all about
|
||||
integers](https://medium.com/concerning-pharo/understanding-base58-encoding-23e673e37ff6)
|
||||
by [Sven VC](https://medium.com/@svenvc),
|
||||
[https://medium.com/concerning-pharo/understanding-base58-encoding-23e673e37ff6](https://medium.com/concerning-pharo/understanding-base58-encoding-23e673e37ff6)
|
||||
|
||||
- [Bech32 Racket
|
||||
Implementation](https://docs.racket-lang.org/bech32/index.html),
|
||||
[https://docs.racket-lang.org/bech32/index.html](https://docs.racket-lang.org/bech32/index.html)
|
||||
@@ -16,6 +16,7 @@ BY-NC-ND](https://creativecommons.org/licenses/by-nc-nd/4.0/)**.
|
||||
| 2023-02-25 | [Implementing nostr Client in Erlang](0003-implementing-nostr-client-in-erlang) | Mathieu Kerjouan | R25
|
||||
| 2023-02-10 | [From Erlang to nostr](0002-from-erlang-to-nostr) | Mathieu Kerjouan | R25
|
||||
| 2023-02-10 | [Create Github Actions Workflow](0001-create-github-actions-workflow) | Mathieu Kerjouan | R25
|
||||
| 2023-08-11 | [Implementing Bech32 and Segwit Address in Pure Erlang](0009-implementing-bech32-and-segwit-address-in-pure-erlang) | Mathieu Kerjouan | R25
|
||||
|
||||
The codes presented in these articles are usually tested under OpenBSD
|
||||
and ParrotLinux (Debian-like distribution) with the latest major
|
||||
|
||||
1316
src/bech32.erl
Normal file
1316
src/bech32.erl
Normal file
File diff suppressed because it is too large
Load Diff
263
src/segwit.erl
Normal file
263
src/segwit.erl
Normal file
@@ -0,0 +1,263 @@
|
||||
%%%===================================================================
|
||||
%%% Copyright 2023 Mathieu Kerjouan
|
||||
%%%
|
||||
%%% 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.
|
||||
%%%
|
||||
%%% @author Mathieu Kerjouan aka Niamtokik
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% == Encoding Usage ==
|
||||
%%%
|
||||
%%% `encode/1' function is used to encode `map()' data like the one
|
||||
%%% returned by `decoded' functions. `encode/3' function is taking the
|
||||
%%% HRP, the segwit address version and the content to encode the full
|
||||
%%% address. Both functions support `list()' or `binary()' strings.
|
||||
%%%
|
||||
%%% ```
|
||||
%%% % Using segwit addressing version 0
|
||||
%%% X0 = [ X || X <- lists:seq(1,20) ].
|
||||
%%% {ok,"test1qqypqxpq9qcrsszg2pvxq6rs0zqg3yyc5uskwrt"}
|
||||
%%% = segwit:encode("test", 0, X0).
|
||||
%%%
|
||||
%%% X1 = <<242,1,222,236,34,170,153,51,244,185,250,46,
|
||||
%%% 97,15,13,60,200,225,235,73>>.
|
||||
%%% {ok,"test1q7gqaampz42vn8a9elghxzrcd8nywr66fupy0m7"}
|
||||
%%% = segwit:encode("test", 0, X1).
|
||||
%%%
|
||||
%%% % Using segwit addressing version 1
|
||||
%%% Y0 = [ X || X <- lists:seq(0,1) ].
|
||||
%%% {ok,"test1pqqqstnxe9u"}
|
||||
%%% = segwit:encode("test", 1, Y0).
|
||||
%%%
|
||||
%%% Y1 = [ X || X <- lists:seq(0,39) ].
|
||||
%%% {ok,"test1pqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0jqgfzyvjz2f38ykjdds"}
|
||||
%%% = segwit:encode("test", 1, Y1).
|
||||
%%%
|
||||
%%% Y3 = <<185,45,39,5,190,182,64,200,255,162,155,35,
|
||||
%%% 222,94,176,194,235,116,165,19,179,96,83>>.
|
||||
%%% {ok,"test1phykjwpd7keqv3laznv3auh4sct4hffgnkds9x8m9qgt"}
|
||||
%%% = segwit:encode("test", 1, Y3).
|
||||
%%% '''
|
||||
%%%
|
||||
%%% == Decoding Usage ==
|
||||
%%%
|
||||
%%% `decode/1' and `decode/2' are used to decode segwit address as
|
||||
%%% `list()' or `binary()'. They will both return a `map()' containing
|
||||
%%% the decoded values.
|
||||
%%%
|
||||
%%% ```
|
||||
%%% {ok, #{ hrp => "test"
|
||||
%%% , value => [185,45,39,5,190,182,64,200,
|
||||
%%% 255,162,155,35,222,94,176,194,
|
||||
%%% 235,116,165,19,179,96,83]
|
||||
%%% , version => 1
|
||||
%%% }
|
||||
%%% } = segwit:decode("test1phykjwpd7keqv3laznv3auh4sct4hffgnkds9x8m9qgt").
|
||||
%%% '''
|
||||
%%%
|
||||
%%% @end
|
||||
%%%===================================================================
|
||||
-module(segwit).
|
||||
-export([encode/1, encode/3]).
|
||||
-export([decode/1, decode/2]).
|
||||
-include("bech32.hrl").
|
||||
|
||||
% local record
|
||||
-record(segwit_address, { version = undefined :: undefined | integer()
|
||||
, value = undefined :: undefined | binary() | list()
|
||||
, hrp = undefined :: undefined | list() | binary()
|
||||
}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc encode data with a sigwit address from a map.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec encode(Map) -> Return when
|
||||
Map :: #{ hrp => hrp(), version => integer(), value => list() },
|
||||
Return :: list().
|
||||
|
||||
encode(#{ hrp := HRP, version := Version, value := Value }) ->
|
||||
encode(HRP, Version, Value).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc encode data with a sigwit address.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec encode(HRP, Witver, Witprog) -> Return when
|
||||
HRP :: hrp(),
|
||||
Witver :: integer(),
|
||||
Witprog :: binary() | string(),
|
||||
Return :: {ok, list()} | {error, Reason},
|
||||
Reason :: proplists:proplist().
|
||||
|
||||
encode(HRP, Witver, Witprog)
|
||||
when is_binary(HRP) ->
|
||||
encode(binary_to_list(HRP), Witver, Witprog);
|
||||
encode(HRP, Witver, Witprog)
|
||||
when is_list(HRP) ->
|
||||
encode1(HRP, Witver, Witprog).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @hidden
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
encode1(HRP, Witver, Witprog)
|
||||
when is_integer(Witver) andalso Witver >= 0 ->
|
||||
encode2(HRP, Witver, Witprog).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @hidden
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
encode2(HRP, Witver, Witprog)
|
||||
when is_binary(Witprog) ->
|
||||
encode2(HRP, Witver, binary_to_list(Witprog));
|
||||
encode2(HRP, Witver, Witprog)
|
||||
when is_list(Witprog) ->
|
||||
encode_format(#{ hrp => HRP, witver => Witver, witprog => Witprog }).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @hidden
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
encode_format(#{ witver := Witver } = State) ->
|
||||
Spec = case Witver of
|
||||
0 -> bech32;
|
||||
_ -> bech32m
|
||||
end,
|
||||
encode_conversion(State#{ format => Spec }).
|
||||
|
||||
% @hidden
|
||||
encode_conversion(#{ witprog := Witprog } = State) ->
|
||||
{ok, Converted} = bech32:convertbits(Witprog, 8, 5),
|
||||
encode_bech32(State#{ converted => Converted }).
|
||||
|
||||
% @hidden
|
||||
encode_bech32(#{ hrp := HRP, witver := Witver, converted := Converted, format := Spec } = State) ->
|
||||
Data = [Witver] ++ Converted,
|
||||
case bech32:encode(HRP, Data, [{format, Spec}]) of
|
||||
{ok, Encoded} -> encode_final(State#{ encoded => Encoded });
|
||||
Elsewise -> Elsewise
|
||||
end.
|
||||
|
||||
% @hidden
|
||||
encode_final(#{ hrp := HRP, encoded := Encoded }) ->
|
||||
case decode(HRP, Encoded) of
|
||||
{ok, _} -> {ok, Encoded};
|
||||
Elsewise -> Elsewise
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @hidden
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
extract_hrp(Address)
|
||||
when is_binary(Address) ->
|
||||
extract_hrp(binary_to_list(Address));
|
||||
extract_hrp(Address)
|
||||
when is_list(Address) ->
|
||||
extract_hrp(Address, lists:reverse(Address)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @hidden
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
extract_hrp(Address, [$1|Rest]) ->
|
||||
HRP = lists:reverse(Rest),
|
||||
{ok, HRP, Address};
|
||||
extract_hrp(Address, [_|T]) ->
|
||||
extract_hrp(Address, T).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc decode a sigwit address. This function can easily crash
|
||||
%% because it splits the string in two parts, the HRP, and the full
|
||||
%% address.
|
||||
%%
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec decode(Address) -> Return when
|
||||
Address :: list() | binary(),
|
||||
Return :: {ok, map()}.
|
||||
|
||||
decode(String) ->
|
||||
{ok, HRP, Address} = extract_hrp(String),
|
||||
decode(HRP, Address).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc decode a sigwit address.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec decode(any(), any()) -> any().
|
||||
decode(HRP, Address) ->
|
||||
case bech32:decode(Address) of
|
||||
{ok, #{ hrp := BechHRP}}
|
||||
when HRP =/= BechHRP ->
|
||||
{error, [{reason, "Invalid hrp"}]};
|
||||
{ok, Bech} ->
|
||||
decode2(Bech);
|
||||
Elsewise -> Elsewise
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @hidden
|
||||
%% @doc internal.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
decode2(#{ data := [] } = _Bech) ->
|
||||
{error, [{reason, "Empty data section"}]};
|
||||
decode2(#{ data := [Version|Data], format := Spec, hrp := HRP } = _Bech) ->
|
||||
case bech32:convertbits(Data, 5, 8, [{padding, false}]) of
|
||||
{ok, Decoded} ->
|
||||
case Decoded of
|
||||
_ when length(Decoded) < 2 ->
|
||||
{error, [{reason, "Invalid program length for witness version 0 (per BIP141)"}]};
|
||||
_ when length(Decoded) > 40 ->
|
||||
{error, [{reason, "Invalid program length for witness version 0 (per BIP141)"}]};
|
||||
_ when Version > 16 ->
|
||||
{error, [{reason, "Invalid version"}]};
|
||||
_ when Version =:= 0 andalso length(Decoded) =/= 20 andalso length(Decoded) =/= 32 ->
|
||||
{error, [{reason, "Invalid data length"}]};
|
||||
_ when Version =:= 0 andalso Spec =/= bech32 ->
|
||||
{error, [{reason, "Invalid version and format"}]};
|
||||
_ when Version =/= 0 andalso Spec =/= bech32m ->
|
||||
{error, [{reason, "Invalid version and format"}]};
|
||||
Decoded ->
|
||||
Segwit = #segwit_address{ version = Version, value = Decoded, hrp = HRP},
|
||||
{ok, segwit_address_to_map(Segwit)}
|
||||
end;
|
||||
Elsewise -> Elsewise
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @hidden
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
segwit_address_to_map(#segwit_address{ version = Version, value = Decoded, hrp = HRP }) ->
|
||||
#{ version => Version
|
||||
, value => Decoded
|
||||
, hrp => HRP
|
||||
}.
|
||||
338
test/segwit_SUITE.erl
Normal file
338
test/segwit_SUITE.erl
Normal file
@@ -0,0 +1,338 @@
|
||||
%%%===================================================================
|
||||
%%% Copyright 2023 Mathieu Kerjouan
|
||||
%%%
|
||||
%%% 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.
|
||||
%%%
|
||||
%%% @author Mathieu Kerjouan
|
||||
%%% @doc
|
||||
%%% @end
|
||||
%%%===================================================================
|
||||
-module(segwit_SUITE).
|
||||
-export([suite/0]).
|
||||
-export([init_per_suite/1, end_per_suite/1]).
|
||||
-export([init_per_group/2, end_per_group/2]).
|
||||
-export([init_per_testcase/2, end_per_testcase/2]).
|
||||
-export([groups/0, all/0]).
|
||||
-export([valid/0, valid/1]).
|
||||
-export([invalid/0, invalid/1]).
|
||||
-export([invalid_input/0, invalid_input/1]).
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
-spec suite() -> any().
|
||||
-spec init_per_suite(any()) -> any().
|
||||
-spec end_per_suite(any()) -> any().
|
||||
-spec init_per_group(any(), any()) -> any().
|
||||
-spec end_per_group(any(), any()) -> any().
|
||||
-spec init_per_testcase(any(), any()) -> any().
|
||||
-spec end_per_testcase(any(), any()) -> any().
|
||||
-spec groups() -> any().
|
||||
-spec all() -> any().
|
||||
|
||||
suite() -> [{timetrap,{minutes,10}}].
|
||||
|
||||
init_per_suite(Config) -> Config.
|
||||
|
||||
end_per_suite(_Config) -> ok.
|
||||
|
||||
init_per_group(_GroupName, Config) -> Config.
|
||||
|
||||
end_per_group(_GroupName, _Config) -> ok.
|
||||
|
||||
init_per_testcase(_TestCase, Config) -> Config.
|
||||
|
||||
end_per_testcase(_TestCase, _Config) -> ok.
|
||||
|
||||
groups() -> [].
|
||||
|
||||
all() -> [valid, invalid, invalid_input].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc check valid inputs.
|
||||
%% @see valid_fixture/0
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec valid() -> any().
|
||||
valid() -> [].
|
||||
|
||||
-spec valid(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
|
||||
valid(_Config) ->
|
||||
Test = fun (#{ version := Version, value := Value, hrp := HRP, address := Address } = Data) ->
|
||||
ct:pal(info, "valid: ~p", [Data]),
|
||||
Lower = string:lowercase(Address),
|
||||
{ok, _} = segwit:decode(Lower),
|
||||
{ok, _} = segwit:decode(list_to_binary(Lower)),
|
||||
{ok, Lower} = segwit:encode(HRP, Version, Value),
|
||||
{ok, Lower} = segwit:encode(list_to_binary(HRP), Version, list_to_binary(Value)),
|
||||
{ok, #{ version := Version , value := Value, hrp := HRP } = Map }
|
||||
= segwit:decode(HRP, Address),
|
||||
{ok, Lower} = segwit:encode(Map)
|
||||
end,
|
||||
lists:map(Test, valid_data_fixture()).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc check invalid segwit address.
|
||||
%% @see invalid_fixture/0
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec invalid() -> any().
|
||||
invalid() -> [].
|
||||
|
||||
-spec invalid(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
|
||||
invalid(_Config) ->
|
||||
TestData = fun(#{ error := _Error, hrp := HRP, address := Address} = Data) ->
|
||||
Ignore = maps:get(ignore, Data, false),
|
||||
Return = segwit:decode(HRP, Address),
|
||||
ct:pal(info, "invalid data: ~p", [{Data, Return}]),
|
||||
{error, _} = Return,
|
||||
case Ignore =:= true of
|
||||
true -> ok;
|
||||
false ->
|
||||
{error, _} = segwit:decode(Address),
|
||||
ok
|
||||
end
|
||||
end,
|
||||
lists:map(TestData, invalid_data_fixture()).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc check invalid input.
|
||||
%% @see invalid_input_fixture/0
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec invalid_input() -> any().
|
||||
invalid_input() -> [].
|
||||
|
||||
-spec invalid_input(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
|
||||
invalid_input(_Config) ->
|
||||
TestInput = fun(#{ error := _Error, hrp := HRP, length := Length, version := Version } = Input) ->
|
||||
Data = [ 0 || _ <- lists:seq(1, Length) ],
|
||||
Encoded = segwit:encode(HRP, Version, Data),
|
||||
ct:pal(info, "invalid input: ~p", [{Input, Data, Encoded}]),
|
||||
{error, _} = Encoded
|
||||
end,
|
||||
lists:map(TestInput, invalid_input_fixture()).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% source: https://github.com/sipa/bech32/blob/master/ref/python/tests.py#L88
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
valid_data_fixture() ->
|
||||
[#{ version => 0
|
||||
, value => [117, 30, 118, 232, 25, 145, 150, 212,
|
||||
84, 148, 28, 69, 209, 179, 163, 35,
|
||||
241, 67, 59, 214]
|
||||
, hrp => "bc"
|
||||
, address => "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"
|
||||
}
|
||||
,#{ version => 0
|
||||
, value => [24, 99, 20, 60, 20, 197, 22, 104, 4, 189, 25, 32,
|
||||
51, 86, 218, 19, 108, 152, 86, 120, 205, 77, 39, 161,
|
||||
184, 198, 50, 150, 4, 144, 50, 98]
|
||||
, hrp => "tb"
|
||||
, address => "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"
|
||||
}
|
||||
,#{ version => 1
|
||||
, value => [117, 30, 118, 232, 25, 145, 150, 212, 84, 148, 28,
|
||||
69, 209, 179, 163, 35, 241, 67, 59, 214, 117, 30, 118,
|
||||
232, 25, 145, 150, 212, 84, 148, 28, 69, 209, 179,
|
||||
163, 35, 241, 67, 59, 214]
|
||||
, hrp => "bc"
|
||||
, address => "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y"
|
||||
}
|
||||
,#{ version => 16
|
||||
, value => [117, 30]
|
||||
, hrp => "bc"
|
||||
, address => "BC1SW50QGDZ25J"
|
||||
}
|
||||
,#{ version => 2
|
||||
, value => [117, 30, 118, 232,
|
||||
25, 145, 150, 212,
|
||||
84, 148, 28, 69, 209,
|
||||
179, 163, 35]
|
||||
, hrp => "bc"
|
||||
, address => "bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs"
|
||||
}
|
||||
,#{ version => 0
|
||||
, value => [0, 0, 0, 196, 165, 202, 212, 98, 33, 178, 161, 135,
|
||||
144, 94, 82, 102, 54, 43, 153, 213, 233, 28, 108,
|
||||
226, 77, 22, 93, 171, 147, 232, 100, 51]
|
||||
, hrp => "tb"
|
||||
, address => "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"
|
||||
}
|
||||
,#{ version => 1
|
||||
, value => [0, 0, 0, 196, 165, 202, 212, 98, 33, 178, 161, 135,
|
||||
144, 94, 82, 102, 54, 43, 153, 213, 233, 28, 108,
|
||||
226, 77, 22, 93, 171, 147, 232, 100, 51]
|
||||
, hrp => "tb"
|
||||
, address => "tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c"
|
||||
}
|
||||
,#{ version => 1
|
||||
, value => [121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98,
|
||||
149, 206, 135, 11, 7, 2, 155, 252, 219, 45, 206, 40,
|
||||
217, 89, 242, 129, 91, 22, 248, 23, 152]
|
||||
, hrp => "bc"
|
||||
, address => "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0"
|
||||
}
|
||||
].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @hidden
|
||||
%% @doc
|
||||
%% source: https://github.com/sipa/bech32/blob/master/ref/python/tests.py#L104
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
invalid_data_fixture() ->
|
||||
[% Invalid HRP
|
||||
#{ error => [{reason, "Invalid hrp"}]
|
||||
, hrp => "bc"
|
||||
, address => "tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut"
|
||||
, ignore => true
|
||||
}
|
||||
|
||||
% Invalid checksum algorithm (bech32 instead of bech32m)
|
||||
,#{ error => [{reason, "Invalid version and format"}]
|
||||
, hrp => "bc"
|
||||
, address => "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd"
|
||||
}
|
||||
|
||||
% Invalid checksum algorithm (bech32 instead of bech32m)
|
||||
,#{ error => [{reason, "Invalid version and format"}]
|
||||
, hrp => "tb"
|
||||
, address => "tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf"
|
||||
}
|
||||
|
||||
% Invalid checksum algorithm (bech32 instead of bech32m)
|
||||
,#{ error => [{reason, "Invalid version and format"}]
|
||||
, hrp => "bc"
|
||||
, address => "BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL"
|
||||
}
|
||||
|
||||
% Invalid checksum algorithm (bech32m instead of bech32)
|
||||
,#{ error => [{reason, "Invalid version and format"}]
|
||||
, hrp => "bc"
|
||||
, address => "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh"
|
||||
}
|
||||
|
||||
% Invalid checksum algorithm (bech32m instead of bech32)
|
||||
,#{ error => [{reason, "Invalid version and format"}]
|
||||
, hrp => "tb"
|
||||
, address => "tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47"
|
||||
}
|
||||
|
||||
% Invalid character in checksum
|
||||
,#{ error => [{reason, "Invalid data character"}
|
||||
,{position, 60}]
|
||||
, hrp => "bc"
|
||||
, address => "bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4"
|
||||
}
|
||||
|
||||
% Invalid witness version
|
||||
,#{ error => [{reason, "Invalid version"}]
|
||||
, hrp => "bc"
|
||||
, address => "BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R"
|
||||
}
|
||||
|
||||
% Invalid program length (1 byte)
|
||||
,#{ error => [{reason, "Invalid program length for witness version 0 (per BIP141)"}]
|
||||
, hrp => "bc"
|
||||
, address => "bc1pw5dgrnzv"
|
||||
}
|
||||
|
||||
% Invalid program length (41 bytes)
|
||||
,#{ error => [{reason, "Invalid program length for witness version 0 (per BIP141)"}]
|
||||
, hrp => "bc"
|
||||
, address => "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav"
|
||||
}
|
||||
|
||||
% Invalid program length for witness version 0 (per BIP141)
|
||||
,#{ error => [{reason, "Invalid data length"}]
|
||||
, hrp => "bc"
|
||||
, address => "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P"
|
||||
}
|
||||
|
||||
% Mixed case
|
||||
,#{ error => [{reason, "wrong case"}
|
||||
,{data, "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq"}]
|
||||
, hrp => "tb"
|
||||
, address => "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq"
|
||||
}
|
||||
|
||||
% TOFIX: More than 4 padding bits
|
||||
,#{ error => [{reason, ""}]
|
||||
, hrp => "bc"
|
||||
, address => "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf"
|
||||
}
|
||||
|
||||
% TOFIX: Non-zero padding in 8-to-5 conversion
|
||||
,#{ error => [{reason, ""}]
|
||||
, hrp => "tb"
|
||||
, address => "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j"
|
||||
}
|
||||
|
||||
% Empty data section
|
||||
,#{ error => [{reason, "Empty data section"}]
|
||||
, hrp => "bc"
|
||||
, address => "bc1gmk9yu"
|
||||
}
|
||||
].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @hidden
|
||||
%% @doc
|
||||
%% source: https://github.com/sipa/bech32/blob/master/ref/python/tests.py#L137
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
%% TOFIX: check if the values are correct
|
||||
invalid_input_fixture() ->
|
||||
[#{ hrp => "BC"
|
||||
, version => 0
|
||||
, length => 19
|
||||
, error => [{reason, ""}]
|
||||
}
|
||||
,#{ hrp => "bc"
|
||||
, version => 0
|
||||
, length => 22
|
||||
, error => [{reason, ""}]
|
||||
}
|
||||
,#{ hrp => "bc"
|
||||
, version => 17
|
||||
, length => 31
|
||||
, error => [{reason, ""}]
|
||||
}
|
||||
,#{ hrp => "bc"
|
||||
, version => 1
|
||||
, length => 1
|
||||
, error => [{reason, ""}]
|
||||
}
|
||||
,#{ hrp => "bc"
|
||||
, version => 16
|
||||
, length => 41
|
||||
, error => [{reason, ""}]
|
||||
}
|
||||
].
|
||||
Reference in New Issue
Block a user