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