├── test ├── test_helper.exs └── b58_test.exs ├── NEWS.md ├── .gitignore ├── LICENSE.md ├── mix.lock ├── mix.exs ├── docs └── FAQ.md ├── README.md └── lib └── b58.ex /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # B58 0.1.0 2 | 3 | * Initial release -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | b58-*.tar 24 | 25 | #Ignore formatters 26 | .formatter.exs 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 Incosequential Software 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "0.13.2", "30cd4ff5f593fdd218a9b26f3c24d580274f297d88ad43383afe525b1543b165", [:mix], [{:deep_merge, "~> 0.1", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "deep_merge": {:hex, :deep_merge, "0.2.0", "c1050fa2edf4848b9f556fba1b75afc66608a4219659e3311d9c9427b5b680b3", [:mix], [], "hexpm"}, 4 | "dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, 6 | "erlex": {:hex, :erlex, "0.1.5", "fca8568751216266b343e182c9f494654e93c8adce3a0189c86ea45b632e8c55", [:mix], [], "hexpm"}, 7 | "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, 11 | } 12 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule B58.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.1.0" 5 | @source_url "https://github.com/nocursor/b58" 6 | 7 | def project do 8 | [ 9 | app: :basefiftyeight, 10 | version: @version, 11 | elixir: ">= 1.6.0", 12 | start_permanent: Mix.env() == :prod, 13 | deps: deps(), 14 | 15 | # Hex 16 | description: "Elixir library for encoding and decoding Base58 and Base58Check using the Bitcoin/IPFS, Ripple, and Flickr alphabets.", 17 | package: package(), 18 | 19 | # Docs 20 | name: "Base Fifty Eight", 21 | source_url: @source_url, 22 | docs: docs() 23 | ] 24 | end 25 | 26 | def application do 27 | [ 28 | extra_applications: [:logger] 29 | ] 30 | end 31 | 32 | defp package do 33 | [ 34 | maintainers: [ 35 | "nocursor", 36 | ], 37 | licenses: ["MIT"], 38 | links: %{github: @source_url}, 39 | files: ~w(lib NEWS.md LICENSE.md mix.exs README.md) 40 | ] 41 | end 42 | 43 | defp docs do 44 | [ 45 | source_ref: "v#{@version}", 46 | main: "b58", 47 | extra_section: "PAGES", 48 | extras: extras(), 49 | groups_for_extras: groups_for_extras() 50 | ] 51 | end 52 | 53 | defp extras do 54 | [ 55 | "docs/FAQ.md", 56 | ] 57 | end 58 | 59 | defp groups_for_extras do 60 | [ 61 | ] 62 | end 63 | 64 | # Run "mix help deps" to learn about dependencies. 65 | defp deps do 66 | [ 67 | {:benchee, "~> 0.13.2", only: [:dev]}, 68 | {:dialyxir, "~> 1.0.0-rc.3", only: [:dev], runtime: false}, 69 | {:ex_doc, "~> 0.13", only: [:dev], runtime: false}, 70 | ] 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | There are a lot of common issues I have noticed between Base58 libraries. I'm listing a few here, please read these to save yourself headaches and wasting time opening non-issues. 4 | 5 | ## Trimming 6 | 7 | The C++ implementation of Base58 found in Bitcoin trims an input string. I believe that you should control your input string. Moreover, forcing extra trimming logic into every encode or decode I think is a mistake when it adds a small, but still noticeable overhead when comparing languages like C++ or Rust to Elixir. 8 | 9 | If you find you're getting the incorrect results, maybe you forgot to trim the leading part of your string. 10 | 11 | ## Hex Strings 12 | 13 | As mentioned in these docs, this library expects a binary string. If your string has been previously hex encoded, you should decode it first use `Base.decode16!/1`. Alternatively, if you control your code, another option is to use `:binary.encode_unsigned/1` and write your hex string using hex notation, ex `0xff |> :binary.encode_unsigned()` 14 | 15 | ## Other Types of Strings 16 | 17 | Again, same thing follows. A string that was once "hello" is a binary in Elixir. If it was encoded using a method such as Base32, it will still be a binary, but it is no longer the same binary as "hello". You can easily prove this by iterating the bytes, or doing something as simple as "to_charlist()" and looking at the data. 18 | 19 | ## Results that Don't Match Examples 20 | 21 | Once again, encoding is primarily the issue. This library takes the stance of using a single representation for encoding and decoding. Many other libraries will show you an example and not explain the result is Base16, Base32, or some other encoding. If in doubt, try use the `Base` module to encode or decode accordingly between formats. 22 | 23 | ## Will You Support Dynamic/Arbitrary Alphabets? 24 | 25 | Only if you can make a compelling case with an accompanying design that would not break the existing one. I am not aware of any real use-cases for this. Dynamic alphabets are simply not fast enough for my use-cases in Elixir at runtime, which is why all supported alphabets have compile time character encoding support. Lookups on such alphabets are typically going to be slow. Moreover, adding such alphabets would require changes to the public interface of this library or some extra options wrangling and branching I want to avoid. 26 | 27 | I am however very open to adding new alphabets if there are others in wide-use. 28 | 29 | ## Are you interested in Bitcoin, IPFS, Crypto Currencies, or Blockchains 30 | 31 | No. 32 | 33 | If you want to pay me money to be interested, I am always interested in being payed to create things as long as they are ethical, legal, and moral. 34 | 35 | I created this library because I wish to use it for various purposes unrelated to the above that would benefit from Base58 encoding. 36 | 37 | ## What other usages are there for Base58/Base58Check encoding outside IPFS, Crypto Currencies, and Blockchain? 38 | 39 | * Clear encoding without ambiguous characters. 40 | * See the Base58 reasons in the original Bitcoin source. They are not unique to Bitcoin usage. 41 | * URL Shorteners 42 | * Gopher and other protocols with limits on URL-like constructs 43 | * Some older protocols and technologies have character limits 44 | * There is someone using Base58 to shorten Gopher URLs for a [proxy](https://gopher.floodgap.com/gopher/) 45 | * Base58Check is useful for cases when you want a version and checksum 46 | * This is good for eliminating some of the nasty edge-cases in other encodings 47 | * Projects such as [Multibase](https://github.com/multiformats/multibase) require Base58 48 | * Anything that wants to support lots of `BaseXX` encodings will inevitably need Base58 49 | 50 | ## Why Shouldn't I Include Some Characters in my custom Base58 alphabet? 51 | 52 | * Many of these characters pose problems for selecting text with the built-in facilities in an OS 53 | * Humans commonly mistake "0" for "O" and other issues 54 | * Putting a "0" creates ambiguity for the case of leading zeroes, breaking the leading "1" conversion of Base58 55 | * Other people have thought more about alphabets than you 56 | 57 | ## Drawbacks of Base58 and Base58Check? 58 | 59 | * Slower than many other methods due to the following 60 | * SHA-256 double hash for Base58Check 61 | * Version and checksum binary concatenation for Base58Check 62 | * Div and Mod operations for both 63 | * CRC + tables is an approach that can be used to reduce this overhead, but is not necessarily a benefit or easily done in certain languages without even more overhead 64 | * Can't handle zeroes in alphabet, but by design if you are obeying the original intentions. It's possible to work around this but requires edge handling for those alphabets. 65 | * Additional dependency on crypto functionality if using Base58Check 66 | * Other encodings may compress better or produce shorter strings if size is a concern for you 67 | 68 | 69 | ## How do I convert between Base58/Base58Check alphabets? 70 | 71 | If you want to convert between a Base58 string encoded with the Bitcoin alphabet to the Ripple alphabet for example, then you have a few options: 72 | 73 | * Decode the string using this library, then re-encode it. Simple, easy, always works, can go between formats. 74 | * Roll a conversion function yourself by hand. I don't recommend this as it's really not worth it. 75 | 76 | I do not really feel it is a good idea to add lots of functions in the format of `x_to_y` and `y_to_x` and `y_to_z`. It would be possible to pass in keywords instead with a single conversion function, but I think how to handle this is best left to the consumer and outside the scope of a library. 77 | 78 | ## Why Bitcoin, Ripple, and Flickr alphabets? 79 | 80 | These alphabets are very common and in real-world usage. 81 | 82 | ## Can you add the IPFS alphabet? 83 | 84 | There is not reason to do this as it uses the same alphabet as Bitcoin. I have seen libraries that add additional function or duplicate alphabets to make a distinction. This is unnecessary overhead and cognitive load. Moreover, if IPFS changes alphabets, it has bigger problems. 85 | 86 | ## Why did you name this library Base Fifty Eight? 87 | 88 | The other names were taken. Top-level namespacing doesn't seem to be enough of a "thing" in Elixir. Don't blame me. 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Base Fifty Eight 2 | 3 | Base Fifty Eight (B58) is an Elixir library for transparently encoding and decoding Base58 and Base58 checked binaries. 4 | 5 | The following alphabets are fully supported for standard and checked encoding and decoding: 6 | 7 | * BTC 8 | * Flickr 9 | * Ripple 10 | 11 | If by some unholy magic, an additional alphabet is required, please leave an issue and it can be easily added. 12 | 13 | ## Features 14 | 15 | * Base58 and Base58Check encode/decode for all supported alphabets 16 | * BTC, Flickr, Ripple 17 | * Support for Base58Check encoding and decoding both versioned and unversioned data 18 | * Version byte may be specified explicitly, extracted, or inferred 19 | * Checksum calculation per Base58Check spec using sha-256 double hash 20 | * Proper handling for encoding and decoding leading zeroes 21 | * Data is not lost or corrupted 22 | * No strange behaviors or surprises when adding new alphabets 23 | * Binary in, Binary out clean interface 24 | * Transparent encoding and decoding for all alphabets and types of encoding 25 | * You will always be able to reverse encoding with decoding, including zero bytes 26 | * Explicit handling for versioned and unversioned binaries when dealing with Base58Check 27 | * Encoding or Decode a binary with or without the version bytes included 28 | * Test coverage 29 | * Consistent functional interface that easily supports piping 30 | * Consistent design with Elixir core `Base` module 31 | * Glorious documentation 32 | 33 | Additionally, the following miscellaneous design-related features are present: 34 | 35 | * No external dependencies 36 | * Compile-time generated matching and optimizations for speed 37 | * Attention to binary optimization 38 | * Easy to add new alphabets with a single compile-time update 39 | * Designed for additional encoding/decoding options should feature requests or needs arise 40 | * Pragmatic attention to ease-of-use 41 | * Rather than having a combinatorial explosion for the number of alphabets, all alphabets share the same functional interface 42 | * Adding new alphabets does not place burden on programmers 43 | * Performance still maintained without resorting to bad decisions that impact encoding and decoding 44 | 45 | ## Usage 46 | 47 | Base58 Encode a Binary (default bitcoin/IPFS alphabet): 48 | 49 | ```elixir 50 | B58.encode58("Happy trees happily encode") 51 | "3DYrnueGqPGpJ2tCbbkATC71LBK1kRGm9g9z" 52 | 53 | # default for all alphabet options is always bitcoin, but we can specify it too 54 | B58.encode58("Happy trees happily encode", alphabet: :btc) 55 | "3DYrnueGqPGpJ2tCbbkATC71LBK1kRGm9g9z" 56 | ``` 57 | 58 | Base58 Encode a Binary using the Flickr alphabet: 59 | 60 | ```elixir 61 | B58.encode58("Happy trees happily encode", alphabet: :flickr) 62 | "3dxRMUDgQogPi2TcAAKasc71kbj1KqgL9F9Z" 63 | ``` 64 | 65 | Base58 Encode a Binary using the Ripple alphabet: 66 | 67 | ```elixir 68 | B58.encode58("Happy trees happily encode", alphabet: :ripple) 69 | "sDYi8ueGqPGFJptUbbkwTUfrLBKrkRGm9g9z" 70 | ``` 71 | 72 | Base58 Decode our original Base58 binary encoded with bitcoin: 73 | 74 | ```elixir 75 | B58.decode58!("3DYrnueGqPGpJ2tCbbkATC71LBK1kRGm9g9z") 76 | "Happy trees happily encode" 77 | ``` 78 | 79 | Base58 Decode our original Base58 binary encoded with bitcoin, but with error handling: 80 | 81 | ```elixir 82 | B58.decode58("3DYrnueGqPGpJ2tCbbkATC71LBK1kRGm9g9z") 83 | {:ok, "Happy trees happily encode"} 84 | 85 | # fails if the encoded string is invalid in some way 86 | # lets add a 0 at the end to break it since it is not part of the bitcoin alphabet 87 | B58.decode58("3DYrnueGqPGpJ2tCbbkATC71LBK1kRGm9g9z0", alphabet: :btc) 88 | {:error, "non-alphabet digit found: \"0\" (byte 48)"} 89 | ``` 90 | 91 | Base58Check encoding works in a similar way to Base58 for all functions, but requires a version byte: 92 | 93 | ```elixir 94 | B58.encode58_check!("Why is Jack always so Hungry", 0) 95 | "16tCsFCTp3ADeU8WsCt36HFYwg3kiBKtBXUBmU6KAQBsA" 96 | 97 | B58.encode58_check!("Why is Jack always so Hungry", 0, alphabet: :btc) 98 | "16tCsFCTp3ADeU8WsCt36HFYwg3kiBKtBXUBmU6KAQBsA" 99 | ``` 100 | 101 | Base58Check takes the same alphabets too: 102 | 103 | ```elixir 104 | B58.encode58_check!("Why is Jack always so Hungry", 0, alphabet: :ripple) 105 | "1atU1EUTFswDe73W1UtsaHEYAgsk5BKtBX7Bm7aKwQB1w" 106 | 107 | B58.encode58_check!("Why is Jack always so Hungry", 0, alphabet: :flickr) 108 | "16TcSfcsP3adDt8vScT36hfxWF3KHbjTbwtbLt6japbSa" 109 | ``` 110 | 111 | Base58Check can use version bytes a few ways: 112 | 113 | ```elixir 114 | B58.encode58_check!("Chip Clip sounds less cool in other languages", "m", alphabet: :btc) 115 | "2M5paPVxJK1G77Dm9GLXfyXmHsB8L32U2NDy12GfYxrg181WQzkx52nDnc9BMcuahPEJA" 116 | 117 | B58.encode58_check!("Chip Clip sounds less cool in other languages", ?m, alphabet: :btc) 118 | "2M5paPVxJK1G77Dm9GLXfyXmHsB8L32U2NDy12GfYxrg181WQzkx52nDnc9BMcuahPEJA" 119 | 120 | B58.encode58_check!("Chip Clip sounds less cool in other languages", 42, alphabet: :btc) 121 | "XCkzrRJxzqYE9tFeaWPsKeY4m1Fxm34CEe4repib7cLiGFjM7Dzf6ubruT68QZiUE5hC" 122 | 123 | # we can also try to pass some bad version info that violates the Base58Check version constraints 124 | B58.encode58_check!("Chip Clip sounds less cool in other languages", 42424242, alphabet: :btc) 125 | # raise ** (ArgumentError) version must be a single byte binary or unsigned integer, data must be a binary. 126 | 127 | # And we can use our trust error handling version too 128 | B58.encode58_check("Chip Clip sounds less cool in other languages", 42424242) 129 | {:error, 130 | "version must be a single byte binary or unsigned integer, data must be a binary."} 131 | ``` 132 | 133 | Base58Check can be encoded with a version byte already present. 134 | All calls with an explicit version byte are prefixed with `version` so there is no confusion and not error-prone: 135 | 136 | ```elixir 137 | # notice the "m" byte in front 138 | B58.version_encode58_check!("mChip Clip sounds less cool in other languages") 139 | "2M5paPVxJK1G77Dm9GLXfyXmHsB8L32U2NDy12GfYxrg181WQzkx52nDnc9BMcuahPEJA" 140 | 141 | # same but specifying the alphabet too explicitly 142 | B58.version_encode58_check!("mChip Clip sounds less cool in other languages", alphabet: :btc) 143 | "2M5paPVxJK1G77Dm9GLXfyXmHsB8L32U2NDy12GfYxrg181WQzkx52nDnc9BMcuahPEJA" 144 | 145 | # of course we can also construct a binary too ourselves 146 | B58.version_encode58_check!(<<42, "Chip Clip sounds less cool in other languages">>, alphabet: :btc) 147 | "XCkzrRJxzqYE9tFeaWPsKeY4m1Fxm34CEe4repib7cLiGFjM7Dzf6ubruT68QZiUE5hC" 148 | ``` 149 | 150 | Likewise, Base58Check can be decoded much like Base58. 151 | The big difference is how the version byte is returned. 152 | 153 | ```elixir 154 | B58.decode58_check!("2M5paPVxJK1G77Dm9GLXfyXmHsB8L32U2NDy12GfYxrg181WQzkx52nDnc9BMcuahPEJA") 155 | {"Chip Clip sounds less cool in other languages", "m"} 156 | 157 | B58.decode58_check!("2M5paPVxJK1G77Dm9GLXfyXmHsB8L32U2NDy12GfYxrg181WQzkx52nDnc9BMcuahPEJA", alphabet: :btc) 158 | {"Chip Clip sounds less cool in other languages", "m"} 159 | ``` 160 | 161 | The `version` prefixed calls can be used to include the version byte: 162 | 163 | ```elixir 164 | B58.version_decode58_check!("2M5paPVxJK1G77Dm9GLXfyXmHsB8L32U2NDy12GfYxrg181WQzkx52nDnc9BMcuahPEJA") 165 | "mChip Clip sounds less cool in other languages" 166 | 167 | # This is quite nice as we can use it for piping too, and as you can see it keeps things fully transparent 168 | # We can also pipe in the alphabet easily wherever 169 | B58.version_decode58_check!("2M5paPVxJK1G77Dm9GLXfyXmHsB8L32U2NDy12GfYxrg181WQzkx52nDnc9BMcuahPEJA") 170 | |> B58.version_encode58_check!() 171 | |> B58.version_decode58_check!(alphabet: :btc) 172 | "mChip Clip sounds less cool in other languages" 173 | ``` 174 | 175 | Base58Check includes a checksum. Let's tamper with our encoded bytes to make it fail: 176 | 177 | ```elixir 178 | B58.encode58_check!("Bank Details Poorly Handled by Bitcoin Startup", 42, alphabet: :btc) 179 | "3JJAcmj3WdhuKTNRAHx1Yjg4XorKHxPoAA8uokfDz9DGFw456jADxtLp6DEP5zTKsM3zq9" 180 | 181 | # Let's change that J in second position to a D because we are an evil hacker who hates J 182 | B58.decode58_check!("3DJAcmj3WdhuKTNRAHx1Yjg4XorKHxPoAA8uokfDz9DGFw456jADxtLp6DEP5zTKsM3zq9") 183 | # raises ** (ArgumentError) Invalid checksum. 184 | 185 | # Fortunately, we can also use our trusty error/reason tuples too 186 | # Notice the lack of a `!` 187 | B58.decode58_check("3DJAcmj3WdhuKTNRAHx1Yjg4XorKHxPoAA8uokfDz9DGFw456jADxtLp6DEP5zTKsM3zq9") 188 | {:error, "Invalid checksum."} 189 | 190 | # The `version` prefixed functions behave the same 191 | B58.version_decode58_check("3DJAcmj3WdhuKTNRAHx1Yjg4XorKHxPoAA8uokfDz9DGFw456jADxtLp6DEP5zTKsM3zq9") 192 | {:error, "Invalid checksum."} 193 | ``` 194 | 195 | If we are truly lazy or want some error checking, we can use this library to version our binaries too: 196 | 197 | ```elixir 198 | B58.version_binary("Bad Startup Idea", 255) 199 | <<255, 66, 97, 100, 32, 83, 116, 97, 114, 116, 117, 112, 32, 73, 100, 101, 97>> 200 | 201 | B58.version_binary("Bad Startup Idea", "X") 202 | "XBad Startup Idea" 203 | ``` 204 | 205 | Let's suppose we want to work with something more fancy like Bitcoin (I accept your wealth if you have any to spare). 206 | We can work our way through the example in the [Bitcoin wiki](https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses) like so: 207 | 208 | ```elixir 209 | # Suppose we have a RIPEMD-160 hashed public key like f54a5851e9372b87810a8e60cdd2e7cfd80b6e31 210 | # You can do the previous steps with the erlang `crypto` module if you wish, but it's out of scope here 211 | # Let's version it with zero for the BTC network, then encode it. 212 | # As sane humans, we do this if starting from such a string: 213 | Base.decode16!("f54a5851e9372b87810a8e60cdd2e7cfd80b6e31", case: :lower) |> B58.encode58_check!(0) 214 | "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs" 215 | 216 | # We could also do this if we had it as an integer like so 217 | # Notice the hex syntax for Elixir. Be careful doing this when working with leading zeroes though as `:binary.encode_unsigned/1` will use the smallest representation. 218 | 0xf54a5851e9372b87810a8e60cdd2e7cfd80b6e31 |> :binary.encode_unsigned() |> B58.encode58_check!(0) 219 | "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs" 220 | 221 | # DO NOT DO THIS. 222 | # I am quite positive someone will file an issue after doing this, thus I warn you here for later posterity. 223 | # Suppose we are unintelligent people. 224 | # This could never happen, but StackOverflow and other examples in the wild whisper to me 225 | # Notice how the result is completely wrong. Why? We're encoding a hex string, which is not a plain binary. 226 | B58.encode58_check!("f54a5851e9372b87810a8e60cdd2e7cfd80b6e31", 0) 227 | "1aES43oGwSz1ve4QREaEbvpnkqjWbohSsRPM6whcwAA8eQ8efC7oieTtucMiN" 228 | 229 | # DO NOT DO THIS 230 | # Likewise, someone will inevitably do this despite warnings 231 | # Notice the leading zero in the source data, and the resulting corrupted data due to the lack of a leading zero 232 | 0x0f54a5851e9372b87810a8e60cdd2e7cfd80b6e31 |> :binary.encode_unsigned() |> B58.version_encode58_check!() 233 | "PMycacnJaSqwwJqjawXBErnLsZ7N9KgRw" 234 | 235 | # Instead, do this 236 | versioned_binary = Base.decode16!("f54a5851e9372b87810a8e60cdd2e7cfd80b6e31", case: :lower) |> B58.version_binary(0) 237 | # Do a bunch of other stuff in between, for whatever twisted reason, finally doing this 238 | B58.version_encode58_check!(versioned_binary) 239 | "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs" 240 | 241 | # You could also do this, but you might as well use `encode58_check/3` and pass a zero for the version. 242 | Base.decode16!("f54a5851e9372b87810a8e60cdd2e7cfd80b6e31", case: :lower) |> B58.version_binary(0) |> B58.version_encode58_check!() 243 | "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs" 244 | ``` 245 | 246 | Finally, suppose you have decoded some data and are wondering why it doesn't look your friendly internet example. 247 | The problem is often format related, for example perhaps the final answer was in hex. We always keep things in plain Elixir binaries: 248 | 249 | ```elixir 250 | # Given some address like this, we get this. 251 | B58.decode58!("16UjcYNBG9GTK4uq2f7yYEbuifqCzoLMGS") 252 | <<0, 60, 23, 110, 101, 155, 234, 15, 41, 163, 233, 191, 120, 128, 193, 18, 177, 253 | 179, 27, 77, 200, 38, 38, 129, 135>> 254 | 255 | # That doesn't look like what uncle crypto told me I should get. Why? 256 | B58.decode58!("16UjcYNBG9GTK4uq2f7yYEbuifqCzoLMGS") |> Base.encode16(case: :lower) 257 | # Much better 258 | "003c176e659bea0f29a3e9bf7880c112b1b31b4dc826268187" 259 | ``` 260 | 261 | Please pay attention to the following at all times to avoid mistakes and self-induced pain: 262 | 263 | * Know your source data encoding. This is usually UTF-8, Latin-1, Base2, Base16, Base32, or Base64 these days 264 | * Convert your source data to a universal input format, using the Elixir `Base` module and `decode` functions to decode it typically. 265 | * Use the correct alphabet for encoding and decoding. You cannot encode in one alphabet and decode in another properly. 266 | * Know your intended destination format when checking your results. If your result is another format, again typically use the Elixir `Base` module and `encode` functions to encode it. 267 | * Pay attention to your source format when using integers. You will lose leading zeroes if you rely on anything that tries to convert to the smallest representation, such as `:binary.encode_unsigned/2` 268 | * Decide up front whether you will pass version or unversioned data. Use the `version` prefixed functions for dealing with cases where you want things to include a version by default. 269 | 270 | See the [API]([https://hexdocs.pm/basefiftyeight/B58.html)) docs for more examples. 271 | 272 | ## Installation 273 | 274 | Base Fifty Eight is available via [Hex](https://hex.pm/packages/basefiftyeight). The package can be installed by adding `basefiftyeight` to your list of dependencies in `mix.exs`: 275 | 276 | ```elixir 277 | def deps do 278 | [ 279 | {:basefiftyeight, "~> 0.1.0"} 280 | ] 281 | end 282 | ``` 283 | 284 | API Documentation can be found at [https://hexdocs.pm/basefiftyeight/B58.html](https://hexdocs.pm/basefiftyeight/B58.html). 285 | 286 | ## Why 287 | 288 | There are several Elixir Base58 libraries as well as implementations buried in Bitcoin, IPFS, and other technologies that use Base58. I reviewed many of these libraries, Benchmarked them, and tested them. No, I will not name names. 289 | 290 | Among the many issues across implementations and languages I consistently identified included: 291 | 292 | * Conversions of characters and codes that were O(n) or worse 293 | * Lack of support for 0-prefixed binaries or incorrect handling 294 | * Leading zeroes cause bugs in alternative alphabets if added to many implementations with naive assumptions about decoding 295 | * Weird assumptions about input and output formats that do not match real-world use-cases 296 | * Unnecessary binary allocations 297 | * Unoptimized binary handling 298 | * Compiling with `ERL_COMPILER_OPTIONS=bin_opt_info` listed additional issues in some cases 299 | * Limited alphabet support 300 | * I require the ability to support multiple with easy extensibility 301 | * I do not want call overhead passing in an alphabet that must be parsed at runtime 302 | * Unable to round-trip encoding and decoding, i.e. bugs 303 | * Lack of or incorrect Base58 check encoding, including failure to round-trip 304 | * Obtuse or inconsistent APIs that worked with types that add overhead or don't make sense for fast encoding/decoding 305 | * NIF dependencies - I want a clean VM for things that can be done in Elixir, not to mention some of these approaches were actually slower 306 | * General performance issues 307 | * Caused by binary handling, concatenation, abuse of Enum/List (find, find_index, at, etc.) on binaries, failure to leverage TCO, etc. 308 | * Overflow issues 309 | * Typically caused by using built-in pow function 310 | * Incorrect conversions 311 | * Integers becoming floats, for example as a result of not using integer division 312 | * Many libraries only implement one of encoding or decoding, I need both ways 313 | * Input and output ambiguity 314 | * Lack of clarity and documentation regarding if the expectation is a hex encoded string, a utf-8 binary string, or something else 315 | * Some libraries encode one way and decode another, creating further confusion 316 | * Some libraries try to be "smart" and try/fail with hex encoding, which adds unnecessary overhead 317 | * Base58Check libraries conflate regular binaries with versioned binaries or do not specify which the library is accepting 318 | 319 | Rather than trying to resolve all these issues via pull requests and possibly asking authors to further extract code, I decided to create a clean implementation that is closer aligned to the existing `Base` module in Elixir's core. 320 | 321 | ## Goals 322 | 323 | * Use best practices handling binaries and large numbers 324 | * Avoid extra memory allocations and overhead when possible and within reason 325 | * Consistent performance behavior for long and short binaries 326 | * Avoid external dependencies 327 | * Keep API as consistent as possible with existing Elixir core `Base` module 328 | * Minimize runtime pattern-matching overhead for encoding and decoding, especially with different alphabets 329 | * Avoid making code overly generic at the expense of runtime performance, instead look to compile-time solutions 330 | * Ensure adding new alphabets is reasonably painless 331 | * Create functions with documentation 332 | * Avoid generating functions that can't easily be documented without compile-time annoyances such as extra loops for macro fragments to please the doc gods 333 | * Be pragmatic 334 | * Accept some sugar trade-offs where benchmarking shows negligible performance impacts 335 | 336 | ## Issues 337 | 338 | Please see the [FAQ](docs/FAQ.md) in the `docs` folder before filing an issue. Further, there is a discussion of common issues and concerns there. -------------------------------------------------------------------------------- /lib/b58.ex: -------------------------------------------------------------------------------- 1 | defmodule B58 do 2 | @moduledoc """ 3 | This module provides Base58 and Base58Check data encoding and decoding functions. 4 | 5 | The following alphabets are supported for encoding and decoding, for both Base58 and Base58Check: 6 | 7 | * `Bitcoin` 8 | * `Flickr` 9 | * `Ripple` 10 | 11 | Note that `IPFS` uses the same alphabet as Bitcoin. Since this alphabet appeared in Bitcoin first, it will be referred to hereafter as Bitcoin, though it can be used for encoding and decoding data from IPFS without issue. 12 | 13 | ## Overview 14 | 15 | Base58 is a Base58 and Base58Check library that tries to make pragmatic compromises, while maintaining best practices with regard to Elixir and BEAM performance. It has no external dependencies and supports adding additional values with a simple update of an internal map. 16 | 17 | Additionally, B58 encodes and decodes data in a consistent manner - plain binary in, plain binary out. No additional steps, clever hacks, or assumptions are made with regard to input and output. Furthermore, the library is relatively consistent with the approach taken in the Elixir core `Base` library. 18 | 19 | In cases where exceptions may be more common, `!` suffixed functions are provided, ex: `base58_decode` vs `base58_decode!`. 20 | 21 | Base58Check data also supports two versions of encoding and decoding respectively. Version binaries are assumed to be input and output via functions prefixed with `version`, ex: `version_encode58_check!/2`. A versioned binary can also be created using `version_binary/2`. Version arguments are also consistently arranged so that they may be easily piped using Elixir. Unversioned binaries may also be encoded and decoded and simply use the same pattern of functions without the version prefix in the name, ex: `encode58_check!`. 22 | 23 | Ensure that when encoding and decoding, you use the appropriate alphabet. For example, if you encode using the Flickr alphabet, decode using the same alphabet. If you do not control encoding, ensure you know up-front what alphabet is used. Base58 will often decode using the incorrect alphabet without an error, but produce wrong results. Base58Check on the other hand will typically fail up-front due to the checksum being incorrect. It is thus possible, but not advisable to try to brute-force detect the alphabet for Base58Check using the `alphabets/0` function with a `reduce_while/3` function as a last resort. 24 | 25 | Ensure the Erlang "crypto" module is installed. There are some OS distributions that do not include this. 26 | 27 | ## Why Base58 Encoding 28 | 29 | The most compelling reason to use Base58 is if you are using or interfacing any technology that already uses it. This includes various crypto-currencies such as Bitcoin, Blockchain-based experiments, various distributed systems, IPFS, Flickr URLs, and many more. 30 | 31 | Generally, you can also decide to use Base58 if you want an unambiguous encoding that is less prone to errors when read by a human. Moreover, you can use Base58Check when you need to checksum and/or version your data. 32 | 33 | Taken from the original Bitcoin source code: 34 | 35 | ``` 36 | Why base-58 instead of standard base-64 encoding? 37 | - Don't want 0OIl characters that look the same in some fonts and could be used to create visually identical looking account numbers. 38 | - A string with non-alphanumeric characters is not as easily accepted as an account number. 39 | - E-mail usually won't line-break if there's no punctuation to break at. 40 | - Double-clicking selects the whole number as one word if it's all alphanumeric. 41 | ``` 42 | 43 | ## Base58Check Encoding 44 | 45 | The following features are outlined per the bitcoin wiki regarding Base58Check: 46 | 47 | ``` 48 | * An arbitrarily sized payload. 49 | * A set of 58 alphanumeric symbols consisting of easily distinguished uppercase and lowercase letters (0OIl are not used) 50 | * One byte of version/application information. Bitcoin addresses use 0x00 for this byte (future ones may use 0x05). 51 | * Four bytes (32 bits) of SHA256-based error checking code. This code can be used to automatically detect and possibly correct typographical errors. 52 | * An extra step for preservation of leading zeroes in the data. 53 | 54 | ## Base58Check vs Base58 Encoding 55 | 56 | You should use Base58Check over Base58 encoding if any of the features it presents are appealing to you. Perhaps the most compelling reason to use Base58Check, however, is the ability to version data and embed a checksum. 57 | 58 | Generally, Base58 should perform better as Base58Check is a layer on top of Base58. The checksum in Base58Check, however, will usually prevent accidental decoding using the wrong alphabet. 59 | 60 | ``` 61 | 62 | ## Why Not Base58 or Base58Check 63 | 64 | Base58 and Base58Check are suitable and desirable for many reasons ranging from protection from character ambiguity to checksums to real-world software usage. Nevertheless, there are several considerations you should evaluate before selecting Base58 or Base58Check. 65 | 66 | Among the reasons not to use Base58 or Base58Check include: 67 | 68 | * Speed 69 | * Usage of `div` and `mod`. Both these operations are slow, especially in Elixir. Many other encodings are not performance limited by these operations and thus faster. 70 | * It may be possible to gain more speed using a technique where div and mod (divmod) together are replaced with a CRC + table approach to avoid the overhead. This is more readily doable in C or C++. It is not clear whether doing this in Elixir has benefits nor if it is worth it in terms of stability and performance doing so via a NIF. 71 | * Base58Check introduces additional overhead via sha-256, extra binary manipulation, and checksum verification during decoding. 72 | * Dynamic alphabets 73 | * Base58 is not as amenable to dynamic alphabets. This library also intentionally ignores this constraint. The main reason is that there are some characters that are counter to the goals of Base58. 74 | * Losing performance to support a dynamic alphabet is probably not a tradeoff you should be making. Note that dynamic alphabets that follow the guidelines of existing alphabets can be added with a simple compile-time updates of this library. 75 | * Payload size 76 | * Some encodings may produce a smaller payload size relative to the source data. 77 | * You can compress a Base58 payload, but it is not an inherent goal of the algorithm itself. 78 | 79 | ## Selecting an Alphabet 80 | 81 | You should select an alphabet that aligns with your inteded use-cases. If in doubt, start with the bitcoin (`:btc`) alphabet. For usage with URLs such as shorteners or parameters, the Flickr (`:flickr`) alphabet has seen some real-world usage and is thus worth considering. 82 | 83 | If you need to add an alphabet, it is suggested to do so at compile time by updating the `alphabet_meta` map found in the source. It is strongly advised to use an existing alphabet, however. 84 | 85 | ## Base58 Bitcoin Alphabet 86 | 87 | | Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding | 88 | |------:|---------:|------:|---------:|------:|---------:|------:|---------:| 89 | | 0| 1| 17| J| 34| b| 51| t| 90 | | 1| 2| 18| K| 35| c| 52| u| 91 | | 2| 3| 19| L| 36| d| 53| v| 92 | | 3| 4| 20| M| 37| e| 54| w| 93 | | 4| 5| 21| N| 38| f| 55| x| 94 | | 5| 6| 22| P| 39| g| 56| y| 95 | | 6| 7| 23| Q| 40| h| 57| z| 96 | | 7| 8| 24| R| 41| i| | | 97 | | 8| 9| 25| S| 42| j| | | 98 | | 9| A| 26| T| 43| k| | | 99 | | 10| B| 27| U| 44| m| | | 100 | | 11| C| 28| V| 45| n| | | 101 | | 12| D| 29| W| 46| o| | | 102 | | 13| E| 30| X| 47| p| | | 103 | | 14| F| 31| Y| 48| q| | | 104 | | 15| G| 32| Z| 49| r| | | 105 | | 16| H| 33| a| 50| s| | | 106 | 107 | ## Base58 Flickr Alphabet 108 | 109 | | Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding | 110 | |------:|---------:|------:|---------:|------:|---------:|------:|---------:| 111 | | 0| 1| 17| i| 34| A| 51| T| 112 | | 1| 2| 18| j| 35| B| 52| U| 113 | | 2| 3| 19| k| 36| C| 53| V| 114 | | 3| 4| 20| m| 37| D| 54| W| 115 | | 4| 5| 21| n| 38| E| 55| X| 116 | | 5| 6| 22| o| 39| F| 56| Y| 117 | | 6| 7| 23| p| 40| G| 57| Z| 118 | | 7| 8| 24| q| 41| H| | | 119 | | 8| 9| 25| r| 42| J| | | 120 | | 9| a| 26| s| 43| K| | | 121 | | 10| b| 27| t| 44| L| | | 122 | | 11| c| 28| u| 45| M| | | 123 | | 12| d| 29| v| 46| N| | | 124 | | 13| e| 30| w| 47| P| | | 125 | | 14| f| 31| x| 48| Q| | | 126 | | 15| g| 32| y| 49| R| | | 127 | | 16| h| 33| z| 50| S| | | 128 | 129 | ## Base58 Ripple Alphabet 130 | 131 | | Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding | 132 | |------:|---------:|------:|---------:|------:|---------:|------:|---------:| 133 | | 0| r| 17| J| 34| b| 51| t| 134 | | 1| p| 18| K| 35| c| 52| u| 135 | | 2| s| 19| L| 36| d| 53| v| 136 | | 3| h| 20| M| 37| e| 54| A| 137 | | 4| n| 21| 4| 38| C| 55| x| 138 | | 5| a| 22| P| 39| g| 56| y| 139 | | 6| f| 23| Q| 40| 6| 57| z| 140 | | 7| 3| 24| R| 41| 5| | | 141 | | 8| 9| 25| S| 42| j| | | 142 | | 9| w| 26| T| 43| k| | | 143 | | 10| B| 27| 7| 44| m| | | 144 | | 11| U| 28| V| 45| 8| | | 145 | | 12| D| 29| W| 46| o| | | 146 | | 13| N| 30| X| 47| F| | | 147 | | 14| E| 31| Y| 48| q| | | 148 | | 15| G| 32| Z| 49| i| | | 149 | | 16| H| 33| 2| 50| 1| | | 150 | 151 | ## References 152 | 153 | * [Base58](https://en.wikipedia.org/wiki/Base58) 154 | * [Base58Check](https://en.bitcoin.it/wiki/Base58Check_encoding) 155 | * [Base58 Flickr](https://www.flickr.com/groups/51035612836@N01/discuss/72157616713786392/) 156 | * [Elixir Base Module](https://github.com/elixir-lang/elixir/blob/master/lib/elixir/lib/base.ex) 157 | * [Ripple Github](https://github.com/ripple) 158 | * [Bitcoin Base58 Implementation](https://github.com/bitcoin/bitcoin/blob/master/src/base58.cpp) 159 | * [Erlang Crypto Module](http://erlang.org/doc/man/crypto.html) 160 | 161 | """ 162 | 163 | # note that each of these alphabets is a charlist, not a binary! 164 | b58_btc_alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 165 | b58_flickr_alphabet = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' 166 | b58_ripple_alphabet = 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz' 167 | 168 | # you can add more alphabets here. alphabets are kept in a specific order here intentionally. 169 | alphabet_meta = [ 170 | %{alphabet_id: :btc, alphabet: b58_btc_alphabet}, 171 | %{alphabet_id: :flickr, alphabet: b58_flickr_alphabet}, 172 | %{alphabet_id: :ripple, alphabet: b58_ripple_alphabet}, 173 | ] 174 | 175 | @typedoc """ 176 | 1-byte version binary 177 | """ 178 | @type version :: <<_::8>> 179 | 180 | @typedoc""" 181 | Unsigned single-byte integer (unint8 commonly) 182 | """ 183 | @type version_integer :: 0..255 184 | 185 | @typedoc """ 186 | A binary where the first byte is assumed to be a version. 187 | """ 188 | @type versioned_binary :: <<_::8, _::_*8>> 189 | 190 | @typedoc """ 191 | A Base58 encoded binary. 192 | """ 193 | @type b58_binary :: binary() 194 | 195 | @typedoc """ 196 | A Base58Check encoded binary 197 | """ 198 | @type b58check_binary() :: <<_::48, _::_*8>> 199 | 200 | @typedoc """ 201 | An alphabet ID atom that corresponds to the intended alphabet to be used for encoding or decoding. 202 | """ 203 | @type alphabet :: :btc | :flickr | :ripple 204 | 205 | # A word about errors: 206 | # ============================================================================ 207 | # We raise usually in this module because we're optimizing for the case of correct values for encoding and decoding. 208 | # Introducing tuples everywhere adds some GC churn and extra memory when all is well no matter what. 209 | # Rather than converting {:ok, result} into result or duping code, we simply rescue for now. 210 | # The other big reason this is done is to be somewhat consistent with the behavior of the Elixir `Base` library. 211 | # Raising is certainly less desirable than some alternatives, but we follow the core library's approach. 212 | # If someone has a concern about this or really needs it a different way, it would be easy enough to refactor most of the code here. 213 | # Stuffing some more code into private functions would help this effort, but I'm less of a fan of this unless necessary. 214 | # Personally, I do prefer an {:error, reason} approach, especially when atoms can be a reason to pattern match against., 215 | # The main case where I feel there is some benefit here is when dealing with checksums errors in Base58Check. In that case I would prefer {:error, :invalid_checksum}. 216 | # We could manipulate the rescue body to do some of this by raising more specific errors, but I'm not sure there is a demand for this and it introduces yet more code. 217 | 218 | @doc """ 219 | Encodes a binary in Base58. 220 | 221 | ## Examples 222 | 223 | iex> B58.encode58("Some chicken is better than no chicken") 224 | "DLrwGKtrzZkxaLwqpwBBbkgFiEGTYhhzEpjXuj5R2DZPGzbXfeVK" 225 | 226 | iex> B58.encode58("Some chicken is better than no chicken", alphabet: :btc) 227 | "DLrwGKtrzZkxaLwqpwBBbkgFiEGTYhhzEpjXuj5R2DZPGzbXfeVK" 228 | 229 | iex> B58.encode58("The T in Tacos stands for terrific", alphabet: :flickr) 230 | "2UF2YLgW2MQSutxJWpcW6afxo1YbVm4ZnXociLQ1eExXRwK" 231 | 232 | iex> "536D656C6C73206C696B652061206C6F636B657220726F6F6D20696E2068657265" |> Base.decode16!() |> B58.encode58(alphabet: :ripple) 233 | "R8RKuNXBxzb1Ai6HrP3mHmgyBEKmWjp944zS4Y9Q7hsPS" 234 | 235 | """ 236 | @spec encode58(binary(), keyword()) :: b58_binary() 237 | def encode58(data, opts \\ []) when is_binary(data) do 238 | alphabet = Keyword.get(opts, :alphabet, :btc) 239 | do_encode58(data, alphabet) 240 | end 241 | 242 | @doc """ 243 | Encodes a binary in Base58Check using the given version byte. 244 | 245 | ## Examples 246 | 247 | iex> B58.encode58_check!("Lost my sauce, sorry boss.", 0) 248 | "1GKJd5qu8gaFbd4tHVPF6q8ZtzER5ttJSwLTN2A5MT" 249 | 250 | iex> B58.encode58_check!("Lost my sauce, sorry boss.", 1, alphabet: :btc) 251 | "29cSfWnUdLoFyrRquaF4fQ2xnKSMg8TWenhe89Mc95" 252 | 253 | iex> B58.encode58_check!("Lettuceless Burritos", "m", alphabet: :ripple) 254 | "kyo6DdGZMd7yc1EmNVz2jh4sEc3qUNBY1D" 255 | 256 | iex> "466F6F20426172204578616D706C657320617265207265616C6C792064616D6E20737475706964" |> Base.decode16!() |> B58.encode58_check!("2", alphabet: :flickr) 257 | "hm63nMd9884zRLqGD2cYDUgk3JR1EEEbP6UQfEU2uPAoZfQe3VrXHfYvRkok" 258 | 259 | """ 260 | @spec encode58_check!(binary(), version | version_integer(), keyword()) :: b58check_binary() 261 | def encode58_check!(data, version, opts \\ []) do 262 | version_binary(data, version) |> do_encode58_check(opts) 263 | end 264 | 265 | @doc """ 266 | Encodes a binary in Base58Check using the given version byte. 267 | 268 | ## Examples 269 | 270 | iex> B58.encode58_check("So Afraid of failure", <<1>>, alphabet: :btc) 271 | {:ok, "XwmCCcWGbEjGsJk4THsxw28EcQuz6TYyu"} 272 | 273 | iex> B58.encode58_check("Incapable coworker lacking understanding what a byte is", <<1, 2>>, alphabet: :btc) 274 | {:error, 275 | "version must be a single byte binary or unsigned integer, data must be a binary."} 276 | 277 | """ 278 | @spec encode58_check(binary(), version | version_integer(), keyword()) :: {:ok, b58check_binary()} | {:error, term()} 279 | def encode58_check(data, version, opts \\ []) do 280 | {:ok, encode58_check!(data, version, opts)} 281 | rescue 282 | e in _ -> {:error, Exception.message(e)} 283 | end 284 | 285 | @doc """ 286 | Encodes a binary in Base58Check using the first byte as the version byte. Assumes the binary is already versioned. 287 | 288 | ## Examples 289 | 290 | iex> B58.version_encode58_check!(<<5, "256 versions ought to be enough for anyone">>) 291 | "3WGnVcRSYcejzu1tmjqG36i8zGFhPH5v7399paTXYSvCQPRsqSa86CcnRUPVJxXw" 292 | 293 | iex> B58.version_encode58_check!("1Mad Lad", alphabet: :ripple) 294 | "vx82hmfYGg4kdDK5" 295 | 296 | iex> B58.version_encode58_check!(<<255, "Push the limits until the car starts">>, alphabet: :flickr) 297 | "XZABbyyTLAtpXB4uyan6stxnyFhPH2kbr1v6j26SBAvDqrQi8GGBDKxs" 298 | 299 | """ 300 | @spec version_encode58_check!(versioned_binary(), keyword()) :: b58check_binary() 301 | def version_encode58_check!(versioned_data, opts \\ []) do 302 | do_encode58_check(versioned_data, opts) 303 | end 304 | 305 | @doc """ 306 | Encodes a binary in Base58Check using the first byte as the version byte. Assumes the binary is already versioned. 307 | 308 | ## Examples 309 | 310 | iex> B58.version_encode58_check(<<5, "256 versions ought to be enough for anyone">>) 311 | {:ok, "3WGnVcRSYcejzu1tmjqG36i8zGFhPH5v7399paTXYSvCQPRsqSa86CcnRUPVJxXw"} 312 | 313 | iex> B58.version_encode58_check("1Mad Lad", alphabet: :ripple) 314 | {:ok, "vx82hmfYGg4kdDK5"} 315 | 316 | iex> B58.version_encode58_check(<<255, "Push the limits until the car starts">>, alphabet: :flickr) 317 | {:ok, "XZABbyyTLAtpXB4uyan6stxnyFhPH2kbr1v6j26SBAvDqrQi8GGBDKxs"} 318 | 319 | """ 320 | @spec version_encode58_check(versioned_binary(), keyword()) :: {:ok, b58check_binary()} | {:error, term()} 321 | def version_encode58_check(versioned_data, opts \\ []) do 322 | {:ok, do_encode58_check(versioned_data, opts)} 323 | rescue 324 | e in _ -> {:error, Exception.message(e)} 325 | end 326 | 327 | @doc """ 328 | Decodes a Base58 binary. 329 | 330 | An `ArgumentError` exception is raised if the binary contains characters that are invalid for the given alphabet. 331 | 332 | ## Examples 333 | 334 | iex> B58.decode58!("59QpGHkaSK8tngYpe9h17cseyAEGc") 335 | "Cheese Platter Please" 336 | 337 | iex> B58.decode58!("asDvaq68ktFaubY7KsnvBFMMD5Q8cd9PhGma3YcPqoWXX2aJoiaK1kc65gn", alphabet: :ripple) 338 | "Decode using the alphabet it was encoded in" 339 | 340 | iex> B58.decode58!("2zPJuvDvDNbavb7zdUVM9niBSHNTjPni85f8hPLJ119awVLKud3kWU5LsRGiqkzav8wD6MFj2QshN3b8SyTEpkG2pYcSAioeUUDe6wqhigji4vkBVQoBJ9mPLmySgjQyy8FrYu3rd", alphabet: :btc) |> Base.decode16!() 341 | "0088C2D2FA846282C870A76CADECBE45C4ACD72BB655DA1216" 342 | 343 | """ 344 | @spec decode58!(b58_binary(), keyword()) :: b58_binary() 345 | def decode58!(string, opts \\ []) when is_binary(string) do 346 | alphabet = Keyword.get(opts, :alphabet, :btc) 347 | do_decode58(string, alphabet) 348 | end 349 | 350 | @doc """ 351 | Decodes a Base58 binary. 352 | 353 | An `{:error, reason}` tuple is returned if the binary contains characters that are invalid for the given alphabet. 354 | 355 | ## Examples 356 | 357 | iex> B58.decode58("59QpGHkaSK8tngYpe9h17cseyAEGc", alphabet: :btc) 358 | {:ok, "Cheese Platter Please"} 359 | 360 | iex> B58.decode58("2nePN7syqoQe2mfeY", alphabet: :flickr) 361 | {:ok, "Hello Sailor"} 362 | 363 | iex> B58.decode58("02nePN7syqoQe2mfeY", alphabet: :flickr) 364 | {:error, "non-alphabet digit found: \\"0\\" (byte 48)"} 365 | 366 | """ 367 | @spec decode58(b58_binary(), keyword()) :: {:ok, b58_binary()} | {:error, term()} 368 | def decode58(string, opts \\ []) when is_binary(string) do 369 | {:ok, decode58!(string, opts)} 370 | rescue 371 | e in _ -> {:error, Exception.message(e)} 372 | end 373 | 374 | @doc """ 375 | Decodes a Base58Check binary. 376 | 377 | An `ArgumentError` exception is raised if the binary contains characters that are invalid for the given alphabet or if the checksum bytes do not match the payload. 378 | 379 | ## Examples 380 | 381 | iex> B58.decode58_check!("12nwcb8vmxv3waEzzPtaY3XBmWbwvGvqw", alphabet: :flickr) 382 | {"Backstrom for Selke", <<0>>} 383 | 384 | iex> B58.decode58_check!("ftyYJFWLdZWzNfyFqPGXGpxM6aFNnTkqmz1QwcoT3eQAQPb9V4bRQScejwWQWTR8", alphabet: :ripple) 385 | {"No one wants to play Sega Genesis with you", <<14>>} 386 | 387 | iex> B58.decode58_check!("2mzYnhGdFMacGVTrvyZsnqDGPqWFefouMQA") 388 | {<<245, 74, 88, 81, 233, 55, 43, 135, 129, 10, 142, 96, 205, 210, 231, 207, 216, 389 | 11, 110, 49>>, <<255>>} 390 | 391 | """ 392 | @spec decode58_check!(b58check_binary(), keyword()) :: {binary(), version()} 393 | def decode58_check!(string, opts \\ []) 394 | def decode58_check!(string, opts) when is_binary(string) and byte_size(string) > 5 do 395 | decoded_bin = decode58!(string, opts) 396 | decoded_size = byte_size(decoded_bin) 397 | payload_size = decoded_size - 5 398 | 399 | <> = decoded_bin 400 | if calculate_checksum(<>) == checksum do 401 | {payload, version} 402 | else 403 | raise ArgumentError, "Invalid checksum." 404 | end 405 | end 406 | 407 | def decode58_check!(_string, _opts) do 408 | # A base 58 string will always be at least 5 bytes due to the version and checksum, 409 | raise ArgumentError, "Invalid Base58Check string." 410 | end 411 | 412 | @doc """ 413 | Decodes a Base58Check binary. 414 | 415 | An `{:error, reason}` tuple is returned if the binary contains characters that are invalid for the given alphabet or if the checksum bytes do not match the payload. 416 | 417 | ## Examples 418 | 419 | iex> B58.decode58_check("QGgq7M6oMmgLQiWY9SUgo5QoaK6FH44nM") 420 | {:ok, {"Backstrom for Selke", <<255>>}} 421 | 422 | iex> B58.decode58_check!("17B1aKcMUBrrX9t6Y7ZUSkDoedGLiybgnJ", alphabet: :flickr) 423 | {"Hot Delicious Treats", <<0>>} 424 | 425 | iex> B58.decode58_check("QGgq7M6oMmgLQiWY9SUgo5QoaK6FH44n_", alphabet: :btc) 426 | {:error, "non-alphabet digit found: \\"_\\" (byte 95)"} 427 | 428 | iex> B58.decode58_check("QGgq7M6oMmgLQiWY9SUgo5QoaK6FH44nM", alphabet: :ripple) 429 | {:error, "Invalid checksum."} 430 | 431 | """ 432 | @spec decode58_check(b58check_binary(), keyword()) :: {:ok, {binary(), version()}} | {:error, term()} 433 | def decode58_check(string, opts \\ []) when is_binary(string) do 434 | {:ok, decode58_check!(string, opts)} 435 | rescue 436 | e in _ -> {:error, Exception.message(e)} 437 | end 438 | 439 | @doc """ 440 | Decodes a Base58Check binary with the version byte included in the returned binary. 441 | 442 | An `ArgumentError` exception is raised if the binary contains characters that are invalid for the given alphabet or if the checksum bytes do not match the payload. 443 | 444 | ## Examples 445 | 446 | iex> B58.version_decode58_check!("2XLgTmqxf6wu4zAwBAb2hvezGWht") 447 | "mSeal of Quality" 448 | 449 | iex> B58.version_decode58_check!("5XtVTWqqaYj7VUodxQwwgnioaGCoB1fVFzMSXLfQWBiUmsEfo6oJ73CpJojLDuYa3fFE77uvAax4ctzBT17me", alphabet: :flickr) 450 | "SEvery library says it is fast. Surely, they are all fast." 451 | 452 | iex> B58.version_decode58_check!("YXUaKUi6gkm3DRXp8hDrbB", alphabet: :ripple) 453 | <<255, 66, 114, 111, 103, 114, 97, 109, 109, 101, 114, 115>> 454 | 455 | """ 456 | @spec version_decode58_check!(b58check_binary(), keyword()) :: versioned_binary() 457 | def version_decode58_check!(string, opts \\ []) 458 | def version_decode58_check!(string, opts) when is_binary(string) and byte_size(string) > 5 do 459 | # Again, intentionally avoiding error tuples and such here 460 | decoded_bin = decode58!(string, opts) 461 | decoded_size = byte_size(decoded_bin) 462 | bin_size = decoded_size - 4 463 | 464 | <> = decoded_bin 465 | if calculate_checksum(versioned_binary) == checksum do 466 | versioned_binary 467 | else 468 | raise ArgumentError, "Invalid checksum." 469 | end 470 | end 471 | 472 | def version_decode58_check!(_string, _opts) do 473 | raise ArgumentError, "Invalid Base58Check string." 474 | end 475 | 476 | @doc """ 477 | Decodes a Base58Check binary with the version byte included in the returned binary. 478 | 479 | An `{:error, reason}` tuple is returned if the binary contains characters that are invalid for the given alphabet or if the checksum bytes do not match the payload. 480 | 481 | ## Examples 482 | 483 | iex> B58.version_decode58_check("2XLgTmqxf6wu4zAwBAb2hvezGWht") 484 | {:ok, "mSeal of Quality"} 485 | 486 | iex> B58.version_decode58_check("2XLgTmqxf6wu4zAwBAb2hvezGWht", alphabet: :flickr) 487 | {:error, "Invalid checksum."} 488 | 489 | iex> B58.version_decode58_check("2XLgTmqxf6wu4zAwBAb2hvezGWht0", alphabet: :btc) 490 | {:error, "non-alphabet digit found: \\"0\\" (byte 48)"} 491 | 492 | iex> B58.version_decode58_check("2QRkeNpTrYPiooUJjcubi1fprQ7aB8XvxSZ2wa4JQeBryrPVCvvkcY5NkXeptH6UzjKBBznXp58MWv4TTBvSHgeyjwPtY", alphabet: :ripple) 493 | {:ok, 494 | <<255, 83, 116, 111, 112, 32, 117, 115, 105, 110, 103, 32, 69, 110, 117, 109, 495 | 46, 97, 116, 32, 116, 111, 32, 108, 111, 111, 107, 117, 112, 32, 116, 104, 496 | 105, 110, 103, 115, 32, 105, 110, 32, 99, 111, 100, 101, 99, 115, 46, 32, 75, 497 | 110, 111, 119, 32, 121, 111, 117, 114, 32, 66, 105, 103, 32, 79, 46>>} 498 | 499 | """ 500 | @spec version_decode58_check(b58check_binary(), keyword()) :: {:ok, versioned_binary()} | {:error, term()} 501 | def version_decode58_check(string, opts \\ []) when is_binary(string) do 502 | {:ok, version_decode58_check!(string, opts)} 503 | rescue 504 | e in _ -> {:error, Exception.message(e)} 505 | end 506 | 507 | @doc """ 508 | Versions a binary according to the Base58Check rules. 509 | 510 | Only use with unversioned binaries. 511 | 512 | An ArgumentError is raised if the version is not a single byte unsigned integer (uint8). 513 | 514 | ## Examples 515 | 516 | iex> B58.version_binary("Chicken Recipe", 42) 517 | "*Chicken Recipe" 518 | 519 | iex> B58.version_binary("Chicken Recipe", <<255>>) 520 | <<255, 67, 104, 105, 99, 107, 101, 110, 32, 82, 101, 99, 105, 112, 101>> 521 | 522 | iex> B58.version_binary("Chicken Recipe", <<0>>) |> B58.version_encode58_check!() 523 | "13oALC94NVeHBQsneF6tYRmW4R" 524 | 525 | """ 526 | @spec version_binary(binary(), version | version_integer()) :: versioned_binary() 527 | def version_binary(data, version) 528 | def version_binary(data, version) 529 | when is_binary(data) and is_integer(version) and version >= 0 and version <= 255 do 530 | <> 531 | end 532 | 533 | def version_binary(data, version) 534 | when is_binary(data) and is_binary(version) and byte_size(version) == 1 do 535 | <> 536 | end 537 | 538 | def version_binary(_data, _version) do 539 | raise ArgumentError, "version must be a single byte binary or unsigned integer, data must be a binary." 540 | end 541 | 542 | @doc """ 543 | Lists all the alphabets available as identifiers to use with Base58. 544 | 545 | ## Examples 546 | 547 | iex> B58.alphabets() 548 | [:btc, :flickr, :ripple] 549 | 550 | """ 551 | @spec alphabets() :: [alphabet()] 552 | defmacro alphabets() do 553 | alphabet_ids = Enum.map(unquote(alphabet_meta|> Macro.escape()) , fn(%{alphabet_id: alphabet_id}) -> alphabet_id end) 554 | quote do 555 | unquote(alphabet_ids) 556 | end 557 | end 558 | 559 | #=============================================================================== 560 | # Private eyes, is watching you 561 | #=============================================================================== 562 | 563 | #=============================================================================== 564 | # Would prefer to write the following few macros a little bit differently, but leaving this to be semi-consistent with the 'Base' Elixir core library. 565 | # This code here has been adapted from https://github.com/elixir-lang/elixir/blob/v1.7.3/lib/elixir/lib/base.ex and is somewhat simplified. 566 | #=============================================================================== 567 | defmacrop encode_char(alphabet, value) do 568 | quote do 569 | case unquote(value) do 570 | unquote(encode_char_clauses(alphabet)) 571 | end 572 | end 573 | end 574 | 575 | defp encode_char_clauses(alphabet) do 576 | clauses = alphabet 577 | |> Enum.with_index() 578 | |> encode_clauses() 579 | 580 | clauses ++ bad_digit_clause() 581 | end 582 | 583 | defp encode_clauses(alphabet) do 584 | for {encoding, value} <- alphabet do 585 | [clause] = quote(do: (unquote(value) -> unquote(encoding))) 586 | clause 587 | end 588 | end 589 | 590 | defmacrop decode_char(alphabet, encoding) do 591 | quote do 592 | case unquote(encoding) do 593 | unquote(decode_char_clauses(alphabet)) 594 | end 595 | end 596 | end 597 | 598 | defp decode_char_clauses(alphabet) do 599 | clauses = 600 | alphabet 601 | |> Enum.with_index() 602 | |> decode_clauses() 603 | 604 | clauses ++ bad_digit_clause() 605 | end 606 | 607 | defp decode_clauses(alphabet) do 608 | for {encoding, value} <- alphabet do 609 | [clause] = quote(do: (unquote(encoding) -> unquote(value))) 610 | clause 611 | end 612 | end 613 | 614 | defp bad_digit_clause() do 615 | quote do 616 | c -> 617 | raise ArgumentError, 618 | "non-alphabet digit found: #{inspect(<>, binaries: :as_strings)} (byte #{c})" 619 | end 620 | end 621 | 622 | # =============================================================================== 623 | # Encoding 624 | # =============================================================================== 625 | defp calculate_checksum(versioned_data) do 626 | <> = 627 | :crypto.hash(:sha256, :crypto.hash(:sha256, versioned_data)) 628 | 629 | checksum 630 | end 631 | 632 | defp do_encode58_check(versioned_data, opts) do 633 | checksum = calculate_checksum(versioned_data) 634 | encode58(<>, opts) 635 | end 636 | 637 | for %{alphabet_id: alphabet_id, alphabet: alphabet} <- alphabet_meta do 638 | encode_body_fun = :"encode58_#{alphabet_id}_body" 639 | char_fun = :"encode58_#{alphabet_id}_char" 640 | prefix_encode_fun = :"prefix_#{alphabet_id}_encode" 641 | 642 | defp unquote(char_fun)(value) do 643 | encode_char(unquote(alphabet), value) 644 | end 645 | 646 | defp do_encode58(<<>>, unquote(alphabet_id)) do 647 | <<>> 648 | end 649 | 650 | # we proxy this call first to pattern match *once*, not every iteration when recursively decoding 651 | # this code could be extracted, but with real benchmarks, emitting the same code seemed to perform a bit better than trying to have an extra call to produce a hot path 652 | # in the future might be worth a slight refactor + bench to see if this changes as new BEAM and Elixir versions are released 653 | defp do_encode58(data, unquote(alphabet_id)) do 654 | data 655 | |> :binary.decode_unsigned() 656 | |> unquote(encode_body_fun)([]) 657 | |> unquote(prefix_encode_fun)(data) 658 | |> to_string() 659 | end 660 | 661 | defp unquote(encode_body_fun)(0, acc) do 662 | acc 663 | end 664 | 665 | defp unquote(encode_body_fun)(n, acc) do 666 | quotient = div(n, 58) 667 | char = rem(n, 58) |> unquote(char_fun)() 668 | unquote(encode_body_fun)(quotient, [char | acc]) 669 | end 670 | 671 | defp unquote(prefix_encode_fun)(encoded, <<0, rest::binary>>), 672 | do: unquote(prefix_encode_fun)([unquote(char_fun)(0) | encoded], rest) 673 | 674 | defp unquote(prefix_encode_fun)(encoded, _data), do: encoded 675 | end 676 | 677 | # =============================================================================== 678 | # Decoding 679 | # =============================================================================== 680 | defp do_decode58(<<>>, _alphabet_id) do 681 | <<>> 682 | end 683 | 684 | defp do_decode58(string, alphabet_id) do 685 | decode_body(alphabet_id, string) 686 | end 687 | 688 | for %{alphabet_id: alphabet_id, alphabet: alphabet} <- alphabet_meta do 689 | decode_body_fun = :"decode58_#{alphabet_id}_body" 690 | char_fun = :"decode58_#{alphabet_id}_char" 691 | decode_prefix_fun = :"decode58_#{alphabet_id}_prefix" 692 | zero_char = Enum.at(alphabet, 0) 693 | 694 | defp unquote(char_fun)(value) do 695 | decode_char(unquote(alphabet), value) 696 | end 697 | 698 | defp unquote(decode_body_fun)([], acc) do 699 | acc |> :binary.encode_unsigned() 700 | end 701 | 702 | defp unquote(decode_body_fun)([char | remaining_chars], acc) do 703 | unquote(decode_body_fun)(remaining_chars, acc * 58 + unquote(char_fun)(char)) 704 | end 705 | 706 | defp decode_body(unquote(alphabet_id), string) do 707 | {remaining_string, leading_zeroes_count} = unquote(decode_prefix_fun)(string, 0) 708 | 709 | body = 710 | if remaining_string == <<>>, 711 | do: <<>>, 712 | else: unquote(decode_body_fun)(to_charlist(remaining_string), 0) 713 | 714 | <<0::size(leading_zeroes_count)-unit(8), body::binary>> 715 | end 716 | 717 | defp unquote(decode_prefix_fun)(<>, acc), 718 | do: unquote(decode_prefix_fun)(rest, acc + 1) 719 | 720 | defp unquote(decode_prefix_fun)(bin, acc), do: {bin, acc} 721 | end 722 | end 723 | -------------------------------------------------------------------------------- /test/b58_test.exs: -------------------------------------------------------------------------------- 1 | defmodule B58Test do 2 | use ExUnit.Case, async: true 3 | 4 | doctest B58 5 | import B58 6 | 7 | # ============================================================================ 8 | # Defaults 9 | # ============================================================================ 10 | test "encode58/1 Base58 encodes strings according to the bitcoin alphabet" do 11 | assert B58.encode58("hello") == "Cn8eVZg" 12 | assert B58.encode58("hello world") == "StV1DL6CwTryKyV" 13 | assert B58.encode58("Hello World") == "JxF12TrwUP45BMd" 14 | assert B58.encode58(<<>>) == <<>> 15 | end 16 | 17 | test "encode58/1 Base58 encodes sha-256 strings according to the bitcoin alphabet" do 18 | # From https://github.com/multiformats/multihash 19 | assert "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 20 | |> Base.decode16!() 21 | |> B58.encode58() == "QmYtUc4iTCbbfVSDNKvtQqrfyezPPnFvE33wFmutw9PBBk" 22 | end 23 | 24 | test "encode58/1 handles Base58 encoding leading zeroes using the bitcoin alphabet" do 25 | assert B58.encode58(<<0>>) == "1" 26 | assert B58.encode58(<<0, 0, 0, "hello world">>) == "111StV1DL6CwTryKyV" 27 | assert B58.encode58(<<0, 0, 0>>) == "111" 28 | end 29 | 30 | test "encode58_check!/2 accepts only unsigned single byte integers using the bitcoin alphabet" do 31 | assert B58.encode58_check!("a", <<0>>) == "1C3t9Nib" 32 | assert B58.encode58_check!("a", 0) == "1C3t9Nib" 33 | assert B58.encode58_check!("a", <<1>>) == "gqkwoXD" 34 | assert B58.encode58_check!("a", 1) == "gqkwoXD" 35 | assert B58.encode58_check!("a", <<2>>) == "2BkJbhjW" 36 | assert B58.encode58_check!("a", 2) == "2BkJbhjW" 37 | assert B58.encode58_check!("a", <<255>>) == "3CAw3RMCe" 38 | assert B58.encode58_check!("a", 255) == "3CAw3RMCe" 39 | assert B58.encode58_check!(<<>>, <<2>>) == "Epi3KP" 40 | assert B58.encode58_check!(<<>>, 2) == "Epi3KP" 41 | assert_raise ArgumentError, fn -> 42 | B58.encode58_check!("a", 256) 43 | end 44 | assert_raise ArgumentError, fn -> 45 | B58.encode58_check!("a", -1) 46 | end 47 | assert_raise ArgumentError, fn -> 48 | B58.encode58_check!("a", <<1, 0>>) 49 | end 50 | end 51 | 52 | test "encode58_check!/2 Base58Check encodes strings according to the bitcoin alphabet" do 53 | assert B58.encode58_check!("hello", 0) == "12L5B5yqsf7vwb" 54 | assert B58.encode58_check!("hello world", 1) == "B5oSH5yUDQS9XwzogrcWP" 55 | assert B58.encode58_check!("Hello World", 255) == "YXMkDYBSTEWVuE2vQhQ1nc" 56 | assert B58.encode58_check!(<<>>, 0) == "1Wh4bh" 57 | end 58 | 59 | test "encode58_check/2 Base58Check encodes strings according to the bitcoin alphabet" do 60 | assert B58.encode58_check("hello", 0) == {:ok, "12L5B5yqsf7vwb"} 61 | assert B58.encode58_check("hello world", 1) == {:ok, "B5oSH5yUDQS9XwzogrcWP"} 62 | assert B58.encode58_check("Hello World", 255) == {:ok, "YXMkDYBSTEWVuE2vQhQ1nc"} 63 | assert B58.encode58_check(<<>>, 0) == {:ok, "1Wh4bh"} 64 | end 65 | 66 | test "encode58_check!/2 Base58Check encodes sha-256 strings according to the bitcoin alphabet" do 67 | # From https://github.com/multiformats/multihash 68 | assert "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 69 | |> Base.decode16!() 70 | |> B58.encode58_check!(0) == "13gXk986h9pApW3uNeGAHDqUnYHo8c1oSfHajVDuzPtiSokwhzzeK" 71 | end 72 | 73 | test "encode58_check!/2 Base58Check encodes a RIPEMD-160 hash" do 74 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 75 | assert "f54a5851e9372b87810a8e60cdd2e7cfd80b6e31" 76 | |> Base.decode16!(case: :lower) 77 | |> B58.encode58_check!(0) == "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs" 78 | end 79 | 80 | test "encode58_check!/1 handles Base58 encoding leading zeroes" do 81 | assert B58.encode58_check!(<<0>>, 0) == "112edB6q" 82 | assert B58.encode58_check!(<<0, 0, 0, "hello world">>, 0) == "11113vQB7B6MrGQZaxCrokgx4" 83 | assert B58.encode58_check!(<<0, 0, 0>>, 0) == "11114bdQda" 84 | end 85 | 86 | test "version_encode58_check!/2 accepts unsigned single byte integer versions" do 87 | assert B58.version_encode58_check!(<<0, "a">>) == "1C3t9Nib" 88 | assert B58.version_encode58_check!(<<1, "a">>) == "gqkwoXD" 89 | assert B58.version_encode58_check!(<<2, "a">>) == "2BkJbhjW" 90 | assert B58.version_encode58_check!(<<255, "a">>) == "3CAw3RMCe" 91 | assert B58.version_encode58_check!(<<2>>) == "Epi3KP" 92 | end 93 | 94 | test "version_encode58_check!/2 Base58Check encodes strings according to the bitcoin alphabet" do 95 | assert B58.version_encode58_check!(<<0, "hello">>) == "12L5B5yqsf7vwb" 96 | assert B58.version_encode58_check!(<<1, "hello world">>) == "B5oSH5yUDQS9XwzogrcWP" 97 | assert B58.version_encode58_check!(<<255, "Hello World">>) == "YXMkDYBSTEWVuE2vQhQ1nc" 98 | assert B58.version_encode58_check!(<<0>>) == "1Wh4bh" 99 | end 100 | 101 | test "version_encode58_check/2 Base58Check encodes strings according to the bitcoin alphabet" do 102 | assert B58.version_encode58_check(<<0, "hello">>) == {:ok, "12L5B5yqsf7vwb"} 103 | assert B58.version_encode58_check(<<1, "hello world">>) == {:ok, "B5oSH5yUDQS9XwzogrcWP"} 104 | assert B58.version_encode58_check(<<255, "Hello World">>) == {:ok, "YXMkDYBSTEWVuE2vQhQ1nc"} 105 | assert B58.version_encode58_check(<<0>>) == {:ok, "1Wh4bh"} 106 | end 107 | 108 | test "version_encode58_check!/1 Base58Check encodes a versioned RIPEMD-160 hash" do 109 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 110 | assert 0xf54a5851e9372b87810a8e60cdd2e7cfd80b6e31 111 | |> :binary.encode_unsigned() 112 | |> B58.version_binary(0) 113 | |> B58.version_encode58_check!() == "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs" 114 | end 115 | 116 | test "version_encode58_check!/1 handles Base58 encoding leading zeroes" do 117 | assert B58.version_encode58_check!(<<0, 0>>) == "112edB6q" 118 | assert B58.version_encode58_check!(<<0, 0, 0, 0, "hello world">>) == "11113vQB7B6MrGQZaxCrokgx4" 119 | assert B58.version_encode58_check!(<<0, 0, 0, 0>>) == "11114bdQda" 120 | end 121 | 122 | test "decode58!/1 decodes Base58 encoded binaries using the bitcoin alphabet" do 123 | assert B58.decode58!("Cn8eVZg") == "hello" 124 | assert B58.decode58!("StV1DL6CwTryKyV") == "hello world" 125 | assert B58.decode58!("JxF12TrwUP45BMd") == "Hello World" 126 | assert B58.decode58!(<<>>) == <<>> 127 | end 128 | 129 | test "decode58!/1 handles Base58 encoded with leading zeroes using the bitcoin alphabet" do 130 | assert B58.decode58!("1") == <<0>> 131 | assert B58.decode58!("111StV1DL6CwTryKyV") == <<0, 0, 0, "hello world">> 132 | assert B58.decode58!("111") == <<0, 0, 0>> 133 | end 134 | 135 | test "decode58!/1 Base58 decodes sha-256 strings using the bitcoin alphabet" do 136 | # From https://github.com/multiformats/multihash 137 | assert "QmYtUc4iTCbbfVSDNKvtQqrfyezPPnFvE33wFmutw9PBBk" 138 | |> B58.decode58!() 139 | |> Base.encode16() == "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 140 | end 141 | 142 | test "decode58!/1 Base58 handles invalid binaries when using the bitcoin alphabet" do 143 | #invalid character 144 | assert_raise ArgumentError, fn -> 145 | B58.decode58!("~") 146 | end 147 | #invalid leading character 148 | assert_raise ArgumentError, fn -> 149 | B58.decode58!("~Cn8eVZg") 150 | end 151 | #invalid trailing character 152 | assert_raise ArgumentError, fn -> 153 | B58.decode58!("Cn8eVZg^") 154 | end 155 | #invalid character mid string 156 | assert_raise ArgumentError, fn -> 157 | B58.decode58!("Cn8%eVZg") 158 | end 159 | #invalid character excluded from alphabet due to clarity 160 | assert_raise ArgumentError, fn -> 161 | B58.decode58!("OCn8eVZg") 162 | end 163 | #base16 encoded string 164 | assert_raise ArgumentError, fn -> 165 | "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 166 | |> B58.decode58!() 167 | end 168 | end 169 | 170 | test "decode58/1 Base58 handles invalid binaries when using the bitcoin alphabet" do 171 | #invalid character 172 | {:error, _} = B58.decode58("~") 173 | #invalid leading character 174 | {:error, _} = B58.decode58("~Cn8eVZg") 175 | #invalid trailing character 176 | {:error, _} = B58.decode58("Cn8eVZg^") 177 | #invalid character mid string 178 | {:error, _} = B58.decode58("Cn8%eVZg") 179 | #invalid character excluded from alphabet due to clarity 180 | {:error, _} = B58.decode58("OCn8eVZg") 181 | #base16 encoded string 182 | {:error, _} = "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 183 | |> B58.decode58() 184 | end 185 | 186 | test "decode58_check!/1 decodes Base58Check encoded binaries using the bitcoin alphabet" do 187 | assert B58.decode58_check!("12L5B5yqsf7vwb") == {"hello", <<0>>} 188 | assert B58.decode58_check!("B5oSH5yUDQS9XwzogrcWP") == {"hello world", <<1>>} 189 | assert B58.decode58_check!("YXMkDYBSTEWVuE2vQhQ1nc") == {"Hello World", <<255>>} 190 | assert B58.decode58_check!("1Wh4bh") == {<<>>, <<0>>} 191 | end 192 | 193 | test "decode58_check/1 decodes Base58Check encoded binaries using the bitcoin alphabet" do 194 | assert B58.decode58_check("12L5B5yqsf7vwb") == {:ok, {"hello", <<0>>}} 195 | assert B58.decode58_check("B5oSH5yUDQS9XwzogrcWP") == {:ok, {"hello world", <<1>>}} 196 | assert B58.decode58_check("YXMkDYBSTEWVuE2vQhQ1nc") == {:ok, {"Hello World", <<255>>}} 197 | assert B58.decode58_check("1Wh4bh") == {:ok, {<<>>, <<0>>}} 198 | end 199 | 200 | test "decode58_check!/1 Base58Check decodes to a RIPEMD-160 encoded hash" do 201 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 202 | {hash_bin, version} = "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs" 203 | |> B58.decode58_check!() 204 | assert version == <<0>> 205 | assert hash_bin 206 | |> Base.encode16(case: :lower) == "f54a5851e9372b87810a8e60cdd2e7cfd80b6e31" 207 | end 208 | 209 | test "decode58_check!/1 handles binaries with invalid checksums encoded using the bitcoin alphabet" do 210 | #corrupt last byte 211 | assert_raise ArgumentError, fn -> B58.decode58_check!("12L5B5yqsf7vwc") end 212 | #corrupt first byte 213 | assert_raise ArgumentError, fn -> B58.decode58_check!("D5oSH5yUDQS9XwzogrcWP") end 214 | #corrupt middle byte 215 | assert_raise ArgumentError, fn -> B58.decode58_check!("YXMkDYBSTEWWuE2vQhQ1nc") end 216 | #corrupted empty 217 | assert_raise ArgumentError, fn -> B58.decode58_check!("1Wi4bh") end 218 | end 219 | 220 | test "decode58_check/1 handles binaries with invalid checksums encoded using the bitcoin alphabet" do 221 | #corrupt last byte 222 | {:error, _} = B58.decode58_check("12L5B5yqsf7vwc") 223 | #corrupt first byte 224 | {:error, _} = B58.decode58_check("D5oSH5yUDQS9XwzogrcWP") 225 | #corrupt middle byte 226 | {:error, _} = B58.decode58_check("YXMkDYBSTEWWuE2vQhQ1nc") 227 | #corrupted empty 228 | {:error, _} = B58.decode58_check("1Wi4bh") 229 | end 230 | 231 | test "decode58_check!/1 handles invalid binaries when using encoding using the bitcoin alphabet" do 232 | # Zero is not in this alphabet 233 | assert_raise ArgumentError, fn -> B58.decode58_check!("012L5B5yqsf7vwb") end 234 | # Undescore is not in this alphabet 235 | assert_raise ArgumentError, fn -> B58.decode58_check!("B5oSH5yUDQS9XwzogrcW_") end 236 | # Base64 alphabet is not compatible 237 | assert_raise ArgumentError, fn -> 238 | "Hello World" 239 | |> Base.encode64() 240 | |> B58.decode58_check!() 241 | end 242 | # missing bytes 243 | assert_raise ArgumentError, fn -> B58.decode58_check!("1Wh4b") end 244 | assert_raise ArgumentError, fn -> B58.decode58_check!(<<>>) end 245 | end 246 | 247 | test "decode58_check/1 handles invalid binaries when using encoding using the bitcoin alphabet" do 248 | # Zero is not in this alphabet 249 | {:error, _} = B58.decode58_check("012L5B5yqsf7vwb") 250 | # Underscore is not in this alphabet 251 | {:error, _} = B58.decode58_check("B5oSH5yUDQS9XwzogrcW_") 252 | # Base64 alphabet is not compatible 253 | {:error, _} = "Hello World" 254 | |> Base.encode64() 255 | |> B58.decode58_check() 256 | # missing bytes 257 | {:error, _} = B58.decode58_check("1Wh4b") 258 | {:error, _} = B58.decode58_check(<<>>) 259 | end 260 | 261 | test "version_decode58_check!/1 decodes Base58Check encoded binaries using the bitcoin alphabet" do 262 | assert B58.version_decode58_check!("12L5B5yqsf7vwb") == <<0, "hello">> 263 | assert B58.version_decode58_check!("B5oSH5yUDQS9XwzogrcWP") == <<1, "hello world">> 264 | assert B58.version_decode58_check!("YXMkDYBSTEWVuE2vQhQ1nc") == <<255, "Hello World">> 265 | assert B58.version_decode58_check!("1Wh4bh") == <<0>> 266 | end 267 | 268 | test "version_decode58_check/1 decodes Base58Check encoded binaries using the bitcoin alphabet" do 269 | assert B58.version_decode58_check("12L5B5yqsf7vwb") == {:ok, <<0, "hello">>} 270 | assert B58.version_decode58_check("B5oSH5yUDQS9XwzogrcWP") == {:ok, <<1, "hello world">>} 271 | assert B58.version_decode58_check("YXMkDYBSTEWVuE2vQhQ1nc") == {:ok, <<255, "Hello World">>} 272 | assert B58.version_decode58_check("1Wh4bh") == {:ok, <<0>>} 273 | end 274 | 275 | test "version_decode58_check!/1 Base58Check decodes to a versioned RIPEMD-160 encoded hash" do 276 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 277 | assert "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs" 278 | |> B58.version_decode58_check!() 279 | |> :binary.decode_unsigned() == 0x0f54a5851e9372b87810a8e60cdd2e7cfd80b6e31 280 | end 281 | 282 | test "version_decode58_check!/1 handles binaries with invalid checksums encoded using the bitcoin alphabet" do 283 | #corrupt last byte 284 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("12L5B5yqsf7vwc") end 285 | #corrupt first byte 286 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("D5oSH5yUDQS9XwzogrcWP") end 287 | #corrupt middle byte 288 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("YXMkDYBSTEWWuE2vQhQ1nc") end 289 | #corrupted empty 290 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("1Wi4bh") end 291 | end 292 | 293 | test "version_decode58_check/1 handles binaries with invalid checksums encoded using the bitcoin alphabet" do 294 | #corrupt last byte 295 | {:error, _} = B58.version_decode58_check("12L5B5yqsf7vwc") 296 | #corrupt first byte 297 | {:error, _} = B58.version_decode58_check("D5oSH5yUDQS9XwzogrcWP") 298 | #corrupt middle byte 299 | {:error, _} = B58.version_decode58_check("YXMkDYBSTEWWuE2vQhQ1nc") 300 | #corrupted empty 301 | {:error, _} = B58.version_decode58_check("1Wi4bh") 302 | end 303 | 304 | test "version_decode58_check!/1 handles invalid binaries when using encoding using the bitcoin alphabet" do 305 | # Zero is not in this alphabet 306 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("012L5B5yqsf7vwb") end 307 | # Undescore is not in this alphabet 308 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("B5oSH5yUDQS9XwzogrcW_") end 309 | # Base64 alphabet is not compatible 310 | assert_raise ArgumentError, fn -> 311 | "Hello World" 312 | |> Base.encode64() 313 | |> B58.version_decode58_check!() 314 | end 315 | # missing bytes 316 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("1Wh4b") end 317 | assert_raise ArgumentError, fn -> B58.version_decode58_check!(<<>>) end 318 | end 319 | 320 | test "version_decode58_check/1 handles invalid binaries when using encoding using the bitcoin alphabet" do 321 | # Zero is not in this alphabet 322 | {:error, _} = B58.version_decode58_check("012L5B5yqsf7vwb") 323 | # Underscore is not in this alphabet 324 | {:error, _} = B58.version_decode58_check("B5oSH5yUDQS9XwzogrcW_") 325 | # Base64 alphabet is not compatible 326 | {:error, _} = "Hello World" 327 | |> Base.encode64() 328 | |> B58.version_decode58_check() 329 | # missing bytes 330 | {:error, _} = B58.version_decode58_check("1Wh4b") 331 | {:error, _} = B58.version_decode58_check(<<>>) 332 | end 333 | 334 | # ============================================================================ 335 | # Bitcoin 336 | # ============================================================================ 337 | test "encode58/2 Base58 encodes strings according to the bitcoin alphabet" do 338 | assert B58.encode58("hello", alphabet: :btc) == "Cn8eVZg" 339 | assert B58.encode58("hello world", alphabet: :btc) == "StV1DL6CwTryKyV" 340 | assert B58.encode58("Hello World") == "JxF12TrwUP45BMd" 341 | assert B58.encode58(<<>>, alphabet: :btc) == <<>> 342 | end 343 | 344 | test "encode58/2 Base58 encodes sha-256 strings according to the bitcoin alphabet" do 345 | # From https://github.com/multiformats/multihash 346 | assert "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 347 | |> Base.decode16!() 348 | |> B58.encode58(alphabet: :btc) == "QmYtUc4iTCbbfVSDNKvtQqrfyezPPnFvE33wFmutw9PBBk" 349 | end 350 | 351 | test "encode58/2 handles encodes leading zeroes for the bitcoin alphabet" do 352 | assert B58.encode58(<<0>>, alphabet: :btc) == "1" 353 | assert B58.encode58(<<0, 0, 0, "hello world">>, alphabet: :btc) == "111StV1DL6CwTryKyV" 354 | assert B58.encode58(<<0, 0, 0>>, alphabet: :btc) == "111" 355 | end 356 | 357 | test "encode58_check!/3 accepts only unsigned single byte integers using the bitcoin alphabet" do 358 | assert B58.encode58_check!("a", <<0>>, alphabet: :btc) == "1C3t9Nib" 359 | assert B58.encode58_check!("a", 0, alphabet: :btc) == "1C3t9Nib" 360 | assert B58.encode58_check!("a", <<1>>, alphabet: :btc) == "gqkwoXD" 361 | assert B58.encode58_check!("a", 1, alphabet: :btc) == "gqkwoXD" 362 | assert B58.encode58_check!("a", <<2>>, alphabet: :btc) == "2BkJbhjW" 363 | assert B58.encode58_check!("a", 2, alphabet: :btc) == "2BkJbhjW" 364 | assert B58.encode58_check!("a", <<255>>, alphabet: :btc) == "3CAw3RMCe" 365 | assert B58.encode58_check!("a", 255, alphabet: :btc) == "3CAw3RMCe" 366 | assert B58.encode58_check!(<<>>, <<2>>, alphabet: :btc) == "Epi3KP" 367 | assert B58.encode58_check!(<<>>, 2, alphabet: :btc) == "Epi3KP" 368 | assert_raise ArgumentError, fn -> 369 | B58.encode58_check!("a", 256, alphabet: :btc) 370 | end 371 | assert_raise ArgumentError, fn -> 372 | B58.encode58_check!("a", -1, alphabet: :btc) 373 | end 374 | assert_raise ArgumentError, fn -> 375 | B58.encode58_check!("a", <<1, 0>>, alphabet: :btc) 376 | end 377 | end 378 | 379 | test "encode58_check!/3 Base58Check encodes strings according to the bitcoin alphabet" do 380 | assert B58.encode58_check!("hello", 0, alphabet: :btc) == "12L5B5yqsf7vwb" 381 | assert B58.encode58_check!("hello world", 1, alphabet: :btc) == "B5oSH5yUDQS9XwzogrcWP" 382 | assert B58.encode58_check!("Hello World", 255, alphabet: :btc) == "YXMkDYBSTEWVuE2vQhQ1nc" 383 | assert B58.encode58_check!(<<>>, 0, alphabet: :btc) == "1Wh4bh" 384 | end 385 | 386 | test "encode58_check/3 Base58Check encodes strings according to the bitcoin alphabet" do 387 | assert B58.encode58_check("hello", 0, alphabet: :btc) == {:ok, "12L5B5yqsf7vwb"} 388 | assert B58.encode58_check("hello world", 1, alphabet: :btc) == {:ok, "B5oSH5yUDQS9XwzogrcWP"} 389 | assert B58.encode58_check("Hello World", 255, alphabet: :btc) == {:ok, "YXMkDYBSTEWVuE2vQhQ1nc"} 390 | assert B58.encode58_check(<<>>, 0, alphabet: :btc) == {:ok, "1Wh4bh"} 391 | end 392 | 393 | test "encode58_check!/3 Base58Check encodes sha-256 strings according to the bitcoin alphabet" do 394 | # From https://github.com/multiformats/multihash 395 | assert "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 396 | |> Base.decode16!() 397 | |> B58.encode58_check!(0, alphabet: :btc) == "13gXk986h9pApW3uNeGAHDqUnYHo8c1oSfHajVDuzPtiSokwhzzeK" 398 | end 399 | 400 | test "encode58_check!/3 Base58Check encodes a RIPEMD-160 hash" do 401 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 402 | assert "f54a5851e9372b87810a8e60cdd2e7cfd80b6e31" 403 | |> Base.decode16!(case: :lower) 404 | |> B58.encode58_check!(0, alphabet: :btc) == "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs" 405 | end 406 | 407 | test "encode58_check!/2 handles Base58 encoding leading zeroes" do 408 | assert B58.encode58_check!(<<0>>, 0, alphabet: :btc) == "112edB6q" 409 | assert B58.encode58_check!(<<0, 0, 0, "hello world">>, 0, alphabet: :btc) == "11113vQB7B6MrGQZaxCrokgx4" 410 | assert B58.encode58_check!(<<0, 0, 0>>, 0, alphabet: :btc) == "11114bdQda" 411 | end 412 | 413 | test "version_encode58_check!/3 accepts unsigned single byte integer versions" do 414 | assert B58.version_encode58_check!(<<0, "a">>, alphabet: :btc) == "1C3t9Nib" 415 | assert B58.version_encode58_check!(<<1, "a">>, alphabet: :btc) == "gqkwoXD" 416 | assert B58.version_encode58_check!(<<2, "a">>, alphabet: :btc) == "2BkJbhjW" 417 | assert B58.version_encode58_check!(<<255, "a">>, alphabet: :btc) == "3CAw3RMCe" 418 | assert B58.version_encode58_check!(<<2>>, alphabet: :btc) == "Epi3KP" 419 | end 420 | 421 | test "version_encode58_check/3 Base58Check encodes strings according to the bitcoin alphabet" do 422 | assert B58.version_encode58_check(<<0, "hello">>, alphabet: :btc) == {:ok, "12L5B5yqsf7vwb"} 423 | assert B58.version_encode58_check(<<1, "hello world">>, alphabet: :btc) == {:ok, "B5oSH5yUDQS9XwzogrcWP"} 424 | assert B58.version_encode58_check(<<255, "Hello World">>, alphabet: :btc) == {:ok, "YXMkDYBSTEWVuE2vQhQ1nc"} 425 | assert B58.version_encode58_check(<<0>>, alphabet: :btc) == {:ok, "1Wh4bh"} 426 | end 427 | 428 | test "version_encode58_check!/3 Base58Check encodes strings according to the bitcoin alphabet" do 429 | assert B58.version_encode58_check!(<<0, "hello">>, alphabet: :btc) == "12L5B5yqsf7vwb" 430 | assert B58.version_encode58_check!(<<1, "hello world">>, alphabet: :btc) == "B5oSH5yUDQS9XwzogrcWP" 431 | assert B58.version_encode58_check!(<<255, "Hello World">>, alphabet: :btc) == "YXMkDYBSTEWVuE2vQhQ1nc" 432 | assert B58.version_encode58_check!(<<0>>, alphabet: :btc) == "1Wh4bh" 433 | end 434 | 435 | test "version_encode58_check!/2 Base58Check encodes a versioned RIPEMD-160 hash" do 436 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 437 | assert 0xf54a5851e9372b87810a8e60cdd2e7cfd80b6e31 438 | |> :binary.encode_unsigned() 439 | |> B58.version_binary(0) 440 | |> B58.version_encode58_check!(alphabet: :btc) == "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs" 441 | end 442 | 443 | test "version_encode58_check!/2 handles Base58 encoding leading zeroes" do 444 | assert B58.version_encode58_check!(<<0, 0>>, alphabet: :btc) == "112edB6q" 445 | assert B58.version_encode58_check!(<<0, 0, 0, 0, "hello world">>, alphabet: :btc) == "11113vQB7B6MrGQZaxCrokgx4" 446 | assert B58.version_encode58_check!(<<0, 0, 0, 0>>, alphabet: :btc) == "11114bdQda" 447 | end 448 | 449 | test "decode58!/2 decodes Base58 encoded binaries using the bitcoin alphabet" do 450 | assert B58.decode58!("Cn8eVZg", alphabet: :btc) == "hello" 451 | assert B58.decode58!("StV1DL6CwTryKyV", alphabet: :btc) == "hello world" 452 | assert B58.decode58!("JxF12TrwUP45BMd", alphabet: :btc) == "Hello World" 453 | assert B58.decode58!(<<>>, alphabet: :btc) == <<>> 454 | end 455 | 456 | test "decode58!/2 handles Base58 encoded with leading zeroes using the bitcoin alphabet" do 457 | assert B58.decode58!("1", alphabet: :btc) == <<0>> 458 | assert B58.decode58!("111StV1DL6CwTryKyV", alphabet: :btc) == <<0, 0, 0, "hello world">> 459 | assert B58.decode58!("111", alphabet: :btc) == <<0, 0, 0>> 460 | end 461 | 462 | test "decode58!/2 Base58 decodes sha-256 strings using the bitcoin alphabet" do 463 | # From https://github.com/multiformats/multihash 464 | assert "QmYtUc4iTCbbfVSDNKvtQqrfyezPPnFvE33wFmutw9PBBk" 465 | |> B58.decode58!(alphabet: :btc) 466 | |> Base.encode16() == "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 467 | end 468 | 469 | test "decode58!/2 Base58 handles invalid binaries when using the bitcoin alphabet" do 470 | #invalid character 471 | assert_raise ArgumentError, fn -> 472 | B58.decode58!("~", alphabet: :btc) 473 | end 474 | #invalid leading character 475 | assert_raise ArgumentError, fn -> 476 | B58.decode58!("~Cn8eVZg", alphabet: :btc) 477 | end 478 | #invalid trailing character 479 | assert_raise ArgumentError, fn -> 480 | B58.decode58!("Cn8eVZg^", alphabet: :btc) 481 | end 482 | #invalid character mid string 483 | assert_raise ArgumentError, fn -> 484 | B58.decode58!("Cn8%eVZg", alphabet: :btc) 485 | end 486 | #invalid character excluded from alphabet due to clarity 487 | assert_raise ArgumentError, fn -> 488 | B58.decode58!("OCn8eVZg", alphabet: :btc) 489 | end 490 | #base16 encoded string 491 | assert_raise ArgumentError, fn -> 492 | "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 493 | |> B58.decode58!(alphabet: :btc) 494 | end 495 | end 496 | 497 | test "decode58/2 Base58 handles invalid binaries when using the bitcoin alphabet" do 498 | #invalid character 499 | {:error, _} = B58.decode58("~", alphabet: :btc) 500 | #invalid leading character 501 | {:error, _} = B58.decode58("~Cn8eVZg", alphabet: :btc) 502 | #invalid trailing character 503 | {:error, _} = B58.decode58("Cn8eVZg^", alphabet: :btc) 504 | #invalid character mid string 505 | {:error, _} = B58.decode58("Cn8%eVZg", alphabet: :btc) 506 | #invalid character excluded from alphabet due to clarity 507 | {:error, _} = B58.decode58("OCn8eVZg", alphabet: :btc) 508 | #base16 encoded string 509 | {:error, _} = "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 510 | |> B58.decode58(alphabet: :btc) 511 | end 512 | 513 | test "decode58_check!/2 decodes Base58Check encoded binaries using the bitcoin alphabet" do 514 | assert B58.decode58_check!("12L5B5yqsf7vwb", alphabet: :btc) == {"hello", <<0>>} 515 | assert B58.decode58_check!("B5oSH5yUDQS9XwzogrcWP", alphabet: :btc) == {"hello world", <<1>>} 516 | assert B58.decode58_check!("YXMkDYBSTEWVuE2vQhQ1nc", alphabet: :btc) == {"Hello World", <<255>>} 517 | assert B58.decode58_check!("1Wh4bh", alphabet: :btc) == {<<>>, <<0>>} 518 | end 519 | 520 | test "decode58_check/2 decodes Base58Check encoded binaries using the bitcoin alphabet" do 521 | assert B58.decode58_check("12L5B5yqsf7vwb", alphabet: :btc) == {:ok, {"hello", <<0>>}} 522 | assert B58.decode58_check("B5oSH5yUDQS9XwzogrcWP", alphabet: :btc) == {:ok, {"hello world", <<1>>}} 523 | assert B58.decode58_check("YXMkDYBSTEWVuE2vQhQ1nc", alphabet: :btc) == {:ok, {"Hello World", <<255>>}} 524 | assert B58.decode58_check("1Wh4bh", alphabet: :btc) == {:ok, {<<>>, <<0>>}} 525 | end 526 | 527 | test "decode58_check!/2 Base58Check decodes to a RIPEMD-160 encoded hash" do 528 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 529 | {hash_bin, version} = "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs" 530 | |> B58.decode58_check!(alphabet: :btc) 531 | assert version == <<0>> 532 | assert hash_bin 533 | |> Base.encode16(case: :lower) == "f54a5851e9372b87810a8e60cdd2e7cfd80b6e31" 534 | end 535 | 536 | test "decode58_check!/2 handles binaries with invalid checksums encoded using the bitcoin alphabet" do 537 | #corrupt last byte 538 | assert_raise ArgumentError, fn -> B58.decode58_check!("12L5B5yqsf7vwc", alphabet: :btc) end 539 | #corrupt first byte 540 | assert_raise ArgumentError, fn -> B58.decode58_check!("D5oSH5yUDQS9XwzogrcWP", alphabet: :btc) end 541 | #corrupt middle byte 542 | assert_raise ArgumentError, fn -> B58.decode58_check!("YXMkDYBSTEWWuE2vQhQ1nc", alphabet: :btc) end 543 | #corrupted empty 544 | assert_raise ArgumentError, fn -> B58.decode58_check!("1Wi4bh", alphabet: :btc) end 545 | end 546 | 547 | test "decode58_check/2 handles binaries with invalid checksums encoded using the bitcoin alphabet" do 548 | #corrupt last byte 549 | {:error, _} = B58.decode58_check("12L5B5yqsf7vwc", alphabet: :btc) 550 | #corrupt first byte 551 | {:error, _} = B58.decode58_check("D5oSH5yUDQS9XwzogrcWP", alphabet: :btc) 552 | #corrupt middle byte 553 | {:error, _} = B58.decode58_check("YXMkDYBSTEWWuE2vQhQ1nc", alphabet: :btc) 554 | #corrupted empty 555 | {:error, _} = B58.decode58_check("1Wi4bh", alphabet: :btc) 556 | end 557 | 558 | test "decode58_check!/2 handles invalid binaries when using encoding using the bitcoin alphabet" do 559 | # Zero is not in this alphabet 560 | assert_raise ArgumentError, fn -> B58.decode58_check!("012L5B5yqsf7vwb", alphabet: :btc) end 561 | # Underscore is not in this alphabet 562 | assert_raise ArgumentError, fn -> B58.decode58_check!("B5oSH5yUDQS9XwzogrcW_", alphabet: :btc) end 563 | # Base64 alphabet is not compatible 564 | assert_raise ArgumentError, fn -> 565 | "Hello World" 566 | |> Base.encode64() 567 | |> B58.decode58_check!(alphabet: :btc) 568 | end 569 | # missing bytes 570 | assert_raise ArgumentError, fn -> B58.decode58_check!("1Wh4b", alphabet: :btc) end 571 | assert_raise ArgumentError, fn -> B58.decode58_check!(<<>>, alphabet: :btc) end 572 | end 573 | 574 | test "decode58_check/2 handles invalid binaries when using encoding using the bitcoin alphabet" do 575 | # Zero is not in this alphabet 576 | {:error, _} = B58.decode58_check("012L5B5yqsf7vwb", alphabet: :btc) 577 | # Underscore is not in this alphabet 578 | {:error, _} = B58.decode58_check("B5oSH5yUDQS9XwzogrcW_", alphabet: :btc) 579 | # Base64 alphabet is not compatible 580 | {:error, _} = "Hello World" 581 | |> Base.encode64() 582 | |> B58.decode58_check(alphabet: :btc) 583 | # missing bytes 584 | {:error, _} = B58.decode58_check("1Wh4b", alphabet: :btc) 585 | {:error, _} = B58.decode58_check(<<>>, alphabet: :btc) 586 | end 587 | 588 | test "version_decode58_check!/2 decodes Base58Check encoded binaries using the bitcoin alphabet" do 589 | assert B58.version_decode58_check!("12L5B5yqsf7vwb", alphabet: :btc) == <<0, "hello">> 590 | assert B58.version_decode58_check!("B5oSH5yUDQS9XwzogrcWP", alphabet: :btc) == <<1, "hello world">> 591 | assert B58.version_decode58_check!("YXMkDYBSTEWVuE2vQhQ1nc", alphabet: :btc) == <<255, "Hello World">> 592 | assert B58.version_decode58_check!("1Wh4bh", alphabet: :btc) == <<0>> 593 | end 594 | 595 | test "version_decode58_check/2 decodes Base58Check encoded binaries using the bitcoin alphabet" do 596 | assert B58.version_decode58_check("12L5B5yqsf7vwb", alphabet: :btc) == {:ok, <<0, "hello">>} 597 | assert B58.version_decode58_check("B5oSH5yUDQS9XwzogrcWP", alphabet: :btc) == {:ok, <<1, "hello world">>} 598 | assert B58.version_decode58_check("YXMkDYBSTEWVuE2vQhQ1nc", alphabet: :btc) == {:ok, <<255, "Hello World">>} 599 | assert B58.version_decode58_check("1Wh4bh", alphabet: :btc) == {:ok, <<0>>} 600 | end 601 | 602 | test "version_decode58_check!/2 Base58Check decodes to a versioned RIPEMD-160 encoded hash" do 603 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 604 | assert "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs" 605 | |> B58.version_decode58_check!(alphabet: :btc) 606 | |> :binary.decode_unsigned() == 0x0f54a5851e9372b87810a8e60cdd2e7cfd80b6e31 607 | end 608 | 609 | test "version_decode58_check!/2 handles binaries with invalid checksums encoded using the bitcoin alphabet" do 610 | #corrupt last byte 611 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("12L5B5yqsf7vwc", alphabet: :btc) end 612 | #corrupt first byte 613 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("D5oSH5yUDQS9XwzogrcWP", alphabet: :btc) end 614 | #corrupt middle byte 615 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("YXMkDYBSTEWWuE2vQhQ1nc", alphabet: :btc) end 616 | #corrupted empty 617 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("1Wi4bh", alphabet: :btc) end 618 | end 619 | 620 | test "version_decode58_check/2 handles binaries with invalid checksums encoded using the bitcoin alphabet" do 621 | #corrupt last byte 622 | {:error, _} = B58.version_decode58_check("12L5B5yqsf7vwc", alphabet: :btc) 623 | #corrupt first byte 624 | {:error, _} = B58.version_decode58_check("D5oSH5yUDQS9XwzogrcWP", alphabet: :btc) 625 | #corrupt middle byte 626 | {:error, _} = B58.version_decode58_check("YXMkDYBSTEWWuE2vQhQ1nc", alphabet: :btc) 627 | #corrupted empty 628 | {:error, _} = B58.version_decode58_check("1Wi4bh", alphabet: :btc) 629 | end 630 | 631 | test "version_decode58_check!/2 handles invalid binaries when using encoding using the bitcoin alphabet" do 632 | # Zero is not in this alphabet 633 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("012L5B5yqsf7vwb", alphabet: :btc) end 634 | # Underscore is not in this alphabet 635 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("B5oSH5yUDQS9XwzogrcW_", alphabet: :btc) end 636 | # Base64 alphabet is not compatible 637 | assert_raise ArgumentError, fn -> 638 | "Hello World" 639 | |> Base.encode64() 640 | |> B58.version_decode58_check!(alphabet: :btc) 641 | end 642 | # missing bytes 643 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("1Wh4b", alphabet: :btc) end 644 | assert_raise ArgumentError, fn -> B58.version_decode58_check!(<<>>, alphabet: :btc) end 645 | end 646 | 647 | test "version_decode58_check/2 handles invalid binaries when using encoding using the bitcoin alphabet" do 648 | # Zero is not in this alphabet 649 | {:error, _} = B58.version_decode58_check("012L5B5yqsf7vwb", alphabet: :btc) 650 | # Underscore is not in this alphabet 651 | {:error, _} = B58.version_decode58_check("B5oSH5yUDQS9XwzogrcW_", alphabet: :btc) 652 | # Base64 alphabet is not compatible 653 | {:error, _} = "Hello World" 654 | |> Base.encode64() 655 | |> B58.version_decode58_check(alphabet: :btc) 656 | # missing bytes 657 | {:error, _} = B58.version_decode58_check("1Wh4b", alphabet: :btc) 658 | {:error, _} = B58.version_decode58_check(<<>>, alphabet: :btc) 659 | end 660 | 661 | 662 | # ============================================================================ 663 | # Flickr 664 | # ============================================================================ 665 | test "encode58/2 Base58 encodes strings according to the flickr alphabet" do 666 | assert B58.encode58("hello", alphabet: :flickr) == "cM8DuyF" 667 | assert B58.encode58("hello world", alphabet: :flickr) == "rTu1dk6cWsRYjYu" 668 | assert B58.encode58("Hello World", alphabet: :flickr) == "iXf12sRWto45bmC" 669 | assert B58.encode58(<<>>, alphabet: :flickr) == <<>> 670 | end 671 | 672 | test "encode58/2 Base58 encodes sha-256 strings according to the flickr alphabet" do 673 | # From https://github.com/multiformats/multihash 674 | assert "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 675 | |> Base.decode16!() 676 | |> B58.encode58(alphabet: :flickr) == "pLxTtB4HscAAEurdnjVTpQREYDZooMfVe33WfLUTW9obbK" 677 | end 678 | 679 | test "encode58/2 handles encodes leading zeroes for the flickr alphabet" do 680 | assert B58.encode58(<<0>>, alphabet: :flickr) == "1" 681 | assert B58.encode58(<<0, 0, 0, "hello world">>, alphabet: :flickr) == "111rTu1dk6cWsRYjYu" 682 | assert B58.encode58(<<0, 0, 0>>, alphabet: :flickr) == "111" 683 | end 684 | 685 | test "encode58_check!/3 accepts only unsigned single byte integers using the flickr alphabet" do 686 | assert B58.encode58_check!("a", <<0>>, alphabet: :flickr) == "1c3T9nHA" 687 | assert B58.encode58_check!("a", 0, alphabet: :flickr) == "1c3T9nHA" 688 | assert B58.encode58_check!("a", <<1>>, alphabet: :flickr) == "FQKWNwd" 689 | assert B58.encode58_check!("a", 1, alphabet: :flickr) == "FQKWNwd" 690 | assert B58.encode58_check!("a", <<2>>, alphabet: :flickr) == "2bKiAGJv" 691 | assert B58.encode58_check!("a", 2, alphabet: :flickr) == "2bKiAGJv" 692 | assert B58.encode58_check!("a", <<255>>, alphabet: :flickr) == "3caW3qmcD" 693 | assert B58.encode58_check!("a", 255, alphabet: :flickr) == "3caW3qmcD" 694 | assert B58.encode58_check!(<<>>, <<2>>, alphabet: :flickr) == "ePH3jo" 695 | assert B58.encode58_check!(<<>>, 2, alphabet: :flickr) == "ePH3jo" 696 | assert_raise ArgumentError, fn -> 697 | B58.encode58_check!("a", 256, alphabet: :flickr) 698 | end 699 | assert_raise ArgumentError, fn -> 700 | B58.encode58_check!("a", -1, alphabet: :flickr) 701 | end 702 | assert_raise ArgumentError, fn -> 703 | B58.encode58_check!("a", <<1, 0>>, alphabet: :flickr) 704 | end 705 | end 706 | 707 | test "encode58_check!/3 Base58Check encodes strings according to the flickr alphabet" do 708 | assert B58.encode58_check!("hello", 0, alphabet: :flickr) == "12k5b5YQSE7VWA" 709 | assert B58.encode58_check!("hello world", 1, alphabet: :flickr) == "b5Nrh5Ytdpr9wWZNFRBvo" 710 | assert B58.encode58_check!("Hello World", 255, alphabet: :flickr) == "xwmKdxbrsevuUe2VpGp1MB" 711 | assert B58.encode58_check!(<<>>, 0, alphabet: :flickr) == "1vG4AG" 712 | end 713 | 714 | test "encode58_check/3 Base58Check encodes strings according to the flickr alphabet" do 715 | assert B58.encode58_check("hello", 0, alphabet: :flickr) == {:ok, "12k5b5YQSE7VWA"} 716 | assert B58.encode58_check("hello world", 1, alphabet: :flickr) == {:ok, "b5Nrh5Ytdpr9wWZNFRBvo"} 717 | assert B58.encode58_check("Hello World", 255, alphabet: :flickr) == {:ok, "xwmKdxbrsevuUe2VpGp1MB"} 718 | assert B58.encode58_check(<<>>, 0, alphabet: :flickr) == {:ok, "1vG4AG"} 719 | end 720 | 721 | test "encode58_check!/3 Base58Check encodes sha-256 strings according to the flickr alphabet" do 722 | # From https://github.com/multiformats/multihash 723 | assert "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 724 | |> Base.decode16!() 725 | |> B58.encode58_check!(0, alphabet: :flickr) == "13FwK986G9PaPv3UnDgahdQtMxhN8B1NrEhzJudUZoTHrNKWGZZDj" 726 | end 727 | 728 | test "encode58_check!/3 Base58Check encodes a RIPEMD-160 hash using the flickr alphabet" do 729 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 730 | assert "f54a5851e9372b87810a8e60cdd2e7cfd80b6e31" 731 | |> Base.decode16!(case: :lower) 732 | |> B58.encode58_check!(0, alphabet: :flickr) == "1omYBzBMizrQWWiQJzWwbeRMkSy7qKwtaS" 733 | end 734 | 735 | test "encode58_check!/2 handles Base58 encoding leading zeroes using the flickr alphabet" do 736 | assert B58.encode58_check!(<<0>>, 0, alphabet: :flickr) == "112DCb6Q" 737 | assert B58.encode58_check!(<<0, 0, 0, "hello world">>, 0, alphabet: :flickr) == "11113Vpb7b6mRgpyzXcRNKFX4" 738 | assert B58.encode58_check!(<<0, 0, 0>>, 0, alphabet: :flickr) == "11114ACpCz" 739 | end 740 | 741 | test "version_encode58_check!/3 accepts unsigned single byte integer versions using the flickr alphabet" do 742 | assert B58.version_encode58_check!(<<0, "a">>, alphabet: :flickr) == "1c3T9nHA" 743 | assert B58.version_encode58_check!(<<1, "a">>, alphabet: :flickr) == "FQKWNwd" 744 | assert B58.version_encode58_check!(<<2, "a">>, alphabet: :flickr) == "2bKiAGJv" 745 | assert B58.version_encode58_check!(<<255, "a">>, alphabet: :flickr) == "3caW3qmcD" 746 | assert B58.version_encode58_check!(<<2>>, alphabet: :flickr) == "ePH3jo" 747 | end 748 | 749 | test "version_encode58_check/3 Base58Check encodes strings according to the flickr alphabet" do 750 | assert B58.version_encode58_check(<<0, "hello">>, alphabet: :flickr) == {:ok, "12k5b5YQSE7VWA"} 751 | assert B58.version_encode58_check(<<1, "hello world">>, alphabet: :flickr) == {:ok, "b5Nrh5Ytdpr9wWZNFRBvo"} 752 | assert B58.version_encode58_check(<<255, "Hello World">>, alphabet: :flickr) == {:ok, "xwmKdxbrsevuUe2VpGp1MB"} 753 | assert B58.version_encode58_check(<<0>>, alphabet: :flickr) == {:ok, "1vG4AG"} 754 | end 755 | 756 | test "version_encode58_check!/3 Base58Check encodes strings according to the flickr alphabet" do 757 | assert B58.version_encode58_check!(<<0, "hello">>, alphabet: :flickr) == "12k5b5YQSE7VWA" 758 | assert B58.version_encode58_check!(<<1, "hello world">>, alphabet: :flickr) == "b5Nrh5Ytdpr9wWZNFRBvo" 759 | assert B58.version_encode58_check!(<<255, "Hello World">>, alphabet: :flickr) == "xwmKdxbrsevuUe2VpGp1MB" 760 | assert B58.version_encode58_check!(<<0>>, alphabet: :flickr) == "1vG4AG" 761 | end 762 | 763 | test "version_encode58_check!/2 Base58Check encodes a versioned RIPEMD-160 hash using the flickr alphabet" do 764 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 765 | assert 0xf54a5851e9372b87810a8e60cdd2e7cfd80b6e31 766 | |> :binary.encode_unsigned() 767 | |> B58.version_binary(0) 768 | |> B58.version_encode58_check!(alphabet: :flickr) == "1omYBzBMizrQWWiQJzWwbeRMkSy7qKwtaS" 769 | end 770 | 771 | test "version_encode58_check!/2 handles Base58 encoding leading zeroes using the flickr alphabet" do 772 | assert B58.version_encode58_check!(<<0, 0>>, alphabet: :flickr) == "112DCb6Q" 773 | assert B58.version_encode58_check!(<<0, 0, 0, 0, "hello world">>, alphabet: :flickr) == "11113Vpb7b6mRgpyzXcRNKFX4" 774 | assert B58.version_encode58_check!(<<0, 0, 0, 0>>, alphabet: :flickr) == "11114ACpCz" 775 | end 776 | 777 | test "decode58!/2 decodes Base58 encoded binaries using the flickr alphabet" do 778 | assert B58.decode58!("cM8DuyF", alphabet: :flickr) == "hello" 779 | assert B58.decode58!("rTu1dk6cWsRYjYu", alphabet: :flickr) == "hello world" 780 | assert B58.decode58!("iXf12sRWto45bmC", alphabet: :flickr) == "Hello World" 781 | assert B58.decode58!(<<>>, alphabet: :flickr) == <<>> 782 | end 783 | 784 | test "decode58!/2 handles Base58 encoded with leading zeroes using the flickr alphabet" do 785 | assert B58.decode58!("1", alphabet: :flickr) == <<0>> 786 | assert B58.decode58!("111rTu1dk6cWsRYjYu", alphabet: :flickr) == <<0, 0, 0, "hello world">> 787 | assert B58.decode58!("111", alphabet: :flickr) == <<0, 0, 0>> 788 | end 789 | 790 | test "decode58!/2 Base58 decodes sha-256 strings using the flickr alphabet" do 791 | # From https://github.com/multiformats/multihash 792 | assert "pLxTtB4HscAAEurdnjVTpQREYDZooMfVe33WfLUTW9obbK" 793 | |> B58.decode58!(alphabet: :flickr) 794 | |> Base.encode16() == "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 795 | end 796 | 797 | test "decode58!/2 Base58 handles invalid binaries when using the flickr alphabet" do 798 | #invalid character 799 | assert_raise ArgumentError, fn -> 800 | B58.decode58!("~", alphabet: :flickr) 801 | end 802 | #invalid leading character 803 | assert_raise ArgumentError, fn -> 804 | B58.decode58!("~Cn8eVZg", alphabet: :flickr) 805 | end 806 | #invalid trailing character 807 | assert_raise ArgumentError, fn -> 808 | B58.decode58!("Cn8eVZg^", alphabet: :flickr) 809 | end 810 | #invalid character mid string 811 | assert_raise ArgumentError, fn -> 812 | B58.decode58!("Cn8%eVZg", alphabet: :flickr) 813 | end 814 | #invalid character excluded from alphabet due to clarity 815 | assert_raise ArgumentError, fn -> 816 | B58.decode58!("OCn8eVZg", alphabet: :flickr) 817 | end 818 | #base16 encoded string 819 | assert_raise ArgumentError, fn -> 820 | "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 821 | |> B58.decode58!(alphabet: :flickr) 822 | end 823 | end 824 | 825 | test "decode58/2 Base58 handles invalid binaries when using the flickr alphabet" do 826 | #invalid character 827 | {:error, _} = B58.decode58("~", alphabet: :flickr) 828 | #invalid leading character 829 | {:error, _} = B58.decode58("~cM8DuyF", alphabet: :flickr) 830 | #invalid trailing character 831 | {:error, _} = B58.decode58("cM8DuyF^", alphabet: :flickr) 832 | #invalid character mid string 833 | {:error, _} = B58.decode58("cM%DuyF", alphabet: :flickr) 834 | #invalid character excluded from alphabet due to clarity 835 | {:error, _} = B58.decode58("OcM8DuyF", alphabet: :flickr) 836 | #base16 encoded string 837 | {:error, _} = "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 838 | |> B58.decode58(alphabet: :flickr) 839 | end 840 | 841 | test "decode58_check!/2 decodes Base58Check encoded binaries using the flickr alphabet" do 842 | assert B58.decode58_check!("12k5b5YQSE7VWA", alphabet: :flickr) == {"hello", <<0>>} 843 | assert B58.decode58_check!("b5Nrh5Ytdpr9wWZNFRBvo", alphabet: :flickr) == {"hello world", <<1>>} 844 | assert B58.decode58_check!("xwmKdxbrsevuUe2VpGp1MB", alphabet: :flickr) == {"Hello World", <<255>>} 845 | assert B58.decode58_check!("1vG4AG", alphabet: :flickr) == {<<>>, <<0>>} 846 | end 847 | 848 | test "decode58_check/2 decodes Base58Check encoded binaries using the flickr alphabet" do 849 | assert B58.decode58_check("12k5b5YQSE7VWA", alphabet: :flickr) == {:ok, {"hello", <<0>>}} 850 | assert B58.decode58_check("b5Nrh5Ytdpr9wWZNFRBvo", alphabet: :flickr) == {:ok, {"hello world", <<1>>}} 851 | assert B58.decode58_check("xwmKdxbrsevuUe2VpGp1MB", alphabet: :flickr) == {:ok, {"Hello World", <<255>>}} 852 | assert B58.decode58_check("1vG4AG", alphabet: :flickr) == {:ok, {<<>>, <<0>>}} 853 | end 854 | 855 | test "decode58_check!/2 Base58Check decodes to a RIPEMD-160 encoded hash using the flickr alphabet" do 856 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 857 | {hash_bin, version} = "1omYBzBMizrQWWiQJzWwbeRMkSy7qKwtaS" 858 | |> B58.decode58_check!(alphabet: :flickr) 859 | assert version == <<0>> 860 | assert hash_bin 861 | |> Base.encode16(case: :lower) == "f54a5851e9372b87810a8e60cdd2e7cfd80b6e31" 862 | end 863 | 864 | test "decode58_check!/2 handles binaries with invalid checksums encoded using the flickr alphabet" do 865 | #corrupt last byte 866 | assert_raise ArgumentError, fn -> B58.decode58_check!("12k5b5YQSE7VWB", alphabet: :flickr) end 867 | #corrupt first byte 868 | assert_raise ArgumentError, fn -> B58.decode58_check!("x5Nrh5Ytdpr9wWZNFRBvo", alphabet: :flickr) end 869 | #corrupt middle byte 870 | assert_raise ArgumentError, fn -> B58.decode58_check!("xwmKdxbrssvuUe2VpGp1MB", alphabet: :flickr) end 871 | #corrupted empty 872 | assert_raise ArgumentError, fn -> B58.decode58_check!("1vG4AB", alphabet: :flickr) end 873 | end 874 | 875 | test "decode58_check/2 handles binaries with invalid checksums encoded using the flickr alphabet" do 876 | #corrupt last byte 877 | {:error, _} = B58.decode58_check("12k5b5YQSE7VWB", alphabet: :flickr) 878 | #corrupt first byte 879 | {:error, _} = B58.decode58_check("x5Nrh5Ytdpr9wWZNFRBvo", alphabet: :flickr) 880 | #corrupt middle byte 881 | {:error, _} = B58.decode58_check("xwmKdxbrssvuUe2VpGp1MB", alphabet: :flickr) 882 | #corrupted empty 883 | {:error, _} = B58.decode58_check("1vG4AB", alphabet: :flickr) 884 | end 885 | 886 | test "decode58_check!/2 handles invalid binaries when using encoding using the flickr alphabet" do 887 | # Zero is not in this alphabet 888 | assert_raise ArgumentError, fn -> B58.decode58_check!("02k5b5YQSE7VWA", alphabet: :flickr) end 889 | # Underscore is not in this alphabet 890 | assert_raise ArgumentError, fn -> B58.decode58_check!("b5Nrh5Ytdpr9wWZNFRBvo_", alphabet: :flickr) end 891 | # Base64 alphabet is not compatible 892 | assert_raise ArgumentError, fn -> 893 | "Hello World" 894 | |> Base.encode64() 895 | |> B58.decode58_check!(alphabet: :flickr) 896 | end 897 | # missing bytes 898 | assert_raise ArgumentError, fn -> B58.decode58_check!("1v4AG", alphabet: :flickr) end 899 | assert_raise ArgumentError, fn -> B58.decode58_check!(<<>>, alphabet: :flickr) end 900 | end 901 | 902 | test "decode58_check/2 handles invalid binaries when using encoding using the flickr alphabet" do 903 | # Zero is not in this alphabet 904 | {:error, _} = B58.decode58_check("02k5b5YQSE7VWA", alphabet: :flickr) 905 | # Underscore is not in this alphabet 906 | {:error, _} = B58.decode58_check("b5Nrh5Ytdpr9wWZNFRBvo_", alphabet: :flickr) 907 | # Base64 alphabet is not compatible 908 | {:error, _} = "Hello World" 909 | |> Base.encode64() 910 | |> B58.decode58_check(alphabet: :flickr) 911 | # missing bytes 912 | {:error, _} = B58.decode58_check("1v4AG", alphabet: :flickr) 913 | {:error, _} = B58.decode58_check(<<>>, alphabet: :flickr) 914 | end 915 | 916 | test "version_decode58_check!/2 decodes Base58Check encoded binaries using the flickr alphabet" do 917 | assert B58.version_decode58_check!("12k5b5YQSE7VWA", alphabet: :flickr) == <<0, "hello">> 918 | assert B58.version_decode58_check!("b5Nrh5Ytdpr9wWZNFRBvo", alphabet: :flickr) == <<1, "hello world">> 919 | assert B58.version_decode58_check!("xwmKdxbrsevuUe2VpGp1MB", alphabet: :flickr) == <<255, "Hello World">> 920 | assert B58.version_decode58_check!("1vG4AG", alphabet: :flickr) == <<0>> 921 | end 922 | 923 | test "version_decode58_check/2 decodes Base58Check encoded binaries using the flickr alphabet" do 924 | assert B58.version_decode58_check("12k5b5YQSE7VWA", alphabet: :flickr) == {:ok, <<0, "hello">>} 925 | assert B58.version_decode58_check("b5Nrh5Ytdpr9wWZNFRBvo", alphabet: :flickr) == {:ok, <<1, "hello world">>} 926 | assert B58.version_decode58_check("xwmKdxbrsevuUe2VpGp1MB", alphabet: :flickr) == {:ok, <<255, "Hello World">>} 927 | assert B58.version_decode58_check("1vG4AG", alphabet: :flickr) == {:ok, <<0>>} 928 | end 929 | 930 | test "version_decode58_check!/2 Base58Check decodes to a versioned RIPEMD-160 encoded hash using the flickr alphabet" do 931 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 932 | assert "1omYBzBMizrQWWiQJzWwbeRMkSy7qKwtaS" 933 | |> B58.version_decode58_check!(alphabet: :flickr) 934 | |> :binary.decode_unsigned() == 0x0f54a5851e9372b87810a8e60cdd2e7cfd80b6e31 935 | end 936 | 937 | test "version_decode58_check!/2 handles binaries with invalid checksums encoded using the flickr alphabet" do 938 | #corrupt last byte 939 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("12k5b5YQSE7VWB", alphabet: :flickr) end 940 | #corrupt first byte 941 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("r5Nrh5Ytdpr9wWZNFRBvo", alphabet: :flickr) end 942 | #corrupt middle byte 943 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("xwmKdxbrbevuUe2VpGp1MB", alphabet: :flickr) end 944 | #corrupted empty 945 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("1v4AG", alphabet: :flickr) end 946 | end 947 | 948 | test "version_decode58_check/2 handles binaries with invalid checksums encoded using the flickr alphabet" do 949 | #corrupt last byte 950 | {:error, _} = B58.version_decode58_check("12k5b5YQSE7VWB", alphabet: :flickr) 951 | #corrupt first byte 952 | {:error, _} = B58.version_decode58_check("r5Nrh5Ytdpr9wWZNFRBvo", alphabet: :flickr) 953 | #corrupt middle byte 954 | {:error, _} = B58.version_decode58_check("xwmKdxbrbevuUe2VpGp1MB", alphabet: :flickr) 955 | #corrupted empty 956 | {:error, _} = B58.version_decode58_check("1v4AG", alphabet: :flickr) 957 | end 958 | 959 | test "version_decode58_check!/2 handles invalid binaries when using encoding using the flickr alphabet" do 960 | # Zero is not in this alphabet 961 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("02k5b5YQSE7VWB", alphabet: :flickr) end 962 | # Underscore is not in this alphabet 963 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("b5Nrh5Ytdpr9wWZNFRBvo_", alphabet: :flickr) end 964 | # Base64 alphabet is not compatible 965 | assert_raise ArgumentError, fn -> 966 | "Hello World" 967 | |> Base.encode64() 968 | |> B58.version_decode58_check!(alphabet: :flickr) 969 | end 970 | # missing bytes 971 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("1v4AG", alphabet: :flickr) end 972 | assert_raise ArgumentError, fn -> B58.version_decode58_check!(<<>>, alphabet: :flickr) end 973 | end 974 | 975 | test "version_decode58_check/2 handles invalid binaries when using encoding using the flickr alphabet" do 976 | # Zero is not in this alphabet 977 | {:error, _} = B58.version_decode58_check("02k5b5YQSE7VWB", alphabet: :flickr) 978 | # Underscore is not in this alphabet 979 | {:error, _} = B58.version_decode58_check("b5Nrh5Ytdpr9wWZNFRBvo_", alphabet: :flickr) 980 | # Base64 alphabet is not compatible 981 | {:error, _} = "Hello World" 982 | |> Base.encode64() 983 | |> B58.version_decode58_check(alphabet: :flickr) 984 | # missing bytes 985 | {:error, _} = B58.version_decode58_check("1v4AG", alphabet: :flickr) 986 | {:error, _} = B58.version_decode58_check(<<>>, alphabet: :flickr) 987 | end 988 | 989 | 990 | # ============================================================================ 991 | # Ripple 992 | # ============================================================================ 993 | test "encode58/2 Base58 encodes strings according to the ripple alphabet" do 994 | assert B58.encode58("hello", alphabet: :ripple) == "U83eVZg" 995 | assert B58.encode58("hello world", alphabet: :ripple) == "StVrDLaUATiyKyV" 996 | assert B58.encode58("Hello World", alphabet: :ripple) == "JxErpTiA7PhnBMd" 997 | assert B58.encode58(<<>>, alphabet: :ripple) == <<>> 998 | end 999 | 1000 | test "encode58/2 Base58 encodes sha-256 strings according to the ripple alphabet" do 1001 | # From https://github.com/multiformats/multihash 1002 | assert "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 1003 | |> Base.decode16!() 1004 | |> B58.encode58(alphabet: :ripple) == "QmYt7ch5TUbbCVSD4KvtQqiCyezPP8EvNssAEmutA9PBBk" 1005 | end 1006 | 1007 | test "encode58/2 handles encodes leading zeroes for the ripple alphabet" do 1008 | assert B58.encode58(<<0>>, alphabet: :ripple) == "r" 1009 | assert B58.encode58(<<0, 0, 0, "hello world">>, alphabet: :ripple) == "rrrStVrDLaUATiyKyV" 1010 | assert B58.encode58(<<0, 0, 0>>, alphabet: :ripple) == "rrr" 1011 | end 1012 | 1013 | test "encode58_check!/3 accepts only unsigned single byte integers using the ripple alphabet" do 1014 | assert B58.encode58_check!("a", <<0>>, alphabet: :ripple) == "rUst945b" 1015 | assert B58.encode58_check!("a", 0, alphabet: :ripple) == "rUst945b" 1016 | assert B58.encode58_check!("a", <<1>>, alphabet: :ripple) == "gqkAoXD" 1017 | assert B58.encode58_check!("a", 1, alphabet: :ripple) == "gqkAoXD" 1018 | assert B58.encode58_check!("a", <<2>>, alphabet: :ripple) == "pBkJb6jW" 1019 | assert B58.encode58_check!("a", 2, alphabet: :ripple) == "pBkJb6jW" 1020 | assert B58.encode58_check!("a", <<255>>, alphabet: :ripple) == "sUwAsRMUe" 1021 | assert B58.encode58_check!("a", 255, alphabet: :ripple) == "sUwAsRMUe" 1022 | assert B58.encode58_check!(<<>>, <<2>>, alphabet: :ripple) == "NF5sKP" 1023 | assert B58.encode58_check!(<<>>, 2, alphabet: :ripple) == "NF5sKP" 1024 | 1025 | assert_raise ArgumentError, fn -> 1026 | B58.encode58_check!("a", 256, alphabet: :ripple) 1027 | end 1028 | 1029 | assert_raise ArgumentError, fn -> 1030 | B58.encode58_check!("a", -1, alphabet: :ripple) 1031 | end 1032 | 1033 | assert_raise ArgumentError, fn -> 1034 | B58.encode58_check!("a", <<1, 0>>, alphabet: :ripple) 1035 | end 1036 | end 1037 | 1038 | test "encode58_check!/3 Base58Check encodes strings according to the ripple alphabet" do 1039 | assert B58.encode58_check!("hello", 0, alphabet: :ripple) == "rpLnBnyq1CfvAb" 1040 | assert B58.encode58_check!("hello world", 1, alphabet: :ripple) == "BnoSHny7DQS9XAzogicWP" 1041 | assert B58.encode58_check!("Hello World", 255, alphabet: :ripple) == "YXMkDYBSTNWVuNpvQ6Qr8c" 1042 | assert B58.encode58_check!(<<>>, 0, alphabet: :ripple) == "rW6hb6" 1043 | end 1044 | 1045 | test "encode58_check/3 Base58Check encodes strings according to the ripple alphabet" do 1046 | assert B58.encode58_check("hello", 0, alphabet: :ripple) == {:ok, "rpLnBnyq1CfvAb"} 1047 | 1048 | assert B58.encode58_check("hello world", 1, alphabet: :ripple) == 1049 | {:ok, "BnoSHny7DQS9XAzogicWP"} 1050 | 1051 | assert B58.encode58_check("Hello World", 255, alphabet: :ripple) == 1052 | {:ok, "YXMkDYBSTNWVuNpvQ6Qr8c"} 1053 | 1054 | assert B58.encode58_check(<<>>, 0, alphabet: :ripple) == {:ok, "rW6hb6"} 1055 | end 1056 | 1057 | test "encode58_check!/3 Base58Check encodes sha-256 strings according to the ripple alphabet" do 1058 | # From https://github.com/multiformats/multihash 1059 | assert "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 1060 | |> Base.decode16!() 1061 | |> B58.encode58_check!(0, alphabet: :ripple) == 1062 | "rsgXk93a69FwFWsu4eGwHDq78YHo3croSCH2jVDuzPt5SokA6zzeK" 1063 | end 1064 | 1065 | test "encode58_check!/3 Base58Check encodes a RIPEMD-160 hash using the ripple alphabet" do 1066 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 1067 | assert "f54a5851e9372b87810a8e60cdd2e7cfd80b6e31" 1068 | |> Base.decode16!(case: :lower) 1069 | |> B58.encode58_check!(0, alphabet: :ripple) == "rPMyc2c8J2SqAAJqj2AXBNi8L1ZfRkX7w1" 1070 | end 1071 | 1072 | test "encode58_check!/2 handles Base58 encoding leading zeroes using the ripple alphabet" do 1073 | assert B58.encode58_check!(<<0>>, 0, alphabet: :ripple) == "rrpedBaq" 1074 | 1075 | assert B58.encode58_check!(<<0, 0, 0, "hello world">>, 0, alphabet: :ripple) == 1076 | "rrrrsvQBfBaMiGQZ2xUiokgxh" 1077 | 1078 | assert B58.encode58_check!(<<0, 0, 0>>, 0, alphabet: :ripple) == "rrrrhbdQd2" 1079 | end 1080 | 1081 | test "version_encode58_check!/3 accepts unsigned single byte integer versions using the ripple alphabet" do 1082 | assert B58.version_encode58_check!(<<0, "a">>, alphabet: :ripple) == "rUst945b" 1083 | assert B58.version_encode58_check!(<<1, "a">>, alphabet: :ripple) == "gqkAoXD" 1084 | assert B58.version_encode58_check!(<<2, "a">>, alphabet: :ripple) == "pBkJb6jW" 1085 | assert B58.version_encode58_check!(<<255, "a">>, alphabet: :ripple) == "sUwAsRMUe" 1086 | assert B58.version_encode58_check!(<<2>>, alphabet: :ripple) == "NF5sKP" 1087 | end 1088 | 1089 | test "version_encode58_check/3 Base58Check encodes strings according to the ripple alphabet" do 1090 | assert B58.version_encode58_check(<<0, "hello">>, alphabet: :ripple) == 1091 | {:ok, "rpLnBnyq1CfvAb"} 1092 | 1093 | assert B58.version_encode58_check(<<1, "hello world">>, alphabet: :ripple) == 1094 | {:ok, "BnoSHny7DQS9XAzogicWP"} 1095 | 1096 | assert B58.version_encode58_check(<<255, "Hello World">>, alphabet: :ripple) == 1097 | {:ok, "YXMkDYBSTNWVuNpvQ6Qr8c"} 1098 | 1099 | assert B58.version_encode58_check(<<0>>, alphabet: :ripple) == {:ok, "rW6hb6"} 1100 | end 1101 | 1102 | test "version_encode58_check!/3 Base58Check encodes strings according to the ripple alphabet" do 1103 | assert B58.version_encode58_check!(<<0, "hello">>, alphabet: :ripple) == "rpLnBnyq1CfvAb" 1104 | 1105 | assert B58.version_encode58_check!(<<1, "hello world">>, alphabet: :ripple) == 1106 | "BnoSHny7DQS9XAzogicWP" 1107 | 1108 | assert B58.version_encode58_check!(<<255, "Hello World">>, alphabet: :ripple) == 1109 | "YXMkDYBSTNWVuNpvQ6Qr8c" 1110 | 1111 | assert B58.version_encode58_check!(<<0>>, alphabet: :ripple) == "rW6hb6" 1112 | end 1113 | 1114 | test "version_encode58_check!/2 Base58Check encodes a versioned RIPEMD-160 hash using the ripple alphabet" do 1115 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 1116 | assert 0xF54A5851E9372B87810A8E60CDD2E7CFD80B6E31 1117 | |> :binary.encode_unsigned() 1118 | |> B58.version_binary(0) 1119 | |> B58.version_encode58_check!(alphabet: :ripple) == 1120 | "rPMyc2c8J2SqAAJqj2AXBNi8L1ZfRkX7w1" 1121 | end 1122 | 1123 | test "version_encode58_check!/2 handles Base58 encoding leading zeroes using the ripple alphabet" do 1124 | assert B58.version_encode58_check!(<<0, 0>>, alphabet: :ripple) == "rrpedBaq" 1125 | 1126 | assert B58.version_encode58_check!(<<0, 0, 0, 0, "hello world">>, alphabet: :ripple) == 1127 | "rrrrsvQBfBaMiGQZ2xUiokgxh" 1128 | 1129 | assert B58.version_encode58_check!(<<0, 0, 0, 0>>, alphabet: :ripple) == "rrrrhbdQd2" 1130 | end 1131 | 1132 | test "decode58!/2 decodes Base58 encoded binaries using the ripple alphabet" do 1133 | assert B58.decode58!("U83eVZg", alphabet: :ripple) == "hello" 1134 | assert B58.decode58!("StVrDLaUATiyKyV", alphabet: :ripple) == "hello world" 1135 | assert B58.decode58!("JxErpTiA7PhnBMd", alphabet: :ripple) == "Hello World" 1136 | assert B58.decode58!(<<>>, alphabet: :ripple) == <<>> 1137 | end 1138 | 1139 | test "decode58!/2 handles Base58 encoded with leading zeroes using the ripple alphabet" do 1140 | assert B58.decode58!("r", alphabet: :ripple) == <<0>> 1141 | assert B58.decode58!("rrr", alphabet: :ripple) == <<0, 0, 0>> 1142 | assert B58.decode58!("rrrStVrDLaUATiyKyV", alphabet: :ripple) == <<0, 0, 0, "hello world">> 1143 | end 1144 | 1145 | test "decode58!/2 Base58 decodes sha-256 strings using the ripple alphabet" do 1146 | # From https://github.com/multiformats/multihash 1147 | assert "QmYt7ch5TUbbCVSD4KvtQqiCyezPP8EvNssAEmutA9PBBk" 1148 | |> B58.decode58!(alphabet: :ripple) 1149 | |> Base.encode16() == 1150 | "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 1151 | end 1152 | 1153 | test "decode58!/2 Base58 handles invalid binaries when using the ripple alphabet" do 1154 | # invalid character 1155 | assert_raise ArgumentError, fn -> 1156 | B58.decode58!("~", alphabet: :ripple) 1157 | end 1158 | 1159 | # invalid leading character 1160 | assert_raise ArgumentError, fn -> 1161 | B58.decode58!("~U83eVZg", alphabet: :ripple) 1162 | end 1163 | 1164 | # invalid trailing character 1165 | assert_raise ArgumentError, fn -> 1166 | B58.decode58!("U83eVZg^", alphabet: :ripple) 1167 | end 1168 | 1169 | # invalid character mid string 1170 | assert_raise ArgumentError, fn -> 1171 | B58.decode58!("U83%VZg", alphabet: :ripple) 1172 | end 1173 | 1174 | # invalid character excluded from alphabet due to clarity 1175 | assert_raise ArgumentError, fn -> 1176 | B58.decode58!("O83eVZg", alphabet: :ripple) 1177 | end 1178 | 1179 | # base16 encoded string 1180 | assert_raise ArgumentError, fn -> 1181 | "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 1182 | |> B58.decode58!(alphabet: :ripple) 1183 | end 1184 | end 1185 | 1186 | test "decode58/2 Base58 handles invalid binaries when using the ripple alphabet" do 1187 | # invalid character 1188 | {:error, _} = B58.decode58("~", alphabet: :ripple) 1189 | # invalid leading character 1190 | {:error, _} = B58.decode58("~U83eVZg", alphabet: :ripple) 1191 | # invalid trailing character 1192 | {:error, _} = B58.decode58("U83eVZg^", alphabet: :ripple) 1193 | # invalid character mid string 1194 | {:error, _} = B58.decode58("U83%VZ", alphabet: :ripple) 1195 | # invalid character excluded from alphabet due to clarity 1196 | {:error, _} = B58.decode58("O83eVZg", alphabet: :ripple) 1197 | # base16 encoded string 1198 | {:error, _} = 1199 | "12209CBC07C3F991725836A3AA2A581CA2029198AA420B9D99BC0E131D9F3E2CBE47" 1200 | |> B58.decode58(alphabet: :ripple) 1201 | end 1202 | 1203 | test "decode58_check!/2 decodes Base58Check encoded binaries using the ripple alphabet" do 1204 | assert B58.decode58_check!("rpLnBnyq1CfvAb", alphabet: :ripple) == {"hello", <<0>>} 1205 | 1206 | assert B58.decode58_check!("BnoSHny7DQS9XAzogicWP", alphabet: :ripple) == 1207 | {"hello world", <<1>>} 1208 | 1209 | assert B58.decode58_check!("YXMkDYBSTNWVuNpvQ6Qr8c", alphabet: :ripple) == 1210 | {"Hello World", <<255>>} 1211 | 1212 | assert B58.decode58_check!("rW6hb6", alphabet: :ripple) == {<<>>, <<0>>} 1213 | end 1214 | 1215 | test "decode58_check/2 decodes Base58Check encoded binaries using the ripple alphabet" do 1216 | assert B58.decode58_check("rpLnBnyq1CfvAb", alphabet: :ripple) == {:ok, {"hello", <<0>>}} 1217 | 1218 | assert B58.decode58_check("BnoSHny7DQS9XAzogicWP", alphabet: :ripple) == 1219 | {:ok, {"hello world", <<1>>}} 1220 | 1221 | assert B58.decode58_check("YXMkDYBSTNWVuNpvQ6Qr8c", alphabet: :ripple) == 1222 | {:ok, {"Hello World", <<255>>}} 1223 | 1224 | assert B58.decode58_check("rW6hb6", alphabet: :ripple) == {:ok, {<<>>, <<0>>}} 1225 | end 1226 | 1227 | test "decode58_check!/2 Base58Check decodes to a RIPEMD-160 encoded hash using the ripple alphabet" do 1228 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 1229 | {hash_bin, version} = 1230 | "rPMyc2c8J2SqAAJqj2AXBNi8L1ZfRkX7w1" 1231 | |> B58.decode58_check!(alphabet: :ripple) 1232 | 1233 | assert version == <<0>> 1234 | 1235 | assert hash_bin 1236 | |> Base.encode16(case: :lower) == "f54a5851e9372b87810a8e60cdd2e7cfd80b6e31" 1237 | end 1238 | 1239 | test "decode58_check!/2 handles binaries with invalid checksums encoded using the ripple alphabet" do 1240 | # corrupt last byte 1241 | assert_raise ArgumentError, fn -> B58.decode58_check!("1pLnBnyq1CfvAq", alphabet: :ripple) end 1242 | # corrupt first byte 1243 | assert_raise ArgumentError, fn -> B58.decode58_check!("2pLnBnyq1CfvAb", alphabet: :ripple) end 1244 | # corrupt middle byte 1245 | assert_raise ArgumentError, fn -> B58.decode58_check!("1pLnBnqq1CfvAb", alphabet: :ripple) end 1246 | # corrupted empty 1247 | assert_raise ArgumentError, fn -> B58.decode58_check!("1B6hb6", alphabet: :ripple) end 1248 | end 1249 | 1250 | test "decode58_check/2 handles binaries with invalid checksums encoded using the ripple alphabet" do 1251 | # corrupt last byte 1252 | {:error, _} = B58.decode58_check("1pLnBnyq1CfvAq", alphabet: :ripple) 1253 | # corrupt first byte 1254 | {:error, _} = B58.decode58_check("2pLnBnyq1CfvAb", alphabet: :ripple) 1255 | # corrupt middle byte 1256 | {:error, _} = B58.decode58_check("1pLnBnqq1CfvAb", alphabet: :ripple) 1257 | # corrupted empty 1258 | {:error, _} = B58.decode58_check("1W6bb6", alphabet: :ripple) 1259 | end 1260 | 1261 | test "decode58_check!/2 handles invalid binaries when using encoding using the ripple alphabet" do 1262 | # Zero is not in this alphabet 1263 | assert_raise ArgumentError, fn -> B58.decode58_check!("0pLnBnyq1CfvAb", alphabet: :ripple) end 1264 | # Underscore is not in this alphabet 1265 | assert_raise ArgumentError, fn -> 1266 | B58.decode58_check!("1pLnBnyq1CfvAb_", alphabet: :ripple) 1267 | end 1268 | 1269 | # Base64 alphabet is not compatible 1270 | assert_raise ArgumentError, fn -> 1271 | "Hello World" 1272 | |> Base.encode64() 1273 | |> B58.decode58_check!(alphabet: :ripple) 1274 | end 1275 | 1276 | # missing bytes 1277 | assert_raise ArgumentError, fn -> B58.decode58_check!("16hb6", alphabet: :ripple) end 1278 | assert_raise ArgumentError, fn -> B58.decode58_check!(<<>>, alphabet: :ripple) end 1279 | end 1280 | 1281 | test "decode58_check/2 handles invalid binaries when using encoding using the ripple alphabet" do 1282 | # Zero is not in this alphabet 1283 | {:error, _} = B58.decode58_check("0pLnBnyq1CfvAb", alphabet: :ripple) 1284 | # Underscore is not in this alphabet 1285 | {:error, _} = B58.decode58_check("1pLnBnyq1CfvAb_", alphabet: :ripple) 1286 | # Base64 alphabet is not compatible 1287 | {:error, _} = 1288 | "Hello World" 1289 | |> Base.encode64() 1290 | |> B58.decode58_check(alphabet: :ripple) 1291 | 1292 | # missing bytes 1293 | {:error, _} = B58.decode58_check("16hb6", alphabet: :ripple) 1294 | {:error, _} = B58.decode58_check(<<>>, alphabet: :ripple) 1295 | end 1296 | 1297 | test "version_decode58_check!/2 decodes Base58Check encoded binaries using the ripple alphabet" do 1298 | assert B58.version_decode58_check!("rpLnBnyq1CfvAb", alphabet: :ripple) == <<0, "hello">> 1299 | 1300 | assert B58.version_decode58_check!("BnoSHny7DQS9XAzogicWP", alphabet: :ripple) == 1301 | <<1, "hello world">> 1302 | 1303 | assert B58.version_decode58_check!("YXMkDYBSTNWVuNpvQ6Qr8c", alphabet: :ripple) == 1304 | <<255, "Hello World">> 1305 | 1306 | assert B58.version_decode58_check!("rW6hb6", alphabet: :ripple) == <<0>> 1307 | end 1308 | 1309 | test "version_decode58_check/2 decodes Base58Check encoded binaries using the ripple alphabet" do 1310 | assert B58.version_decode58_check("rpLnBnyq1CfvAb", alphabet: :ripple) == 1311 | {:ok, <<0, "hello">>} 1312 | 1313 | assert B58.version_decode58_check("BnoSHny7DQS9XAzogicWP", alphabet: :ripple) == 1314 | {:ok, <<1, "hello world">>} 1315 | 1316 | assert B58.version_decode58_check("YXMkDYBSTNWVuNpvQ6Qr8c", alphabet: :ripple) == 1317 | {:ok, <<255, "Hello World">>} 1318 | 1319 | assert B58.version_decode58_check("rW6hb6", alphabet: :ripple) == {:ok, <<0>>} 1320 | end 1321 | 1322 | test "version_decode58_check!/2 Base58Check decodes to a versioned RIPEMD-160 encoded hash using the ripple alphabet" do 1323 | # ex per: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 1324 | assert "rPMyc2c8J2SqAAJqj2AXBNi8L1ZfRkX7w1" 1325 | |> B58.version_decode58_check!(alphabet: :ripple) 1326 | |> :binary.decode_unsigned() == 0x0F54A5851E9372B87810A8E60CDD2E7CFD80B6E31 1327 | end 1328 | 1329 | test "version_decode58_check!/2 handles binaries with invalid checksums encoded using the ripple alphabet" do 1330 | # corrupt last byte 1331 | assert_raise ArgumentError, fn -> 1332 | B58.version_decode58_check!("rpLnBnyq1CfvAv", alphabet: :ripple) 1333 | end 1334 | 1335 | # corrupt first byte 1336 | assert_raise ArgumentError, fn -> 1337 | B58.version_decode58_check!("bpLnBnyq1CfvAb", alphabet: :ripple) 1338 | end 1339 | 1340 | # corrupt middle byte 1341 | assert_raise ArgumentError, fn -> 1342 | B58.version_decode58_check!("rpLnBnqq1CfvAb", alphabet: :ripple) 1343 | end 1344 | 1345 | # corrupted empty 1346 | assert_raise ArgumentError, fn -> 1347 | B58.version_decode58_check!("rW6bhb6", alphabet: :ripple) 1348 | end 1349 | end 1350 | 1351 | test "version_decode58_check/2 handles binaries with invalid checksums encoded using the ripple alphabet" do 1352 | # corrupt last byte 1353 | {:error, _} = B58.version_decode58_check("rpLnBnyq1CfvAv", alphabet: :ripple) 1354 | # corrupt first byte 1355 | {:error, _} = B58.version_decode58_check("bpLnBnyq1CfvAb", alphabet: :ripple) 1356 | # corrupt middle byte 1357 | {:error, _} = B58.version_decode58_check("rpLnBnqq1CfvAb", alphabet: :ripple) 1358 | # corrupted empty 1359 | {:error, _} = B58.version_decode58_check("rW6bhb6", alphabet: :ripple) 1360 | end 1361 | 1362 | # You, friend, are a reader 1363 | 1364 | test "version_decode58_check!/2 handles invalid binaries when using encoding using the ripple alphabet" do 1365 | # Zero is not in this alphabet 1366 | assert_raise ArgumentError, fn -> 1367 | B58.version_decode58_check!("0pLnBnyq1CfvAb", alphabet: :ripple) 1368 | end 1369 | 1370 | # Underscore is not in this alphabet 1371 | assert_raise ArgumentError, fn -> 1372 | B58.version_decode58_check!("rpLnBnyq1CfvAb_", alphabet: :ripple) 1373 | end 1374 | 1375 | # Base64 alphabet is not compatible 1376 | assert_raise ArgumentError, fn -> 1377 | "Hello World" 1378 | |> Base.encode64() 1379 | |> B58.version_decode58_check!(alphabet: :ripple) 1380 | end 1381 | 1382 | # missing bytes 1383 | assert_raise ArgumentError, fn -> B58.version_decode58_check!("r6hb6", alphabet: :ripple) end 1384 | assert_raise ArgumentError, fn -> B58.version_decode58_check!(<<>>, alphabet: :ripple) end 1385 | end 1386 | 1387 | test "version_decode58_check/2 handles invalid binaries when using encoding using the ripple alphabet" do 1388 | # Zero is not in this alphabet 1389 | {:error, _} = B58.version_decode58_check("0pLnBnyq1CfvAb", alphabet: :ripple) 1390 | # Underscore is not in this alphabet 1391 | {:error, _} = B58.version_decode58_check("rpLnBnyq1CfvAb_", alphabet: :ripple) 1392 | # Base64 alphabet is not compatible 1393 | {:error, _} = 1394 | "Hello World" 1395 | |> Base.encode64() 1396 | |> B58.version_decode58_check(alphabet: :ripple) 1397 | 1398 | # missing bytes 1399 | {:error, _} = B58.version_decode58_check("r6hb6", alphabet: :ripple) 1400 | {:error, _} = B58.version_decode58_check(<<>>, alphabet: :ripple) 1401 | end 1402 | 1403 | # ============================================================================ 1404 | # For the masses, but I prefer other albums 1405 | # ============================================================================ 1406 | test "alphabets/0 returns the IDs of all the known current alphabets" do 1407 | assert alphabets() == [:btc, :flickr, :ripple] 1408 | end 1409 | 1410 | test "version_binary/1 versions a binary according to Base58 rules using an uint8" do 1411 | assert B58.version_binary("a", <<0>>) == <<0, "a">> 1412 | assert B58.version_binary("a", 0) == <<0, "a">> 1413 | assert B58.version_binary("a", <<1>>) == <<1, "a">> 1414 | assert B58.version_binary("a", 1) == <<1, "a">> 1415 | assert B58.version_binary("a", <<255>>) == <<255, "a">> 1416 | assert B58.version_binary("a", 255) == <<255, "a">> 1417 | assert B58.version_binary(<<>>, <<2>>) == <<2>> 1418 | assert B58.version_binary(<<>>, 2) == <<2>> 1419 | assert_raise ArgumentError, fn -> 1420 | B58.version_binary("a", 256) 1421 | end 1422 | assert_raise ArgumentError, fn -> 1423 | B58.version_binary("a", -1) 1424 | end 1425 | assert_raise ArgumentError, fn -> 1426 | B58.version_binary("a", <<1, 0>>) 1427 | end 1428 | end 1429 | end 1430 | --------------------------------------------------------------------------------