|
| 1 | +{-# LANGUAGE OverloadedStrings #-} |
| 2 | + |
| 3 | +{- Copied from Network.Haskoin.Address.Bech32 -} |
| 4 | +{- Copied from reference implementation contributed by Marko Bencun -} |
| 5 | + |
| 6 | +module Functora.Bech32 |
| 7 | + ( HRP, |
| 8 | + Bech32, |
| 9 | + Data, |
| 10 | + bech32Encode, |
| 11 | + bech32Decode, |
| 12 | + toBase32, |
| 13 | + toBase256, |
| 14 | + toBase256', |
| 15 | + segwitEncode, |
| 16 | + segwitDecode, |
| 17 | + Word5 (..), |
| 18 | + word5, |
| 19 | + fromWord5, |
| 20 | + ) |
| 21 | +where |
| 22 | + |
| 23 | +import Control.Monad (guard) |
| 24 | +import Data.Array |
| 25 | + ( Array, |
| 26 | + assocs, |
| 27 | + bounds, |
| 28 | + listArray, |
| 29 | + (!), |
| 30 | + (//), |
| 31 | + ) |
| 32 | +import Data.Bits |
| 33 | + ( Bits, |
| 34 | + testBit, |
| 35 | + unsafeShiftL, |
| 36 | + unsafeShiftR, |
| 37 | + xor, |
| 38 | + (.&.), |
| 39 | + (.|.), |
| 40 | + ) |
| 41 | +import qualified Data.ByteString as B |
| 42 | +import Data.Char (toUpper) |
| 43 | +import Data.Foldable (foldl') |
| 44 | +import Data.Functor.Identity (Identity, runIdentity) |
| 45 | +import Data.Ix (Ix (..)) |
| 46 | +import Data.Text (Text) |
| 47 | +import qualified Data.Text as T |
| 48 | +import qualified Data.Text.Encoding as E |
| 49 | +import Data.Word (Word8) |
| 50 | +import Prelude |
| 51 | + |
| 52 | +-- | Bech32 human-readable string. |
| 53 | +type Bech32 = Text |
| 54 | + |
| 55 | +-- | Human-readable part of 'Bech32' address. |
| 56 | +type HRP = Text |
| 57 | + |
| 58 | +-- | Data part of 'Bech32' address. |
| 59 | +type Data = [Word8] |
| 60 | + |
| 61 | +(.>>.), (.<<.) :: (Bits a) => a -> Int -> a |
| 62 | +(.>>.) = unsafeShiftR |
| 63 | +(.<<.) = unsafeShiftL |
| 64 | + |
| 65 | +-- | Five-bit word for Bech32. |
| 66 | +newtype Word5 |
| 67 | + = UnsafeWord5 Word8 |
| 68 | + deriving newtype (Eq, Ord, Num) |
| 69 | + |
| 70 | +instance Show Word5 where |
| 71 | + show (UnsafeWord5 w8) = show w8 |
| 72 | + |
| 73 | +instance Ix Word5 where |
| 74 | + range (UnsafeWord5 m, UnsafeWord5 n) = map UnsafeWord5 $ range (m, n) |
| 75 | + index (UnsafeWord5 m, UnsafeWord5 n) (UnsafeWord5 i) = index (m, n) i |
| 76 | + inRange (m, n) i = m <= i && i <= n |
| 77 | + |
| 78 | +-- | Convert an integer number into a five-bit word. |
| 79 | +word5 :: (Integral a) => a -> Word5 |
| 80 | +word5 x = UnsafeWord5 (fromIntegral x .&. 31) |
| 81 | +{-# INLINE word5 #-} |
| 82 | +{-# SPECIALIZE INLINE word5 :: Word8 -> Word5 #-} |
| 83 | + |
| 84 | +-- TODO: FIX ME: https://github.com/input-output-hk/cardano-wallet/pull/312 |
| 85 | + |
| 86 | +-- | Convert a five-bit word into a number. |
| 87 | +fromWord5 :: (Num a) => Word5 -> a |
| 88 | +fromWord5 (UnsafeWord5 x) = fromIntegral x |
| 89 | +{-# INLINE fromWord5 #-} |
| 90 | +{-# SPECIALIZE INLINE fromWord5 :: Word5 -> Word8 #-} |
| 91 | + |
| 92 | +-- | 'Bech32' character map as array of five-bit integers to character. |
| 93 | +charset :: Array Word5 Char |
| 94 | +charset = |
| 95 | + listArray (UnsafeWord5 0, UnsafeWord5 31) "qpzry9x8gf2tvdw0s3jn54khce6mua7l" |
| 96 | + |
| 97 | +-- | Convert a character to its five-bit value from 'Bech32' 'charset'. |
| 98 | +charsetMap :: Char -> Maybe Word5 |
| 99 | +charsetMap c |
| 100 | + | inRange (bounds inv) upperC = inv ! upperC |
| 101 | + | otherwise = Nothing |
| 102 | + where |
| 103 | + upperC = toUpper c |
| 104 | + inv = listArray ('0', 'Z') (repeat Nothing) // map swap (assocs charset) |
| 105 | + swap (a, b) = (toUpper b, Just a) |
| 106 | + |
| 107 | +-- | Calculate or validate 'Bech32' checksum. |
| 108 | +bech32Polymod :: [Word5] -> Word |
| 109 | +bech32Polymod values = foldl' go 1 values .&. 0x3fffffff |
| 110 | + where |
| 111 | + go chk value = |
| 112 | + foldl' xor chk' [g | (g, i) <- zip generator [25 ..], testBit chk i] |
| 113 | + where |
| 114 | + generator :: [Word] |
| 115 | + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] |
| 116 | + chk' = chk .<<. 5 `xor` fromWord5 value |
| 117 | + |
| 118 | +-- | Convert human-readable part of 'Bech32' string into a list of five-bit |
| 119 | +-- words. |
| 120 | +bech32HRPExpand :: HRP -> [Word5] |
| 121 | +bech32HRPExpand hrp = |
| 122 | + map (UnsafeWord5 . (.>>. 5)) hrpBytes |
| 123 | + ++ [UnsafeWord5 0] |
| 124 | + ++ map word5 hrpBytes |
| 125 | + where |
| 126 | + hrpBytes = B.unpack $ E.encodeUtf8 hrp |
| 127 | + |
| 128 | +-- | Calculate checksum for a string of five-bit words. |
| 129 | +bech32CreateChecksum :: HRP -> [Word5] -> [Word5] |
| 130 | +bech32CreateChecksum hrp dat = [word5 (polymod .>>. i) | i <- [25, 20 .. 0]] |
| 131 | + where |
| 132 | + values = bech32HRPExpand hrp ++ dat |
| 133 | + polymod = |
| 134 | + bech32Polymod (values ++ map UnsafeWord5 [0, 0, 0, 0, 0, 0]) `xor` 1 |
| 135 | + |
| 136 | +-- | Verify checksum for a human-readable part and string of five-bit words. |
| 137 | +bech32VerifyChecksum :: HRP -> [Word5] -> Bool |
| 138 | +bech32VerifyChecksum hrp dat = bech32Polymod (bech32HRPExpand hrp ++ dat) == 1 |
| 139 | + |
| 140 | +-- | Maximum length of a Bech32 result. |
| 141 | +maxBech32Length :: Int |
| 142 | +maxBech32Length = 90 |
| 143 | + |
| 144 | +-- | Encode string of five-bit words into 'Bech32' using a provided |
| 145 | +-- human-readable part. Can fail if 'HRP' is invalid or result would be longer |
| 146 | +-- than 90 characters. |
| 147 | +bech32Encode :: HRP -> [Word5] -> Maybe Bech32 |
| 148 | +bech32Encode hrp dat = do |
| 149 | + guard $ checkHRP hrp |
| 150 | + let dat' = dat ++ bech32CreateChecksum hrp dat |
| 151 | + rest = map (charset !) dat' |
| 152 | + result = T.concat [T.toLower hrp, T.pack "1", T.pack rest] |
| 153 | + guard $ T.length result <= maxBech32Length |
| 154 | + return result |
| 155 | + |
| 156 | +-- | Check that human-readable part is valid for a 'Bech32' string. |
| 157 | +checkHRP :: HRP -> Bool |
| 158 | +checkHRP hrp = not (T.null hrp) && T.all (\char -> char >= '\x21' && char <= '\x7e') hrp |
| 159 | + |
| 160 | +-- | Decode human-readable 'Bech32' string into a human-readable part and a |
| 161 | +-- string of five-bit words. |
| 162 | +bech32Decode :: Bech32 -> Maybe (HRP, [Word5]) |
| 163 | +bech32Decode bech32 = do |
| 164 | + guard $ T.toUpper bech32 == bech32 || lowerBech32 == bech32 |
| 165 | + let (hrp, dat) = T.breakOnEnd "1" lowerBech32 |
| 166 | + guard $ T.length dat >= 6 |
| 167 | + hrp' <- T.stripSuffix "1" hrp |
| 168 | + guard $ checkHRP hrp' |
| 169 | + dat' <- mapM charsetMap $ T.unpack dat |
| 170 | + guard $ bech32VerifyChecksum hrp' dat' |
| 171 | + return (hrp', take (T.length dat - 6) dat') |
| 172 | + where |
| 173 | + lowerBech32 = T.toLower bech32 |
| 174 | + |
| 175 | +type Pad f = Int -> Int -> Word -> [[Word]] -> f [[Word]] |
| 176 | + |
| 177 | +yesPadding :: Pad Identity |
| 178 | +yesPadding _ 0 _ result = return result |
| 179 | +yesPadding _ _ padValue result = return $ [padValue] : result |
| 180 | +{-# INLINE yesPadding #-} |
| 181 | + |
| 182 | +noPadding :: Pad Maybe |
| 183 | +noPadding frombits bits padValue result = do |
| 184 | + guard $ bits < frombits && padValue == 0 |
| 185 | + return result |
| 186 | +{-# INLINE noPadding #-} |
| 187 | + |
| 188 | +-- | Big endian conversion of a bytestring from base \(2^{frombits}\) to base |
| 189 | +-- \(2^{tobits}\). {frombits} and {twobits} must be positive and |
| 190 | +-- \(2^{frombits}\) and \(2^{tobits}\) must be smaller than the size of Word. |
| 191 | +-- Every value in 'dat' must be strictly smaller than \(2^{frombits}\). |
| 192 | +convertBits :: (Functor f) => [Word] -> Int -> Int -> Pad f -> f [Word] |
| 193 | +convertBits dat frombits tobits pad = concat . reverse <$> go dat 0 0 [] |
| 194 | + where |
| 195 | + go [] acc bits result = |
| 196 | + let padValue = (acc .<<. (tobits - bits)) .&. maxv |
| 197 | + in pad frombits bits padValue result |
| 198 | + go (value : dat') acc bits result = |
| 199 | + go dat' acc' (bits' `rem` tobits) (result' : result) |
| 200 | + where |
| 201 | + acc' = (acc .<<. frombits) .|. value |
| 202 | + bits' = bits + frombits |
| 203 | + result' = |
| 204 | + [ (acc' .>>. b) .&. maxv |
| 205 | + | b <- [bits' - tobits, bits' - 2 * tobits .. 0] |
| 206 | + ] |
| 207 | + maxv = (1 .<<. tobits) - 1 |
| 208 | +{-# INLINE convertBits #-} |
| 209 | + |
| 210 | +-- | Convert from eight-bit to five-bit word string, adding padding as required. |
| 211 | +toBase32 :: [Word8] -> [Word5] |
| 212 | +toBase32 dat = |
| 213 | + map word5 $ runIdentity $ convertBits (map fromIntegral dat) 8 5 yesPadding |
| 214 | + |
| 215 | +-- | Convert from five-bit word string to eight-bit word string, ignoring padding. |
| 216 | +toBase256 :: [Word5] -> Maybe [Word8] |
| 217 | +toBase256 dat = |
| 218 | + map fromIntegral <$> convertBits (map fromWord5 dat) 5 8 noPadding |
| 219 | + |
| 220 | +toBase256' :: [Word5] -> [Word8] |
| 221 | +toBase256' dat = |
| 222 | + map fromIntegral $ runIdentity $ convertBits (map fromWord5 dat) 5 8 yesPadding |
| 223 | + |
| 224 | +-- | Check if witness version and program are valid. |
| 225 | +segwitCheck :: Word8 -> Data -> Bool |
| 226 | +segwitCheck witver witprog = |
| 227 | + witver <= 16 |
| 228 | + && if witver == 0 |
| 229 | + then length witprog == 20 || length witprog == 32 |
| 230 | + else length witprog >= 2 && length witprog <= 40 |
| 231 | + |
| 232 | +-- | Decode SegWit 'Bech32' address from a string and expected human-readable part. |
| 233 | +segwitDecode :: HRP -> Bech32 -> Maybe (Word8, Data) |
| 234 | +segwitDecode hrp addr = do |
| 235 | + (hrp', dat) <- bech32Decode addr |
| 236 | + guard $ (hrp == hrp') && not (null dat) |
| 237 | + case dat of |
| 238 | + [] -> error "empty UnsafeWord5" |
| 239 | + UnsafeWord5 witver : datBase32 -> do |
| 240 | + decoded <- toBase256 datBase32 |
| 241 | + guard $ segwitCheck witver decoded |
| 242 | + return (witver, decoded) |
| 243 | + |
| 244 | +-- | Encode 'Data' as a SegWit 'Bech32' address. Needs human-readable part and |
| 245 | +-- witness program version. |
| 246 | +segwitEncode :: HRP -> Word8 -> Data -> Maybe Text |
| 247 | +segwitEncode hrp witver witprog = do |
| 248 | + guard $ segwitCheck witver witprog |
| 249 | + bech32Encode hrp $ UnsafeWord5 witver : toBase32 witprog |
0 commit comments