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:
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