base58

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

Main.hs (6574B)


      1 {-# LANGUAGE OverloadedStrings #-}
      2 {-# LANGUAGE RecordWildCards #-}
      3 
      4 module Main where
      5 
      6 import Data.Aeson ((.:))
      7 import qualified Data.Aeson as A
      8 import qualified Data.Aeson.Types as AT
      9 import qualified Data.ByteString as BS
     10 import qualified Data.ByteString.Char8 as B8
     11 import qualified Data.ByteString.Base16 as B16
     12 import qualified Data.ByteString.Base58 as B58
     13 import qualified Data.ByteString.Base58Check as B58Check
     14 import qualified Data.Text.Encoding as TE
     15 import qualified Data.Text.IO as TIO
     16 import Test.Tasty
     17 import Test.Tasty.HUnit
     18 import qualified Test.Tasty.QuickCheck as Q
     19 
     20 data Valid_Base58Check = Valid_Base58Check {
     21     vc_string  :: !BS.ByteString
     22   , vc_payload :: !BS.ByteString
     23   } deriving Show
     24 
     25 hex :: BS.ByteString -> AT.Parser BS.ByteString
     26 hex bs = case B16.decode bs of
     27   Nothing -> fail "invalid hex"
     28   Just v  -> pure v
     29 
     30 instance A.FromJSON Valid_Base58Check where
     31   parseJSON = A.withObject "Valid_Base58Check" $ \m -> Valid_Base58Check
     32     <$> fmap TE.encodeUtf8 (m .: "string")
     33     <*> (hex . TE.encodeUtf8 =<< (m .: "payload"))
     34 
     35 data Invalid_Base58Check = Invalid_Base58Check {
     36     ic_string  :: !BS.ByteString
     37   } deriving Show
     38 
     39 instance A.FromJSON Invalid_Base58Check where
     40   parseJSON = A.withObject "Invalid_Base58Check" $ \m -> Invalid_Base58Check
     41     <$> fmap TE.encodeUtf8 (m .: "string")
     42 
     43 data Base58Check = Base58Check {
     44     b58c_valid   :: ![Valid_Base58Check]
     45   , b58c_invalid :: ![Invalid_Base58Check]
     46   } deriving Show
     47 
     48 instance A.FromJSON Base58Check where
     49   parseJSON = A.withObject "Base58Check" $ \m -> Base58Check
     50     <$> (m .: "valid")
     51     <*> (m .: "invalid")
     52 
     53 
     54 execute_base58check :: Base58Check -> TestTree
     55 execute_base58check Base58Check {..} = testGroup "base58check" [
     56       testGroup "valid" (fmap execute_valid b58c_valid)
     57     , testGroup "invalid" (fmap execute_invalid b58c_invalid)
     58     ]
     59   where
     60     execute_valid Valid_Base58Check {..} = testGroup (label vc_string) [
     61         testCase "encode" $
     62           assertEqual mempty vc_string (B58Check.encode vc_payload)
     63       , testCase "decode" $
     64           assertEqual mempty (Just vc_payload) (B58Check.decode vc_string)
     65       ]
     66 
     67     execute_invalid Invalid_Base58Check {..} =
     68       testCase (label ic_string) $
     69         assertEqual mempty Nothing (B58Check.decode ic_string)
     70 
     71 data Valid_Base58 = Valid_Base58 {
     72     vb_decodedHex  :: !BS.ByteString
     73   , vb_encoded     :: !BS.ByteString
     74   } deriving Show
     75 
     76 instance A.FromJSON Valid_Base58 where
     77   parseJSON = A.withObject "Valid_Base58" $ \m -> Valid_Base58
     78     <$> (hex . TE.encodeUtf8 =<< (m .: "decodedHex"))
     79     <*> fmap TE.encodeUtf8 (m .: "encoded")
     80 
     81 execute_base58 :: Valid_Base58 -> TestTree
     82 execute_base58 Valid_Base58 {..} = testGroup (label vb_encoded) [
     83     testCase "encode" $
     84       assertEqual mempty vb_encoded (B58.encode vb_decodedHex)
     85   , testCase "decode" $
     86       assertEqual mempty (Just vb_decodedHex) (B58.decode vb_encoded)
     87   ]
     88 
     89 -- test label for an encoded string
     90 label :: BS.ByteString -> String
     91 label bs
     92   | BS.null bs = "(empty)"
     93   | otherwise  = show (B8.unpack bs)
     94 
     95 -- strings containing non-base58 characters
     96 invalid_base58 :: [BS.ByteString]
     97 invalid_base58 = [
     98     "0"
     99   , "O"
    100   , "I"
    101   , "l"
    102   , " "
    103   , "\NUL"
    104   , "+"
    105   , "/"
    106   , "\xff"
    107   , "\xc3\xa9"
    108   , "StV1DL0CwTryKyV"
    109   , "StV1DLOCwTryKyV"
    110   , "StV1DLICwTryKyV"
    111   , "StV1DLlCwTryKyV"
    112   , " StV1DL6CwTryKyV"
    113   , "StV1DL6CwTryKyV "
    114   , "StV1DL6CwTryKyV\n"
    115   , "11110"
    116   , "111l"
    117   , "11 1"
    118   ]
    119 
    120 execute_invalid_base58 :: BS.ByteString -> TestTree
    121 execute_invalid_base58 bs = testCase (label bs) $
    122   assertEqual mempty Nothing (B58.decode bs)
    123 
    124 -- strings that base58-decode to fewer than four bytes
    125 short_base58check :: [BS.ByteString]
    126 short_base58check = ["", "1", "11", "111", "2g", "a3gV"]
    127 
    128 execute_short_base58check :: BS.ByteString -> TestTree
    129 execute_short_base58check bs = testCase (label bs) $
    130   assertEqual mempty Nothing (B58Check.decode bs)
    131 
    132 newtype BS = BS BS.ByteString
    133   deriving (Eq, Show)
    134 
    135 bytes :: Int -> Q.Gen BS.ByteString
    136 bytes k = do
    137   l <- Q.chooseInt (0, k)
    138   v <- Q.vectorOf l Q.arbitrary
    139   pure (BS.pack v)
    140 
    141 instance Q.Arbitrary BS where
    142   arbitrary = do
    143     b <- bytes 1024
    144     pure (BS b)
    145 
    146 -- arbitrary strings over the base58 alphabet, plus some invalid
    147 -- characters
    148 newtype B58Str = B58Str BS.ByteString
    149   deriving (Eq, Show)
    150 
    151 instance Q.Arbitrary B58Str where
    152   arbitrary = do
    153     l <- Q.chooseInt (0, 64)
    154     v <- Q.vectorOf l $ Q.frequency [
    155         (16, Q.elements alphabet)
    156       , (1, Q.elements (BS.unpack "0OIl +/\NUL\xff"))
    157       ]
    158     pure (B58Str (BS.pack v))
    159     where
    160       alphabet = BS.unpack
    161         "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
    162 
    163 -- decoding succeeds exactly on strings over the alphabet, and every
    164 -- such string is canonical
    165 base58_decode_canonical :: B58Str -> Bool
    166 base58_decode_canonical (B58Str bs) = case B58.decode bs of
    167   Nothing -> BS.any (`BS.notElem` alphabet) bs
    168   Just d  -> B58.encode d == bs
    169   where
    170     alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
    171 
    172 base58_decode_inverts_encode :: BS -> Bool
    173 base58_decode_inverts_encode (BS bs) = case B58.decode (B58.encode bs) of
    174   Nothing -> False
    175   Just b  -> b == bs
    176 
    177 base58check_decode_inverts_encode :: BS -> Bool
    178 base58check_decode_inverts_encode (BS bs) =
    179   case B58Check.decode (B58Check.encode bs) of
    180     Nothing -> False
    181     Just b  -> b == bs
    182 
    183 main :: IO ()
    184 main = do
    185   scure_base58 <- TIO.readFile "etc/base58.json"
    186   scure_base58check <- TIO.readFile "etc/base58_check.json"
    187   let per = do
    188         b0 <- A.decodeStrictText scure_base58 :: Maybe [Valid_Base58]
    189         b1 <- A.decodeStrictText scure_base58check :: Maybe Base58Check
    190         pure (b0, b1)
    191   case per of
    192     Nothing -> error "couldn't parse vectors"
    193     Just (b58, b58c) -> defaultMain $ testGroup "ppad-base58" [
    194         testGroup "unit tests" [
    195             testGroup "base58" (fmap execute_base58 b58)
    196           , testGroup "base58 (invalid)"
    197               (fmap execute_invalid_base58 invalid_base58)
    198           , execute_base58check b58c
    199           , testGroup "base58check (short)"
    200               (fmap execute_short_base58check short_base58check)
    201           ]
    202       , testGroup "property tests" [
    203           Q.testProperty "(base58) decode . encode ~ id" $
    204             Q.withMaxSuccess 250 base58_decode_inverts_encode
    205         , Q.testProperty "(base58) decode succeeds iff canonical" $
    206             Q.withMaxSuccess 1000 base58_decode_canonical
    207         , Q.testProperty "(base58check) decode . encode ~ id" $
    208             Q.withMaxSuccess 250 base58check_decode_inverts_encode
    209         ]
    210       ]
    211