diff --git a/README.md b/README.md index b8528aa..ce2d5e9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/include/bech32.hrl b/include/bech32.hrl new file mode 100644 index 0000000..d05557b --- /dev/null +++ b/include/bech32.hrl @@ -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. diff --git a/notes/0009-implementing-bech32-and-segwit-address-in-pure-erlang/README.md b/notes/0009-implementing-bech32-and-segwit-address-in-pure-erlang/README.md new file mode 100644 index 0000000..9ffa3a7 --- /dev/null +++ b/notes/0009-implementing-bech32-and-segwit-address-in-pure-erlang/README.md @@ -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 + +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) diff --git a/notes/README.md b/notes/README.md index 8bcff32..467cb1a 100644 --- a/notes/README.md +++ b/notes/README.md @@ -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 diff --git a/src/bech32.erl b/src/bech32.erl new file mode 100644 index 0000000..27f1144 --- /dev/null +++ b/src/bech32.erl @@ -0,0 +1,1316 @@ +%%%=================================================================== +%%% 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 +%%% +%%% `bech32' module implements bech32 format from bitcoin in pure +%%% Erlang. One of the goal of this module is to reimplement python `segwit_addr' +%%% module in Erlang. +%%% +%%% == Encoding Usage == +%%% +%%% 4 functions are available to encode data. `encode_bech32/2' and +%%% `encode_bech32m/2' automatically encode data in the right format +%%% using indexed data. In case of issue, these functions raise an +%%% exception. `encode/2' and `encode/3' can accept many options and +%%% will use ok/error patterns. Here some example: +%%% +%%% ``` +%%% % Define an HRP +%%% HRP = "test". +%%% +%%% % Define an indexed string using base32. This data can be +%%% % generated using convertbits function. +%%% IndexedData = [0,1,2,3,4]. +%%% +%%% % Define an unindexed string (a raw one) +%%% UnindexedData = [$n,$o,$s,$t,$r]. +%%% UnindexedData = "nostr". +%%% +%%% % bech32 encoding using bech32 format with indexed string. +%%% "test1qpzryyr0hjg" +%%% = bech32:encode_bech32(HRP, IndexedData). +%%% {ok, "test1qpzryyr0hjg"} +%%% = bech32:encode(HRP, IndexedData, [{format, bech32}]}). +%%% +%%% % bech32 encoding using bech32m format with indexed string. +%%% "test1qpzry3llmh2" = bech32:encode_bech32m(HRP, IndexedData). +%%% {ok, "test1qqqsyqcyzsv7qk"} +%%% = bech32:encode(HRP, IndexedData, [{format, bech32m}]}). +%%% +%%% % bech32 encoding using unindexed data +%%% {ok, "test1dehhxarjyzxdzp"} +%%% = bech32:encode(HRP, UnindexedData, [{format, bech32}, {indexed, false}]). +%%% +%%% % bech32 encoding using binary or bitstring as input +%%% "test1qqqsyqcyzsv7qk" +%%% = bech32:encode(<<"test">>, <<0,1,2,3,4>>, [{format, bech32m}]). +%%% {ok,"test1qpzry3llmh2"} +%%% = bech32:encode(<<"test">>, <<0,1,2,3,4>>, [{format, bech32m}]). +%%% +%%% % bech32 encoding with binary output +%%% {ok, "test1qpzry3llmh2"} +%%% = bech32:encode(HRP, IndexedData, [{format, bech32m}, {as_binary, true}]). +%%% ''' +%%% +%%% == Decoding Usage == +%%% +%%% Only one function is available to decode bech32 string: +%%% `decode/1'. Here few example. +%%% +%%% ``` +%%% % decode a string as list() +%%% {ok, #{ checksum => [26,30,20,18,15,4] +%%% , data => [31,28] +%%% , format => bech32m +%%% , hrp => "test" +%%% , origin => "test1lu675j0y" +%%% } +%%% } = bech32:decode("test1lu675j0y"). +%%% +%%% % decode a string as binary() +%%% {ok, #{ checksum => [26,30,20,18,15,4] +%%% , data => [31,28] +%%% , format => bech32m +%%% , hrp => "test" +%%% , origin => "test1lu675j0y" +%%% } +%%% } = bech32:decode(<<"test1lu675j0y">>) +%%% ''' +%%% +%%% == Convert bits == +%%% +%%% Functions to convert bits from different base called +%%% `convertbits/3' and `convertbits/4' are also provided. +%%% +%%% ``` +%%% {ok,[14,17,18,23,6,29,0]} +%%% = bech32:convertbits("test", 8, 5). +%%% +%%% {ok, [116,101,115,116,0]} +%%% = bech32:convertbits([14,17,18,23,6,29,0], 5, 8). +%%% +%%% {ok,[14,17,18,23,6,29]} +%%% = bech32:convertbits("test", 8, 5, [{padding, false}]). +%%% +%%% {ok,"test"} +%%% = bech32:convertbits([14,17,18,23,6,29], 5, 8, [{padding, true}]). +%%% ''' +%%% +%%% @todo add debug mode +%%% @todo creates errors (and specifies them) +%%% @todo creates types and specification. +%%% @end +%%%=================================================================== +-module(bech32). +-export([encode/2, encode/3]). +-export([encode_bech32/2, encode_bech32m/2]). +-export([decode/1, decode/2]). +-export([create_checksum/3, verify_checksum/2]). +-export([convertbits/3, convertbits/4]). +-include_lib("eunit/include/eunit.hrl"). +-include("bech32.hrl"). + +% charset() type defines all bech32 charset allowed values. +-type charset() :: $0 | $2 | $3 | $4 | $5 | $6 | $7 | $8 | $9 | $a + | $c | $d | $e | $f | $g | $h | $j | $k | $l | $m + | $n | $p | $q | $r | $s | $t | $u | $v | $w | $x + | $y | $z. + +% charset_index() type define the index from 0 to 31 (32 values). +-type charset_index() :: 0 | 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 | 27 | 28 + | 29 | 30 | 31. + +% local record +-record(bech32, { hrp = undefined :: undefined | [integer()] + , data = undefined :: undefined | [integer()] + , checksum = undefined :: undefined | [integer()] + , format = undefined :: format() + , origin = undefined :: undefined | [integer()] + }). + +% define the bech32m constant used for the checksum/format. +-define(BECH32M_CONST, 16#2BC8_30A3). + +%%-------------------------------------------------------------------- +%% This macro is used to generate the different steps used in polymod +%% function and simulate a kind of fixed size loop between each steps. +%%-------------------------------------------------------------------- +-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 + ). + +% required by eunit +-spec test() -> any(). + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. Convert an index into a chart present in charset. +%% +%% ``` +%% % generate with: +%% Index = lists:seq(0,31), +%% Charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l", +%% [ io:format("charset(~p) -> $~c;~n", [I, L]) +%% || {I, L} <- lists:zip(Index, Charset) +%% ]. +%% ''' +%% +%% @end +%%-------------------------------------------------------------------- +-spec index_to_charset(Index) -> Char when + Index :: charset_index(), + Char :: charset(). + +index_to_charset(0) -> $q; +index_to_charset(1) -> $p; +index_to_charset(2) -> $z; +index_to_charset(3) -> $r; +index_to_charset(4) -> $y; +index_to_charset(5) -> $9; +index_to_charset(6) -> $x; +index_to_charset(7) -> $8; +index_to_charset(8) -> $g; +index_to_charset(9) -> $f; +index_to_charset(10) -> $2; +index_to_charset(11) -> $t; +index_to_charset(12) -> $v; +index_to_charset(13) -> $d; +index_to_charset(14) -> $w; +index_to_charset(15) -> $0; +index_to_charset(16) -> $s; +index_to_charset(17) -> $3; +index_to_charset(18) -> $j; +index_to_charset(19) -> $n; +index_to_charset(20) -> $5; +index_to_charset(21) -> $4; +index_to_charset(22) -> $k; +index_to_charset(23) -> $h; +index_to_charset(24) -> $c; +index_to_charset(25) -> $e; +index_to_charset(26) -> $6; +index_to_charset(27) -> $m; +index_to_charset(28) -> $u; +index_to_charset(29) -> $a; +index_to_charset(30) -> $7; +index_to_charset(31) -> $l. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. convert charset value to index. +%% @end +%%-------------------------------------------------------------------- +-spec charset_to_index(Char) -> Index when + Char :: charset(), + Index :: charset_index(). + +charset_to_index($q) -> 0; +charset_to_index($p) -> 1; +charset_to_index($z) -> 2; +charset_to_index($r) -> 3; +charset_to_index($y) -> 4; +charset_to_index($9) -> 5; +charset_to_index($x) -> 6; +charset_to_index($8) -> 7; +charset_to_index($g) -> 8; +charset_to_index($f) -> 9; +charset_to_index($2) -> 10; +charset_to_index($t) -> 11; +charset_to_index($v) -> 12; +charset_to_index($d) -> 13; +charset_to_index($w) -> 14; +charset_to_index($0) -> 15; +charset_to_index($s) -> 16; +charset_to_index($3) -> 17; +charset_to_index($j) -> 18; +charset_to_index($n) -> 19; +charset_to_index($5) -> 20; +charset_to_index($4) -> 21; +charset_to_index($k) -> 22; +charset_to_index($h) -> 23; +charset_to_index($c) -> 24; +charset_to_index($e) -> 25; +charset_to_index($6) -> 26; +charset_to_index($m) -> 27; +charset_to_index($u) -> 28; +charset_to_index($a) -> 29; +charset_to_index($7) -> 30; +charset_to_index($l) -> 31. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. check if a character is present in charset or not. +%% @end +%%-------------------------------------------------------------------- +-spec is_charset(Char) -> Return when + Char :: charset(), + Return :: boolean(). + +is_charset($0) -> true; +is_charset($2) -> true; +is_charset($3) -> true; +is_charset($4) -> true; +is_charset($5) -> true; +is_charset($6) -> true; +is_charset($7) -> true; +is_charset($8) -> true; +is_charset($9) -> true; +is_charset($a) -> true; +is_charset($c) -> true; +is_charset($d) -> true; +is_charset($e) -> true; +is_charset($f) -> true; +is_charset($g) -> true; +is_charset($h) -> true; +is_charset($j) -> true; +is_charset($k) -> true; +is_charset($l) -> true; +is_charset($m) -> true; +is_charset($n) -> true; +is_charset($p) -> true; +is_charset($q) -> true; +is_charset($r) -> true; +is_charset($s) -> true; +is_charset($t) -> true; +is_charset($u) -> true; +is_charset($v) -> true; +is_charset($w) -> true; +is_charset($x) -> true; +is_charset($y) -> true; +is_charset($z) -> true; +is_charset(_) -> false. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. check if a string is indexed or not. +%% @end +%%-------------------------------------------------------------------- +is_index(C) when C >= 0 andalso C < 32 -> true; +is_index(_) -> false. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. check if its' a valid bech32 string. +%% @end +%%-------------------------------------------------------------------- +valid_indexed_string(List) + when is_list(List) -> + valid_indexed_list(List, [], 0). + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc +%% @end +%%-------------------------------------------------------------------- +valid_indexed_list([], Buffer, _Position) -> + {ok, lists:reverse(Buffer)}; +valid_indexed_list([H|T], Buffer, Position) -> + case is_index(H) of + true -> valid_indexed_list(T, [H|Buffer], Position+1); + false -> {error, [{reason, "Invalid char"} + ,{char, [H]} + ,{position, Position} + ,{head, Buffer} + ,{rest, T}]} + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. check bech32 data strings. +%% @end +%%-------------------------------------------------------------------- +-spec valid_string(String) -> Return when + String :: [integer()] | iodata(), + Return :: {ok, [integer()] | iodata()} | {error, Reason}, + Reason :: proplists:proplist(). + +valid_string(List) + when is_list(List) -> + valid_charset_list(List, [], 0); +valid_string(Binary) + when is_binary(Binary) -> + String = binary_to_list(Binary), + valid_string(String); +valid_string(_) -> + {error, [{reason, "Unsupported data type"}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% @end +%%-------------------------------------------------------------------- +valid_charset_list([], Buffer, _Position) -> + {ok, lists:reverse(Buffer)}; +valid_charset_list([H|T], Buffer, Position) -> + case is_charset(H) of + true -> valid_charset_list(T, [H|Buffer], Position+1); + false -> {error, [{reason, "Invalid char"} + ,{char, [H]} + ,{position, Position} + ,{head, Buffer} + ,{rest, T}]} + end. + +% @hidden +-spec valid_string_test() -> any(). +valid_string_test() -> + [?assertEqual({ok, "a"} + , valid_string("a")) + ,?assertEqual({error, [{reason,"Invalid char"},{char,"b"},{position,1},{head,"a"},{rest,[]}]} + , valid_string("ab")) + ,?assertEqual({ok, "a"} + , valid_string(<<"a">>)) + ,?assertEqual({error, [{reason,"Invalid char"},{char,"b"},{position,1},{head,"a"},{rest,[]}]} + , valid_string(<<"ab">>)) + ,?assertEqual({error, [{reason, "Unsupported data type"}]} + , valid_string(test)) + ]. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. generate bech32 polymod. +%% @end +%%-------------------------------------------------------------------- +-spec polymod(Values) -> Return when + Values :: string() | binary(), + Return :: pos_integer(). + +polymod(String) + when is_binary(String) -> + List = erlang:binary_to_list(String), + polymod_loop(List, 1); +polymod(String) + when is_list(String) -> + polymod_loop(String, 1). + +% @hidden +-spec polymod_test() -> any(). +polymod_test() -> + [?assertEqual(1, polymod(<<>>)) + ,?assertEqual(32, polymod(<<0>>)) + ,?assertEqual(33, polymod(<<1>>)) + ,?assertEqual(7103263, polymod(<<16#FFFF_FFFF:32>>)) + ,?assertEqual(71120775, polymod(<< <> || X <- lists:seq(0,9) >>)) + ,?assertEqual(1, polymod([])) + ,?assertEqual(32, polymod([0])) + ,?assertEqual(33, polymod([1])) + ,?assertEqual(4294967263, polymod([16#FFFF_FFFF])) + ,?assertEqual(610366851, polymod([ X || X <- lists:seq(0,9) ])) + ]. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. main polymod loop. +%% @end +%%-------------------------------------------------------------------- +-spec polymod_loop(String, Checksum) -> Return when + String :: [pos_integer()], + Checksum :: pos_integer(), + Return :: pos_integer(). + +polymod_loop([], Checksum) -> Checksum; +polymod_loop([Head|Tail], Checksum) -> + Top = Checksum bsr 25, + ChecksumStep1 = Checksum band 16#01FF_FFFF, + ChecksumStep2 = ChecksumStep1 bsl 5, + ChecksumStep3 = ChecksumStep2 bxor Head, + ChecksumFinal = polymod_generator(0, ChecksumStep3, Top), + polymod_loop(Tail, ChecksumFinal). + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. generate bech32 polymod using POLYMOD_GENERATOR macro. +%% @end +%%-------------------------------------------------------------------- +-spec polymod_generator(Index, Checksum, Top) -> Return when + Index :: 0 | 1 | 2 | 3 | 4 | 5, + Checksum :: pos_integer(), + Top :: pos_integer(), + Return :: pos_integer(). + +?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. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. generate hrp expansion. +%% @end +%%-------------------------------------------------------------------- +-spec hrp_expand(HRP) -> Return when + HRP :: hrp(), + Return :: hrp(). + +hrp_expand(HRP) + when is_list(HRP) -> + Head = [ (X bsr 5) || X <- HRP ], + Tail = [ (X band 31) || X <- HRP ], + Head ++ [0] ++ Tail. + +% @hidden +-spec hrp_expand_test() -> any(). +hrp_expand_test() -> + [?assertEqual([0], hrp_expand([])) + ,?assertEqual([3,3,3,3,0,20,5,19,20], hrp_expand("test")) + ]. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% verify bech32 checksum from data. +%% @end +%%-------------------------------------------------------------------- +-spec verify_checksum(HRP, Data) -> Return when + HRP :: hrp(), + Data :: data(), + Return :: format(). + +verify_checksum(HRP, Data) -> + Expand = hrp_expand(HRP), + Const = polymod(Expand ++ Data), + case Const of + 1 -> bech32; + ?BECH32M_CONST -> bech32m; + _ -> undefined + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% create a new bech32 checksum. +%% @end +%%-------------------------------------------------------------------- +-spec create_checksum(HRP, Data, Opts) -> Return when + HRP :: hrp(), + Data :: data(), + Opts :: [{format, format()}], + Return :: [integer()]. + +create_checksum(HRP, Data, Opts) + when is_list(HRP) andalso is_list(Data) -> + Spec = proplists:get_value(format, Opts), + Expand = hrp_expand(HRP), + Values = Expand ++ Data, + Polymod = polymod(Values ++ [0,0,0,0,0,0]), + case Spec of + bech32m -> create_checksum_final(Polymod, ?BECH32M_CONST); + bech32 -> create_checksum_final(Polymod, 1); + _ -> throw({error, [{reason, "unsupported format"}]}) + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% @end +%%-------------------------------------------------------------------- +create_checksum_final(Polymod, Const) -> + Polymod2 = Polymod bxor Const, + Ret = fun(P, I) -> (P bsr 5 * (5 - I)) band 31 end, + [ Ret(Polymod2, Index) || Index <- lists:seq(0,5) ]. + +% @hidden +-spec create_checksum_test() -> any(). +create_checksum_test() -> + [?assertEqual([2,13,27,24,0,28] + ,create_checksum("a", [2], [{format, bech32}])) + ,?assertEqual([23,17,11,20,5,30] + ,create_checksum("a", [2], [{format, bech32m}])) + ,?assertException(throw, _ + ,create_checksum("a", [2], [{format, bech32z}])) + ]. + +%%-------------------------------------------------------------------- +%% @doc `encode_bech32/2' function encodes indexed data with padding +%% using bech32 format. +%% @see encode/3 +%% @end +%%-------------------------------------------------------------------- +-spec encode_bech32(HRP, Data) -> Return when + HRP :: [integer()], + Data :: [integer()], + Return :: {ok, [pos_integer()]}. + +encode_bech32(HRP, Data) -> + Opts = [{format, bech32}, {indexed, true}], + case encode(HRP, Data, Opts) of + {ok, Result} -> Result; + Elsewise -> throw(Elsewise) + end. + +%%-------------------------------------------------------------------- +%% @doc `encode_bech32m/2' function encodes indexed data with padding +%% using to bech32m format. +%% @see encode/3 +%% @end +%%-------------------------------------------------------------------- +-spec encode_bech32m(HRP, Data) -> Return when + HRP :: [integer()], + Data :: [integer()], + Return :: {ok, [pos_integer()]}. + +encode_bech32m(HRP, Data) -> + Opts = [{format, bech32m},{indexed, true}], + case encode(HRP, Data, Opts) of + {ok, Result} -> Result; + Elsewise -> throw(Elsewise) + end. + +%%-------------------------------------------------------------------- +%% @doc `encode/2' function encodes indexed data with padding using +%% bech32 format. +%% @see encode_bech32/2 +%% @see encode/3 +%% @end +%%-------------------------------------------------------------------- +-spec encode(HRP, Data) -> Return when + HRP :: [integer()], + Data :: [integer()], + Return :: {ok, [pos_integer()]}. + +encode(HRP, Data) -> + encode(HRP, Data, [{format, bech32}]). + +%%-------------------------------------------------------------------- +%% @doc `encode/3' function is used to encode indexed or unindexed +%% data in bech32 or bech32m format. It supports `list()' or +%% `binary()' types in input and few options. +%% @end +%%-------------------------------------------------------------------- +-spec encode(HRP, Data, Opts) -> Return when + HRP :: [integer()], + Data :: [integer()], + Opts :: [Option, ...], + Option :: {format, format()} + | {binary, boolean()} + | {padding, boolean()}, + Return :: {ok, [pos_integer()]}. + +encode(HRP, Data, Opts) + when is_binary(HRP) -> + encode(binary_to_list(HRP), Data, Opts); +encode(HRP, Data, Opts) + when is_binary(Data) -> + encode(HRP, binary_to_list(Data), Opts); +encode(HRP, Data, Opts) -> + encode_check_format(HRP, Data, Opts). + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% @end +%%-------------------------------------------------------------------- +encode_check_format(HRP, Data, Opts) -> + case proplists:get_value(format, Opts) of + bech32 -> encode_check_data(HRP, Data, Opts); + bech32m -> encode_check_data(HRP, Data, Opts); + _ -> {error, [{reason, "Unsupported format"}]} + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% @end +%%-------------------------------------------------------------------- +encode_check_data(HRP, Data, Opts) -> + Indexed = proplists:get_value(indexed, Opts, true), + case valid_indexed_string(Data) of + {ok, _} -> + Checksum = create_checksum(HRP, Data, Opts), + encode_final(HRP, Data, Checksum, Opts); + {error, _} when Indexed =:= false -> + {ok, Converted} = convertbits(Data, 8, 5, Opts), + Checksum = create_checksum(HRP, Converted, Opts), + encode_final(HRP, Converted, Checksum, Opts); + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% @end +%%-------------------------------------------------------------------- +encode_final(HRP, Data, Checksum, Opts) -> + Binary = proplists:get_value(binary, Opts, false), + Combined = Data ++ Checksum, + Result = HRP ++ "1" ++ [ index_to_charset(C) || C <- Combined ], + Final = case Binary of + true -> list_to_binary(Result); + false -> Result + end, + {ok, Final}. + +% @hidden +-spec encode_test() -> any(). +encode_test() -> + [% encode/3 + ?assertEqual({ok, "a12uel5l"} + ,encode("a", [], [{format, bech32}])) + ,?assertEqual({ok, "a12uel5l"} + ,encode(<<"a">>, [], [{format, bech32}])) + ,?assertEqual({ok, "a12uel5l"} + ,encode(<<"a">>, <<>>, [{format, bech32}])) + ,?assertEqual({error, [{reason, "Unsupported format"}]} + ,encode(<<"a">>, <<>>, [{format, bech32z}])) + ,?assertEqual({ok, "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs"} + ,encode("an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio", [], [{format, bech32}])) + ,?assertEqual({ok,"test1lu0zy72x"} + ,encode("test", [255], [{indexed, false},{format, bech32}])) + ,?assertEqual({ok,"test1lu675j0y"} + ,encode("test", [255], [{indexed, false},{format, bech32m}])) + ,?assertEqual({ok,<<"test1lu675j0y">>} + ,encode("test", [255], [{indexed, false},{format, bech32m},{binary, true}])) + % encode/2 + ,?assertEqual({ok,"test1pzryfsa92x"} + , bech32:encode("test", [1,2,3,4])) + % encode_bech32/2 + ,?assertEqual("test1pzryfsa92x" + ,encode_bech32("test", [1,2,3,4])) + ,?assertException(throw, {error,[{reason,"Invalid char"},{char,[255]},{position,0},{head,[]},{rest,[]}]} + ,encode_bech32("test", [255])) + % encode_bech32m/2 + ,?assertEqual("test1pzryuvdf0y" + ,encode_bech32m("test", [1,2,3,4])) + ,?assertException(throw, {error,[{reason,"Invalid char"},{char,[255]},{position,0},{head,[]},{rest,[]}]} + ,encode_bech32m("test", [255])) + ]. + + +%%-------------------------------------------------------------------- +%% @doc`decode/1' function decodes bech32 encoded data. +%% @see decode/2 +%% @end +%%-------------------------------------------------------------------- +-spec decode(Bech) -> Return when + Bech :: list(), + Return :: {ok, map()} | {error, Reason}, + Reason :: term(). + +decode(Bech) -> + decode(Bech, []). + +%%-------------------------------------------------------------------- +%% @doc `decode/2' function decodes bech32 encoded data. +%% +%% == Examples == +%% +%% This is the default behavior, it will output the raw value from +%% base 32 (2^5): +%% +%% ``` +%% {ok, #{ checksum => [4,18,23,26,14,26] +%% , data => [7,15,24,12,12,15,30,11,18,13,3,3,8,1,29,15,18,30,18,30,11,27|...] +%% , format => bech32 +%% , hrp => "npub" +%% , origin => "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6" +%% } +%% } = decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"). +%% ''' +%% +%% One can change this behavior by using a custom base using the +%% converter option and `{base, Base}'where `Base' is a strictly +%% positive integer representing a base 2. +%% +%% ``` +%% Address = "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6". +%% {ok, #{ checksum => [4,18,23,26,14,26] +%% , data => [59,240,198,63,203,147,70,52,7,175,151,165,229,238,100,250|...] +%% , format => bech32 +%% , hrp => "npub" +%% , origin => "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6" +%% } +%% } = bech32:decode(Address, [{converter, {base, 8}}]). +%% ''' +%% +%% One can also create a lambda function to deal with the output and +%% converted the final data in another customer format. +%% +%% ``` +%% % create a new lambda function +%% Converter = fun(Data) -> +%% Binary = erlang:list_to_binary(Data), +%% Hex = binary:encode_hex(Binary), +%% {ok, Hex} +%% end. +%% +%% {ok, #{ checksum => [4,18,23,26,14,26] +%% , data => <<"3BF0C63FCB93463407AF97A5E5EE64FA883D107EF9E558472C4EB9AAAEFA459D00">> +%% , format => bech32 +%% , hrp => "npub" +%% , origin => "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6" +%% } +%% } = bech32:decode(Address, [{converter, Converter}]) +%% ''' +%% +%% @end +%%-------------------------------------------------------------------- +-spec decode(Bech, Opts) -> Return when + Bech :: list(), + Opts :: [Option, ...], + Option :: {converter, Converter}, + Converter :: function() + | {base, Base}, + Base :: pos_integer(), + Return :: {ok, map()} | {error, Reason}, + Reason :: term(). + +decode(Bech, Opts) + when is_binary(Bech) -> + decode(binary_to_list(Bech), Opts); +decode(Bech, _Opts) + when length(Bech) > 90 -> + {error, [{reason, "Overall max length exceeded"}]}; +decode(Bech, Opts) -> + State = #bech32{ origin = Bech }, + decode_check1(Bech, State, Opts). + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% @end +%%-------------------------------------------------------------------- +decode_check1(Bech, State, Opts) -> + case decode_check_characters(Bech) of + {ok, _} -> decode_check2(Bech, State, Opts); + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% @end +%%-------------------------------------------------------------------- +decode_check2(Bech, State, Opts) -> + Lower = string:lowercase(Bech), + Upper = string:uppercase(Bech), + case Lower =/= Bech andalso Upper =/= Bech of + false -> decode_split(Lower, State, Opts); + true -> {error, [{reason, "wrong case"},{data,Bech}]} + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% @end +%%-------------------------------------------------------------------- +decode_split(Bech, #bech32{} = State, Opts) -> + Reverse = lists:reverse(Bech), + decode_split(Bech, Reverse, [], 1, State, Opts). + +% @hidden +decode_split(_Bech, [], _Data, _Position, _State, _Opts) -> + {error, [{reason, "No separator character"}]}; +decode_split(_Bech, [$1], _Data, _Position, _State, _Opts) -> + {error, [{reason, "Empty HRP"}]}; +decode_split(_Bech, [$1|_HRP], Data, _Position, _State, _Opts) + when length(Data) < 6 -> + {error, [{reason, "Too short checksum"}]}; +decode_split(Bech, [$1|HRP], Data, _Position, State, Opts) -> + FinalHRP = lists:reverse(HRP), + NewState = State#bech32{ hrp = FinalHRP }, + decode_data(Bech, Data, NewState, Opts); +decode_split(Bech, [Head|Tail], Buffer, Position, State, Opts) -> + case is_charset(Head) of + true -> decode_split(Bech, Tail, [Head|Buffer], Position+1, State, Opts); + false -> + P = length(Bech)-Position+1, + {error, [{reason, "Invalid data character"} + ,{position, P}]} + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% @end +%%-------------------------------------------------------------------- +decode_data(Bech, RawData, State, Opts) -> + case decode_data2(RawData, [], 1) of + {ok, Data, Checksum} -> + NewState = State#bech32{ data = Data }, + decode_checksum(Bech, Checksum, NewState, Opts); + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% @end +%%-------------------------------------------------------------------- +decode_checksum(Bech, Checksum, #bech32{ data = Data, hrp = HRP } = State, Opts) -> + case decode_format(HRP, Data ++ Checksum) of + {ok, Format} -> + NewState = State#bech32{ checksum = Checksum, format = Format }, + decode_output(Bech, Checksum, NewState, Opts); + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. +%% @todo cleanup this function. It has been created to help to convert +%% data in another base than base32 using a power of 2 or custom +%% function. +%% @end +%%-------------------------------------------------------------------- +decode_output(_Bech, _Checksum, #bech32{ data = Data } = State, Opts) -> + BaseOutput = proplists:get_value(converter, Opts, {base, 5}), + case BaseOutput of + {base, 5} -> + {ok, to_map(State)}; + {base, B} when B > 0 -> + case convertbits(Data, 5, B) of + {ok, NewData} -> + {ok, to_map(State#bech32{ data = NewData })}; + Elsewise -> Elsewise + end; + {base, B} when is_number(B) -> + {error, [{reason, "Invalid base"},{base, B}]}; + B when is_function(B) -> + try B(Data) + of + {ok, NewData} -> {ok, to_map(State#bech32{ data = NewData })}; + Elsewise -> Elsewise + catch + _E:R -> {error, R} + end; + Elsewise -> Elsewise + end. + +% @hidden +-spec decode_test() -> any(). +decode_test() -> + [ + % bech32 valid + ?assertEqual({ok, #{ hrp => "a" + , data => [] + , checksum => [10,28,25,31,20,31] + , format => bech32 + , origin => "A12UEL5L" + } + } + ,decode(<<"A12UEL5L">>)) + ,?assertEqual({ok, #{ hrp => "a" + , data => [] + , checksum => [10,28,25,31,20,31] + , format => bech32 + , origin => "A12UEL5L" + } + } + ,decode("A12UEL5L")) + ,?assertEqual({ok, #{ hrp => "a" + , data => [] + , checksum => [10,28,25,31,20,31] + , format => bech32 + , origin => "a12uel5l" + } + } + ,decode("a12uel5l")) + ,?assertEqual({ok, #{ hrp => "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio" + , data => [] + , checksum => [11,11,20,11,8,16] + , format => bech32 + , origin => "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs" + } + } + ,decode("an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs")) + ,?assertEqual({ok, #{ hrp => "abcdef" + , data => [0, 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, 27, 28, 29, 30, + 31] + , checksum => [27,0,0,0,6,14] + , format => bech32 + , origin => "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw" + } + } + ,decode("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw")) + ,?assertEqual({ok, #{ hrp => "1" + , data => [ 0 || _ <- lists:seq(1,82) ] + , checksum => [24,7,10,21,30,18] + , format => bech32 + , origin => "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j" + } + } + ,decode("11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j")) + ,?assertEqual({ok, #{ hrp => "split" + , data => [24, 23, 25, 24, 22, 28, 1, 16, + 11, 29, 8, 25, 23, 29, 19, 13, + 16, 23, 29, 22, 25, 28, 1, 16, + 11, 3, 25, 29, 27, 25, 3, 3, + 29, 19, 11, 25, 3, 3, 25, 13, + 24, 29, 1, 25, 3, 3, 25, 13] + , checksum => [10,4,5,25,17,14] + , format => bech32 + , origin => "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w" + } + } + ,decode("split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w")) + ,?assertEqual({ok, #{ hrp => "?" + , data => [] + , checksum => [25,2,4,9,24,31] + , format => bech32 + , origin => "?1ezyfcl" + } + } + ,decode("?1ezyfcl")) + + % bech32m valid + ,?assertEqual({ok, #{ hrp => "a" + , data => [] + , checksum => [31,0,9,19,17,29] + , format => bech32m + , origin => "A1LQFN3A" + } + } + ,decode("A1LQFN3A")) + ,?assertEqual({ok, #{ hrp => "a" + , data => [] + , checksum => [31,0,9,19,17,29] + , format => bech32m + , origin => "a1lqfn3a" + } + } + ,decode("a1lqfn3a")) + ,?assertEqual({ok, #{ hrp => "an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber1" + , data => [] + , checksum => [16,8,30,23,8,26] + , format => bech32m + , origin => "an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6" + } + } + ,decode("an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6")) + ,?assertEqual({ok, #{ hrp => "abcdef" + , data => [31, 30, 29, 28, 27, 26, 25, + 24, 23, 22, 21, 20, 19, 18, + 17, 16, 15, 14, 13, 12, 11, + 10, 9, 8, 7, 6, 5, 4, 3, 2, + 1, 0] + , checksum => [2,13,17,3,4,6] + , format => bech32m + , origin => "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx" + } + } + ,decode("abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx")) + ,?assertEqual({ok, #{ hrp => "1" + , data => [ 31 || _ <- lists:seq(1,82) ] + , checksum => [31,28,13,16,3,7] + , format => bech32m + , origin => "11llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllludsr8" + } + } + ,decode("11llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllludsr8")) + ,?assertEqual({ok, #{ hrp => "split" + , data => [24, 23, 25, 24, 22, 28, 1, 16, + 11, 29, 8, 25, 23, 29, 19, 13, + 16, 23, 29, 22, 25, 28, 1, 16, + 11, 3, 25, 29, 27, 25, 3, 3, + 29, 19, 11, 25, 3, 3, 25, 13, + 24, 29, 1, 25, 3, 3, 25, 13] + , checksum => [31,24,21,21,20,12] + , format => bech32m + , origin => "split1checkupstagehandshakeupstreamerranterredcaperredlc445v" + } + } + ,decode("split1checkupstagehandshakeupstreamerranterredcaperredlc445v")) + ,?assertEqual({ok, #{ hrp => "?" + , data => [] + , checksum => [12,30,20,5,29,29] + , format => bech32m + , origin => "?1v759aa" + } + } + ,decode("?1v759aa")) + % bech32 invalid + ,?assertEqual({error, [{reason, "HRP character out of range"},{position, 1}]} + ,decode(" 1nwldj5")) + ,?assertEqual({error, [{reason, "HRP character out of range"},{position, 1}]} + ,decode([16#7F] ++ "1axkwrx")) + ,?assertEqual({error, [{reason, "HRP character out of range"},{position, 1}]} + ,decode([16#80] ++ "1eym55h")) + ,?assertEqual({error, [{reason, "Overall max length exceeded"}]} + ,decode("an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx")) + ,?assertEqual({error, [{reason, "No separator character"}]} + ,decode("pzry9x0s0muk")) + ,?assertEqual({error, [{reason, "Empty HRP"}]} + ,decode("1pzry9x0s0muk")) + ,?assertEqual({error, [{reason, "Invalid data character"},{position,3}]} + ,decode("x1b4n0q5v")) + ,?assertEqual({error, [{reason, "Too short checksum"}]} + ,decode("li1dgmt3")) + ,?assertEqual({error, [{reason,"HRP character out of range"},{position,9}]} + ,decode("de1lg7wt" ++ [16#FF])) + ,?assertEqual({error, [{reason, "Empty HRP"}]} + ,decode("10a06t8")) + ,?assertEqual({error, [{reason, "Empty HRP"}]} + ,decode("1qzzfhee")) + ,?assertEqual({error, [{reason, "Invalid checksum"}]} + ,decode("A1G7SGD8")) + + % bech32m invalid + ,?assertEqual({error, [{reason, "HRP character out of range"},{position, 1}]} + ,decode(" 1xj0phk")) + ,?assertEqual({error, [{reason, "HRP character out of range"},{position, 1}]} + ,decode([16#7F] ++ "1g6xzxy")) + ,?assertEqual({error, [{reason, "HRP character out of range"},{position, 1}]} + ,decode([16#80] ++ "1vctc34")) + ,?assertEqual({error, [{reason, "Overall max length exceeded"}]} + ,decode("an84characterslonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11d6pts4")) + ,?assertEqual({error, [{reason, "No separator character"}]} + ,decode("qyrz8wqd2c9m")) + ,?assertEqual({error, [{reason, "Empty HRP"}]} + ,decode("1qyrz8wqd2c9m")) + ,?assertEqual({error, [{reason, "Invalid data character"},{position,3}]} + ,decode("y1b0jsk6g")) + ,?assertEqual({error, [{reason, "Invalid data character"},{position,4}]} + ,decode("lt1igcx5c0")) + ,?assertEqual({error, [{reason, "Too short checksum"}]} + ,decode("in1muywd")) + ,?assertEqual({error, [{reason, "Invalid data character"},{position,9}]} + ,decode("mm1crxm3i")) + ,?assertEqual({error, [{reason, "Invalid data character"},{position,8}]} + ,decode("au1s5cgom")) + ,?assertEqual({error, [{reason, "Invalid checksum"}]} + ,decode("M1VUXWEZ")) + ,?assertEqual({error, [{reason, "Empty HRP"}]} + ,decode("16plkw9")) + ,?assertEqual({error, [{reason, "Empty HRP"}]} + ,decode("1p2gdwpf")) + ,?assertEqual({ok, #{ checksum => [4,18,23,26,14,26] + , data => [59,240,198,63,203,147,70,52,7,175,151,165,229,238,100,250,136,61,16,126,249, + 229,88,71,44,78,185,170,174,250,69,157,0] + , format => bech32 + , hrp => "npub" + , origin => "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6" + }} + , decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6", [{converter, {base, 8}}])) + ,?assertEqual({error,[{reason,"Invalid base"},{base,-1}]} + , decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6", [{converter, {base, -1}}])) + ,?assertEqual({ok, #{ checksum => [4,18,23,26,14,26] + , data => <<"3BF0C63FCB93463407AF97A5E5EE64FA883D107EF9E558472C4EB9AAAEFA459D00">> + , format => bech32 + , hrp => "npub" + , origin => "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6" + }} + , bech32:decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6" + ,[{converter, fun(Data) -> + {ok, Base8} = convertbits(Data, 5, 8), + Binary = erlang:list_to_binary(Base8), + Hex = binary:encode_hex(Binary), + {ok, Hex} + end}])) + ,?assertEqual({error, [{reason, "custom error"}]} + ,bech32:decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6" + ,[{converter, fun(_) -> {error, [{reason, "custom error"}]} end}])) + ,?assertEqual({error, badarith} + ,bech32:decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6" + ,[{converter, fun(X) -> a/X end}])) + ]. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. check if each characters are correct and valid. +%% @end +%%-------------------------------------------------------------------- +decode_check_characters(Bech) -> + decode_check_characters(Bech, [], 1). + +% @hidden +decode_check_characters([], Buffer, _) -> + {ok, lists:reverse(Buffer)}; +decode_check_characters([Head|_Tail], _Buffer, Position) + when Head < 33 orelse Head > 126 -> + {error, [{reason,"HRP character out of range"} + ,{position, Position}]}; +decode_check_characters([Head|Tail], Buffer, Position) -> + decode_check_characters(Tail, [Head|Buffer], Position+1). + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. decode the data and checksum part. +%% @end +%%-------------------------------------------------------------------- +decode_data2([], [C0,C1,C2,C3,C4,C5|RawData], _Position) -> + RawChecksum = [C0,C1,C2,C3,C4,C5], + Checksum = [ charset_to_index(Char) || Char <- lists:reverse(RawChecksum) ], + Data = [ charset_to_index(Char) || Char <- lists:reverse(RawData) ], + {ok, Data, Checksum}; +decode_data2([Head|Tail], Buffer, Position) -> + case is_charset(Head) of + true -> + decode_data2(Tail, [Head|Buffer], Position+1); + false -> + {error, [{reason, "Invalid charset"},{position, Position}]} + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. decode the format using the checksum. +%% @end +%%-------------------------------------------------------------------- +decode_format(HRP, Data) -> + case verify_checksum(HRP, Data) of + bech32 -> {ok, bech32}; + bech32m -> {ok, bech32m}; + _ -> {error, [{reason, "Invalid checksum"}]} + end. + +%%-------------------------------------------------------------------- +%% @doc `convertbits/3' function implements a power of 2 base +%% conversion with padding support. +%% @see convertbits/4 +%% @end +%%-------------------------------------------------------------------- +-spec convertbits(Data, From, To) -> Return when + Data :: list() | binary(), + From :: integer(), + To :: integer(), + Return :: {ok, list()} | {error, Reason}, + Reason :: proplists:proplist(). + +convertbits(Data, From, To) -> + convertbits(Data, From, To, []). + +% @hidden +-spec convertbits_test() -> any(). +convertbits_test() -> + [?assertEqual({ok, [0, 4, 1, 0, 6, 1, 0, 5]} + ,convertbits(<<1,2,3,4,5>>, 8, 5)) + ,?assertEqual({ok, [0, 4, 1, 0, 6, 1, 0, 5]} + ,convertbits([1,2,3,4,5], 8, 5)) + ,?assertEqual({ok, [31, 28]} + ,convertbits([255], 8, 5)) + ,?assertEqual({ok, [1, 1, 1, 1, 1, 1, 1, 1]} + ,convertbits([255], 8, 1)) + ,?assertEqual({ok, [255]} + ,convertbits([1, 1, 1, 1, 1, 1, 1, 1], 1, 8)) + ,?assertEqual({error, [{reason,"Value greater than source"},{position,1},{value,1024}]} + ,bech32:convertbits([1024], 8, 5)) + ,?assertEqual({error,[{reason,"Negative value"},{position,1},{value,-1}]} + ,bech32:convertbits([-1], 8, 5)) + ,?assertEqual({error,[{reason,"Remaining bits issue"},{position,2},{value,3}]} + ,bech32:convertbits([123], 8, 5, [{padding, false}])) + ,?assertEqual({ok,[15,12]} + ,convertbits([123], 8, 5, [{padding, true}])) + ]. + +%%-------------------------------------------------------------------- +%% @doc `convertbits/4' function implements a power of 2 base +%% conversion with or without padding support. +%% @end +%%-------------------------------------------------------------------- +-spec convertbits(Data, From, To, Opts) -> Return when + Data :: list() | binary(), + From :: integer(), + To :: integer(), + Opts :: [Option, ...], + Option :: {padding, boolean()}, + Return :: {ok, list()} | {error, Reason}, + Reason :: proplists:proplist(). + +convertbits(Data, From, To, Opts) + when is_binary(Data) -> + convertbits(binary_to_list(Data), From, To, Opts); +convertbits(Data, From, To, Opts) -> + Maxv = (1 bsl To) - 1, + Maxa = (1 bsl (From + To - 1)) -1, + State = #{ max_value => Maxv + , max_accumulator => Maxa + , accumulator => 0 + , bits => 0 + , ret => [] + , padding => proplists:get_value(padding, Opts, true) + , position => 1 + }, + convertbits1(Data, From, To, State). + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. main convertbits loop. +%% @end +%%-------------------------------------------------------------------- +convertbits1([], _From, To, #{ max_value := Maxv, accumulator := Acc, bits := Bits, ret := Ret, padding := true }) + when Bits > 0 -> + {ok, lists:reverse([(Acc bsl (To-Bits)) band Maxv|Ret])}; +convertbits1([], _From, _To, #{ ret := Ret, padding := true }) -> + {ok, lists:reverse(Ret)}; +convertbits1([], From, To, #{ bits := Bits, position := Position, accumulator := Acc, max_value := Maxv }) + when Bits >= From orelse ((Acc bsl (To-Bits)) band Maxv) > 0 -> + {error, [{reason, "Remaining bits issue"},{position, Position},{value, Bits}]}; +convertbits1([], _From, _To, #{ ret := Ret }) -> + {ok, lists:reverse(Ret)}; +convertbits1([Head|_], _From, _To, #{ position := Position }) + when Head < 0 -> + {error, [{reason, "Negative value"},{position, Position},{value, Head}]}; +convertbits1([Value|_], From, _To, #{ position := Position }) + when (Value bsr From) > 0 -> + {error, [{reason, "Value greater than source"},{position, Position},{value, Value}]}; +convertbits1([Value|_] = Data, From, To, #{ max_accumulator := Maxa, accumulator := Acc, bits := Bits } = State) -> + Acc2 = ((Acc bsl From) bor Value) band Maxa, + Bits2 = Bits + From, + NewState = State#{ accumulator => Acc2, bits => Bits2 }, + convertbits2(Data, From, To, NewState). + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. bits conversion. +%% @end +%%-------------------------------------------------------------------- +convertbits2(Data, From, To, #{ bits := Bits, ret := Ret, accumulator := Acc, max_value := Maxv } = State) + when Bits >= To -> + Bits2 = Bits - To, + Ret2 = [(Acc bsr Bits2) band Maxv|Ret], + NewState = State#{ bits => Bits2, ret => Ret2 }, + convertbits2(Data, From, To, NewState); +convertbits2([_|Tail], From, To, #{ position := Position } = State) -> + convertbits1(Tail, From, To, State#{ position => Position + 1}). + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal. used to convert bech32 record. +%% @end +%%-------------------------------------------------------------------- +-spec to_map(Record) -> Return when + Record :: #bech32{}, + Return :: map(). + +to_map(#bech32{ hrp = HRP, data = Data, checksum = Checksum, format = Format, origin = Origin}) -> + #{ hrp => HRP + , data => Data + , checksum => Checksum + , format => Format + , origin => Origin + }. diff --git a/src/segwit.erl b/src/segwit.erl new file mode 100644 index 0000000..85490be --- /dev/null +++ b/src/segwit.erl @@ -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 + }. diff --git a/test/segwit_SUITE.erl b/test/segwit_SUITE.erl new file mode 100644 index 0000000..f4ae320 --- /dev/null +++ b/test/segwit_SUITE.erl @@ -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, ""}] + } + ].