base58

Pure Haskell base58, base58check encoding/decoding (docs.ppad.tech/base58).
git clone git://git.ppad.tech/base58.git
Log | Files | Refs | README | LICENSE

commit f0e9ce62fbfd9d0736c9d6535de5597c2160e39a
parent 44cefe2b0a216388e2a2add8ec5538d2cee2ead5
Author: Jared Tobin <jared@jtobin.io>
Date:   Tue, 18 Feb 2025 23:04:55 +0400

lib: allow arbitrary version bytes

Whilst most applications use only a single version byte, BIP32's
extended keys are serialized using several.

Diffstat:
MREADME.md | 6+++---
Mbench/Main.hs | 2+-
Mlib/Data/ByteString/Base58Check.hs | 25+++++++++++--------------
Mtest/Main.hs | 22+++++-----------------
4 files changed, 20 insertions(+), 35 deletions(-)

diff --git a/README.md b/README.md @@ -23,12 +23,12 @@ A sample GHCi session: Just "hello world" > > -- base58check is a versioned, checksummed format - > let b58check = B58Check.encode 0x00 "hello world" + > let b58check = B58Check.encode "\NULhello world" -- 0x00 version byte > b58check "13vQB7B6MrGQZaxCqW9KER" > - > B58Check.decodeb58check - Just (0,"hello world") + > B58Check.decode b58check + Just "\NULhello world" ``` ## Documentation diff --git a/bench/Main.hs b/bench/Main.hs @@ -23,7 +23,7 @@ base32 = bgroup "ppad-base32" [ ] , bgroup "base58check" [ bgroup "encode" [ - bench "0x00, hello world" $ nf (B58C.encode 0x00) "hello world" + bench "0x00, hello world" $ nf B58C.encode "\NULhello world" ] , bgroup "decode" [ bench "13vQB7B6MrGQZaxCqW9KER" $ diff --git a/lib/Data/ByteString/Base58Check.hs b/lib/Data/ByteString/Base58Check.hs @@ -8,10 +8,9 @@ -- -- base58check encoding and decoding of strict bytestrings. -- --- base58check is a versioned, checksummed base58 encoding. A payload is --- constructed from a leading version byte and some base256 input, and --- then a checksum is computed by SHA256d-ing the payload, appending its --- first 4 bytes, and base58-encoding the result. +-- base58check is a versioned, checksummed base58 encoding. A checksum +-- is computed by SHA256d-ing a payload, appending its first 4 bytes, +-- and base58-encoding the result. module Data.ByteString.Base58Check ( encode @@ -22,31 +21,29 @@ import Control.Monad (guard) import qualified Crypto.Hash.SHA256 as SHA256 import qualified Data.ByteString as BS import qualified Data.ByteString.Base58 as B58 -import Data.Word (Word8) --- | Encode a version byte and base256 'ByteString' as base58check. +-- | Encode a base256 'ByteString' as base58check. -- --- >>> encode 0x00 "hello world" +-- >>> encode (BS.singleton 0x00 <> "hello world") -- "13vQB7B6MrGQZaxCqW9KER" -encode :: Word8 -> BS.ByteString -> BS.ByteString -encode ver dat = - let pay = BS.cons ver dat - kek = BS.take 4 (SHA256.hash (SHA256.hash pay)) +encode :: BS.ByteString -> BS.ByteString +encode pay = + let kek = BS.take 4 (SHA256.hash (SHA256.hash pay)) in B58.encode (pay <> kek) -- | Validate and decode a base58check-encoded string. Invalid -- base58check inputs will produce 'Nothing'. -- -- >>> decode "13vQB7B6MrGQZaxCqW9KER" --- Just (0,"hello world") +-- Just "\NULhello world" -- >>> decode "13uQB7B6MrGQZaxCqW9KER" -- s/v/u -- Nothing -decode :: BS.ByteString -> Maybe (Word8, BS.ByteString) +decode :: BS.ByteString -> Maybe BS.ByteString decode mb = do bs <- B58.decode mb let len = BS.length bs (pay, kek) = BS.splitAt (len - 4) bs man = BS.take 4 (SHA256.hash (SHA256.hash pay)) guard (kek == man) - BS.uncons pay + pure pay diff --git a/test/Main.hs b/test/Main.hs @@ -12,7 +12,6 @@ import qualified Data.ByteString.Base58 as B58 import qualified Data.ByteString.Base58Check as B58Check import qualified Data.Text.Encoding as TE import qualified Data.Text.IO as TIO -import Data.Word (Word8) import Test.Tasty import Test.Tasty.HUnit import qualified Test.Tasty.QuickCheck as Q @@ -53,9 +52,7 @@ execute_base58check Base58Check {..} = testGroup "base58check" [ ] where execute_valid Valid_Base58Check {..} = testCase "valid" $ do -- label - let enc = case BS.uncons vc_payload of - Nothing -> error "faulty" - Just (h, t) -> B58Check.encode h t + let enc = B58Check.encode vc_payload assertEqual mempty enc vc_string execute_invalid Invalid_Base58Check {..} = testCase "invalid" $ do -- label @@ -89,30 +86,21 @@ bytes k = do v <- Q.vectorOf l Q.arbitrary pure (BS.pack v) -data B58C = B58C Word8 BS - deriving (Eq, Show) - instance Q.Arbitrary BS where arbitrary = do b <- bytes 1024 pure (BS b) -instance Q.Arbitrary B58C where - arbitrary = do - w8 <- Q.arbitrary - bs <- Q.arbitrary - pure (B58C w8 bs) - base58_decode_inverts_encode :: BS -> Bool base58_decode_inverts_encode (BS bs) = case B58.decode (B58.encode bs) of Nothing -> False Just b -> b == bs -base58check_decode_inverts_encode :: B58C -> Bool -base58check_decode_inverts_encode (B58C w8 (BS bs)) = - case B58Check.decode (B58Check.encode w8 bs) of +base58check_decode_inverts_encode :: BS -> Bool +base58check_decode_inverts_encode (BS bs) = + case B58Check.decode (B58Check.encode bs) of Nothing -> False - Just (w8', bs') -> w8 == w8' && bs == bs' + Just b -> b == bs main :: IO () main = do